小言_互联网的博客

建立简易的TCP服务器和TCP客户端程序(附代码及解析、可运行)

344人阅读  评论(0)

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

根据RFC793的定义:端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)。

#pragma once

#define WIN32_LEAN_AND_MEAN

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <WinSock2.h>

#include <Windows.h>

#include <stdio.h>

 

#pragma comment(lib,"ws2_32.lib") //这是一种添加ws2_32.lib的方法,只适用于windows平台

//另外一种方法是在项目--》属性--》链接器--》输入--》附加依赖项中,添加 ws2_32.lib

 

int main() {

//启动Windows socket 2.x 环境

WORD ver = MAKEWORD(2, 2);

WSADATA dat;

WSAStartup(ver, &dat);

 

//---------------------------

//-- 用Socket API建立简易TCP服务端

//1.建立一个socket 套节字描述符

SOCKET _sock= socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

//2.bind绑定用于接受客户端连接的网络端口

sockaddr_in _sin = {};

_sin.sin_family = AF_INET;

_sin.sin_port = htons(4567);//主机字节序转为网络字节序host to net unsigned short

_sin.sin_addr.S_un.S_addr = INADDR_ANY;//inet_addr("127.0.0.1"); 点分十进制的IP地址转成一个长整数型数

if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in))) {

printf("错误:绑定网络端口失败\n");

}

else {

printf("绑定网络端口成功\n");

}

//3.listen 监听网络端口

if (SOCKET_ERROR == listen(_sock, 5)) {

printf("错误:监听网络端口失败\n");

}

else {

printf("监听网络端口成功\n");

}

//4.accept等待接受客户端连接

sockaddr_in clientAddr = {};

int nAddrLen = sizeof(sockaddr_in);

SOCKET _cSock = INVALID_SOCKET;

char msgBuf[] = "Hello, I'm Server.\n";

//5.send向客户端发送一条数据

int len = strlen(msgBuf)+1;

while (true) {

_cSock = accept(_sock, (const sockaddr*)&clientAddr, &nAddrLen);

if (INVALID_SOCKET == _cSock) {

printf("错误:接收到无效客户端SOCKET...\n");

}

printf("新客户端加入:IP = %s \n",inet_ntoa(clientAddr.sin_addr));

send(_cSock, msgBuf, len, 0);

}

//6.关闭套节字 closesocket

closesocket(_sock);

//---------------------------

//清除 Windows socket环境

WSACleanup();

 

return 0;

}

 

socket API 简单介绍

1. socket() 创建socket描述符

int socket(int domain, int type, int protocol);

//成功返回非负描述符,失败返回-1

domain:即协议域,又称为协议族(family)。

常用的地址族有:

AF_INET

AF_INET6

AF_LOCAL(AF_UNIX,本地通信用)

AF_ROUTE

协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

 

type:信息传送方式。

SOCK_STREAM

SOCK_DGRAM

SOCK_RAW

SOCK_PACKET

SOCK_SEQPACKET

 

protocol:对应协议。

IPPROTO_TCP TCP传输协议

IPPROTO_UDP UDP传输协议

IPPROTO_SCTP STCP传输协议

IPPROTO_TIPCTIPC传输协议

 

通常设置为0,让其自动匹配。

我的理解就是:

domain 网络层相关协议 type 信息传送方式 protocol 运输层相关协议,如果前两个都确定了,系统就可以自动选择协议了。

 

2. bind()绑定实际地址

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//返回值:成功则为0,失败为-1

 

2.1 sockfd 一般服务端才需要绑定,客户端由系统内核解决

addr 所有协议都有一个公共的结构叫做const struct sockaddr,不同协议对应不同的具体结构,结构如下:

 

struct sockaddr {

sa_family_t sin_family;//地址族

   char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息

   };

 

可以看出,我们端口和目标地址放在同一数组中,不太容易使用。对于不同的协议,我们会通过另一种特定的结构体的来完成初始化,再通过强制类型转换,来使用bind函数。

 

2.2 强制转换第二个地址参数为const sockaddr *

 

(const sockaddr *)sockaddr_in;

(const sockaddr *)sockaddr_in6;

(const sockaddr *)sockaddr_un;

 

ipv4结构体

struct sockaddr_in {

sa_family_t sin_family;

in_port_t sin_port; //typedef __uint16_t in_port_t;

struct in_addr sin_addr;

};

 

struct in_addr {

uint32_t s_addr;

};

 

ipv6结构体

struct sockaddr_in6 {

sa_family_t sin6_family;

in_port_t sin6_port;

uint32_t sin6_flowinfo;

struct in6_addr sin6_addr;

uint32_t sin6_scope_id;

};

 

struct in6_addr {

unsigned char s6_addr[16];

};

 

Unix域结构体

#define UNIX_PATH_MAX 108

 

struct sockaddr_un {

sa_family_t sun_family;

char sun_path[UNIX_PATH_MAX];

};

 

2.3 addrlen:协议结构体大小

通常使用sizeof运算符计算

sizeof(sockaddr_in);//IPv4 in:internet

sizeof(sockaddr_in6);//IPv6

sizeof(sockaddr_un);//本地 un:unix

 

3. listen()主机监听、connect()从机链接

int listen(int sockfd, int backlog);

//返回值:成功则为0,失败为-1

 

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//返回值:成功则为0,失败为-1

 

listen决定需要开启的部门热线,connect拨打电话。

 

listen函数的第一个参数即为要监听的socket描述符;

第二个参数为相应socket可以排队的最大连接个数(一般为5)。

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

 

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

 

4 accept 建立链接

只要服务端建立链接,我们就可以进行操作了

 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

//返回值:成功则为0,失败为-1

 

第一个参数为服务器的socket描述字,

第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址

第三个参数为协议地址的长度。

 

如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。后面的读写操作都是根据这个返回值来完成的。

 

5. read和write函数,读写

这不是属于socket的API,但是socket总是伴随着以下函数:

 

ssize_t read(int fd, void *buf, size_t count);

ssize_t write(int fd, const void *buf, size_t count);

 

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

 

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,

const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,

struct sockaddr *src_addr, socklen_t *addrlen);

 

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

 

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

 

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是 全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。

 

close关闭服务

int close(int fd);

//返回值:成功则为0,失败为-1

 

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

 

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

 

#pragma once

#define WIN32_LEAN_AND_MEAN

#include <WinSock2.h>

#include <Windows.h>

#include <stdio.h>

 

int main() {

//启动Windows socket 2.x 环境

WORD ver = MAKEWORD(2, 2);

WSADATA dat;

WSAStartup(ver, &dat);

 

//---------------------------

//--用Socket API 建立简易TCP客户端

//1.建立一个socket套节字描述符

SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);

if (INVALID_SOCKET == _sock) {

printf("error: build socket failse.\n");

}

else {

printf("build socket success.\n");

}

//2.连接服务器 connect

sockaddr_in _sin = {};

_sin.sin_family = AF_INET;

_sin.sin_port = htons(4567);

_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

int ret = connect(_sock, (const sockaddr*)&_sin, sizeof(sockaddr_in));

if (SOCKET_ERROR == ret) {

printf("error: build socket failse.\n");

}

else {

printf("build socket success.\n");

}

//3.接收服务器信息 recv

char recvBuf[256] = {};

int nlen =recv(_sock, recvBuf, 256, 0);

if (nlen > 0) {

printf("receve data: %s \n",recvBuf);

}

//4.关闭套节字 closesocket

closesocket(_sock);

 

 

//清除 Windows socket环境

WSACleanup();

getchar();

return 0;

}

 


转载:https://blog.csdn.net/weixin_37910058/article/details/101293201
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场