win32 socket通信


一般分为Server端(服务端)和Client端(客户端)

基本架构

按协议来划分分为七层

  • 应用层:各种应用,如VirtualBox,QQ等各种应用
  • 表示层:代码转换,数据的加密等
  • 会话层:用户与服务端,用户与用户之间的链接等
  • 传输层:数据的传输(TCP,UDP等协议)
  • 网络层:数据选择端口,IP地址等
  • 数据链路层
  • 物理层

按架构来划分为五层

  • 应用层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

SOCKET套接字

源IP与目标IP,源端口与目标端口组合为套接字

socket():创建套接字。

bind():指定本地地址。一个套接字用socket()创建后,它其实还没有与任何特定的本地或目的地址相关联。在很多情况下,应用程序并不关心它们使用的本地地址,这时就可以不用调用bind指定本地的地址,而由协议软件为它们选择一个。但是,在某个知名端口(Well-known Port)上操作的服务器进程必须要对系统指定本地端口。所以一旦创建了一个套接字,服务器就必须使用bind()系统调用为套接字建立一个本地地址。

connect():将套接字连接到目的地址。初始创建的套接字并未与任何外地目的地址关联。客户机可以调用connect()为套接字绑定一个永久的目的地址,将它置于已连接状态。对数据流方式的套接字,必须在传输数据前,调用connect()构造一个与目的地的TCP连接,并在不能构造连接时返回一个差错代码。如果是数据报方式,则不是必须在传输数据前调用connect。如果调用了connect(),也并不像数据流方式那样发送请求建连的报文,而是只在本地存储目的地址,以后该socket上发送的所有数据都送往这个地址,程序员就可以免去为每一次发送数据都指定目的地址的麻烦。

listen():设置等待连接状态。对于一个服务器的程序,当申请到套接字,并调用bind()与本地地址绑定后,就应该等待某个客户机的程序来要求连接。listen()就是把一个套接字设置为这种状态的函数。

accept():接受连接请求。服务器进程使用系统调用socket,bind和listen创建一个套接字,将它绑定到知名的端口,并指定连接请求的队列长度。然后,服务器调用accept进入等待状态,直到到达一个连接请求。

send()/recv()sendto()/recvfrom():发送和接收数据 。在数据流方式中,一个连接建立以后,或者在数据报方式下,调用了connect()进行了套接字与目的地址的绑定后,就可以调用send()和reev()函数进行数据传输。

closesocket():关闭套接字。

socket函数

//创建套接字 socket()任何用户要进行通信都必须创建套接字,创建套接字是通过系统调用socket()函数实现的。
SOCKET PASCAL FAR socket (_In_ int af,        //协议族
                          _In_ int type,      //表示类型
                          _In_ int protocol); //指定协议

af指定套接字使用的协议族。也就是说,利用它来分辨地址的类型。

UNIX支持的协议族有:UNIXDomain(AF_UNIX)、In-temet(AF_INET)、XeroxNS(AF_NS)等。

而Dos和Windows仅支持AF_INET。

type参数指定所需的通信类型。包括数据流(SOCK_STREAM)、数据报(SOCK-DGRAM)和原始类型(S0CK_RAW)。

protocol说明该套接字使用的协议族中的特定协议。如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。

描述
IPPROTO_IP0 虚拟IP
IPPROTO_ICMP1 控制消息协议
IPPROTO_IGMP2 组管理协议
IPPROTO_GGP3 网关(以弃用)
IPPROTO_TCP6 TCP传输协议
IPPROTO_PUP12 PUP传输协议
IPPROTO_UDP17 UDP用户数据报协议
IPPROTO_IDP22
IPPROTO_ND77

该函数调用成功,返回一个套接字描述符SOCKET,该函数调用失败,返回值为INVALID_SOCKET。

SOCKADDR_IN结构体

typedef struct sockaddr_in {
#if(_WIN32_WINNT < 0x0600)
    short   sin_family;
#else //(_WIN32_WINNT < 0x0600)
    ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)
    USHORT sin_port;
    IN_ADDR sin_addr;
    CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

bind()函数

int PASCAL FAR bind( SOCKET sockaddr,                  //表示已经建立的socket编号
                    const struct sockaddr FAR* my_addr,//指向sockaddr结构体类型的指针
                    int addrlen);//表示my_addr结构的长度,可以用sizeof操作符获得

如无错误发生,则bind()返回0。

否则的话,将返回-1,应用程序可通过WSAGetLastError()获取相应错误代码。

描述
WSANOTINITIALISED在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN套接口实现检测到网络子系统失效。
WSAEADDRINUSE所定端口已在使用中(参见setoption()中的SO_REUSEADDR选项)。
WSAEFAULTnamelen参数太小(小于sockaddr结构的大小)。
WSAEINPROGRESS一个阻塞的套接口调用正在运行中。
WSAEAFNOSUPPORT本协议不支持所指定的地址族。
WSAEINVAL该套接口已与一个地址捆绑。
WSAENOBUFS无足够可用缓冲区,连接过多。
WSAENOTSOCK描述字不是一个套接口。

connect()函数

 int connect(SOCKET s,                    //标识一个未连接socket
             const struct sockaddr * name,//指向要连接套接字的sockaddr结构体的指针
             int namelen);                //sockaddr结构体的字节长度

返回的错误码解释

描述
EBADF参数sockfd 非合法socket处理代码
EFAULT参数serv_addr指针指向无法存取的内存空间
ENOTSOCK参数sockfd为一文件描述词,非socket。
EISCONN参数sockfd的socket已是连线状态
ECONNREFUSED连线要求被server端拒绝。
ETIMEDOUT企图连线的操作超过限定时间仍未有响应。
ENETUNREACH无法传送数据包至指定的主机。
EAFNOSUPPORTsockaddr结构的sa_family不正确。
EALREADYsocket为不可阻塞且先前的连线操作还未完成。

listen()函数

int listen(SOCKET sockfd, //一个已绑定未被连接的套接字描述符
           int backlog);  //连接请求队列的最大长度一般2到4,用SOMAXCONN则由系统确定。

无错误,返回0,

否则,返回SOCKET ERROR,windows上可以调用函数WSAGetLastError取得错误代码,在Linux可使用errno。

accept()函数

SOCKET accept(int sockfd,            //套接字描述符,该套接口在listen()后监听连接。
              struct sockaddr *addr, //指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址,Addr参数的实际格式由套接口创建时所产生的地址族确定。
              socklen_t *addrlen);   //(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数

如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。

addrlen所指的整形数初始时包含addr所指地址空间的大小,在返回时它包含实际返回地址的字节长度。

send()/recv()和sendto()/recvfrom()函数

send()函数

向一个已连接的套接口发送数据。

适用于已连接的数据包或流式套接口发送数据。对于数据报类套接口,必需注意发送数据长度不应超过通讯子网的IP包最大长度。IP包最大长度在WSAStartup()调用返回的WSAData的iMaxUdpDg元素中。如果数据太长无法自动通过下层协议,则返回WSAEMSGSIZE错误,数据不会被发送。

//send()函数
int PASCAL FAR send(SOCKET s,            //一个用于标识已连接套接口的描述字
                    const char FAR* buf, //包含待发送数据的缓冲区
                    int len,             //缓冲区中数据的长度
                    int flags);          //调用执行方式

若无错误发生,send()返回所发送数据的总数(请注意这个数字可能小于len中所规定的大小)。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

flags 参数有如下的选择:

描述
MSG_DONTROUTE勿将数据路由出本地网络
MSG_DONTWAIT允许非阻塞操作(等价于使用O_NONBLOCK)
MSG_EOR如果协议支持,此为记录结束
MSG_OOB如果协议支持,发送带外数据
MSG_NOSIGNAL禁止向系统发送异常信息

recv()函数

本函数用于已连接的数据报或流式套接口进行数据的接收。

不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符;

int recv(_In_ SOCKET s,   //指定接收端套接字描述符
         _Out_ char *buf, //指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
         _In_ int len,    //指明buf的长度
         _In_ int flags); //这个参数一般置0。
/*
第四个参数:
MSG_PEEK 查看当前数据。数据将被复制到缓冲区中,但并不从输入队列中删除。
MSG_OOB 处理[带外数据](https://baike.baidu.com/item/带外数据)*/

若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

sendto()函数

指向一指定目的地发送数据,sendto()适用于发送未建立连接的UDP数据包 (参数为SOCK_DGRAM)。

int PASCAL FAR sendto (_In_ SOCKET s,//套接字
                       _In_reads_bytes_(len) const char FAR * buf,//待发送数据的缓冲区
                       _In_ int len,//缓冲区长度
                       _In_ int flags,//调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
                       _In_reads_bytes_opt_(tolen) const struct sockaddr FAR *to,//(可选)指针,指向目的套接字的地址
                       _In_ int tolen);//所指地址的长度

返回为整型,如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。

recvfrom()函数

本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。

int PASCAL FAR recvfrom (_In_ SOCKET s,             //标识一个已连接套接口的描述字。
                         char FAR * buf,            //接收数据缓冲区。
                         _In_ int len,              //缓冲区长度。
                         _In_ int flags,            //调用操作方式。0表示一次收发
                         struct sockaddr FAR * from,//(可选)指针,指向保存来源地址的缓冲区。
                         int FAR * fromlen);        //(可选)指针,指向from缓冲区长度值。

若无错误发生,recvfrom()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

与recv()函数的比较:UDP使用recvfrom()函数接收数据,他类似于标准的read(),但是在recvfrom()函数中要指明目的地址。从套接字上接收一个消息。对于recvfrom ,可同时应用于面向连接的和无连接的套接字。recv一般只用在面向连接的套接字,几乎等同于recvfrom,只要将recvfrom的第五个参数设置NULL。不管是recv还是recvfrom,都有两种模式,阻塞和非阻塞,可以通过ioctl函数来设置。阻塞模式是一直等待直到有数据到达,非阻塞模式是立即返回,需要通过消息,异步事件等来查询完成状态。


TCP通信

建立稳定链接(c/s)模型,一对一

TCP的状态转换:可靠传输,双工操作,面向连接,端到端的传输,可以用于传输大量的数据,确保数据的安全。

服务端步骤

  1. 准备工作,头文件等<winsock2.h> ,#pragma comment(lib,“ws2_32.lib”)
  2. 确定版本信息,确定socket版本,IPV4与IPV6
  3. 创建socket
  4. 初始化协议地址族
  5. 绑定(吧协议地址族与socket绑定在一起)
  6. 监听
  7. 服务端:接受链接
  8. 开始通讯
  9. 关闭socket

服务端代码演示如下

//1.头文件
#include <stdio.h>
#include <Winsock2.h>
#pragma comment (lib,"ws2_32.lib")


int main()
{
	//2.确定版本信息
	WSADATA wsaData;
	//异步套接字的启动命令
	//参数一:请求哪个版本,高阶字段指的修订版本,低阶字段指的是主版本号
	//参数二:是一个结构体,用来接受socket实现的细节
	WSAStartup(MAKEWORD(2, 2),  //版本请求,2.2
		&wsaData);      //wsaData保存版本信息的结构体
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		printf("请求版本失败!\n");
		return -1;
	}
	printf("请求版本成功!\n");
	
    //3.创建socket
	//参数一:协议族,表示当前socket要用什么类型的地址和端口,AF_INET表示用IPV4地址(32位),端口号16位
	//参数二:表示类型,需要指定为SOCK_STREAM,流式SOCKET,面向连接
	//参数三:制定协议,IPPROTO_TCP,表示TCP协议
	SOCKET serverScoket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if (INVALID_SOCKET == serverScoket)
	{
		printf("创建套接字失败!\n");
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("创建套接字成功!\n");
	
    //4.初始化协议地址族
	SOCKADDR_IN serverAddr = { 0 };   //初始化协议地址
	serverAddr.sin_family = AF_INET;  //此参数必须和上步骤中创建socket一致
	//端口的形式网络传输中和PC的端口存储方式不一致
	//小端:先存低位再存高位   PC中存储是如此
	//大端:先存高位再存低位   网络传输如此
	//所以需要通过函数htons把PC端口的数值转换为网络端口的数值
	serverAddr.sin_port = htons(8888);//指定端口号
	//因为IP是点分格式的字符串,所以需要用inet_addr来转换为整数
	serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.2.103"); //指定服务端IP

	//5.绑定
	//参数一:socket的名字
	//参数二:协议地址族的首地址
	//参数三:协议地址族的长度
	//返回值:返回绑定状态
	if (SOCKET_ERROR == bind(serverScoket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)))
	{
		printf("绑定失败!\n");
		closesocket(serverScoket);//关闭套接字socket
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("绑定成功!\n");

	//6.监听
	//参数一:监听的socket
	//参数二:监听的个数,是等待连接的队列最大长度
	if (SOCKET_ERROR == listen(serverScoket, 10))
	{
		printf("监听失败!\n");
		closesocket(serverScoket);//关闭套接字socket
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("监听成功!\n");

	//7.服务端接受链接
	//参数一:连接的socket
	//参数二:可以给nullptr,表示不保存连接进来的客户端的ip地址等信息
	//参数三:可以给nullptr
	//如果2,3 不给nullptr,表示保存连接进来的客户端ip地址信息
	SOCKADDR_IN clientAddr = { 0 }; //用来保存客户端的信息
	int len = sizeof(clientAddr);
	SOCKET clientSocket = accept(serverScoket, (sockaddr*)&clientAddr, &len);
	if (INVALID_SOCKET == clientSocket)
	{
		printf("接受链接失败!\n");
		closesocket(serverScoket);//关闭套接字socket
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("接受客户链接成功!\n");
	printf("客户ip为:%s", inet_ntoa(clientAddr.sin_addr));

	//8.开始通讯
	char recvbuff[1024] = {}; //接受数据
	char sendbuff[1024] = {}; //发送数据
	//参数一:代表客户端的socket,表示从客户端进行收取数据
	//参数二:接受的数据存放地址
	//参数三:接受数据的长度
	//参数四:表示收发方式,0表示默认,一次收完
	while (true)
	{
		//保存数据清空
		memset(recvbuff, 0, sizeof(recvbuff));
        //从客户端接受数据
		if (recv(clientSocket, recvbuff, sizeof(recvbuff) - 1, 0) > 0)
		{
			printf("客户说:%s\n", recvbuff);
		}
		else
		{
			break;
		}
		memset(sendbuff, 0, sizeof(sendbuff));
		printf("我说:");
		scanf_s("%s", sendbuff,sizeof(sendbuff)-1);
        //发送数据给客户端
		send(clientSocket, sendbuff, strlen(sendbuff), 0);
	}

	//9.关闭链接
	closesocket(clientSocket);//关闭客户端socket
	closesocket(serverScoket);//关闭服务端socket
	WSACleanup();             //关闭套接字请求

	return 0;
}

客户端步骤

  1. 准备工作,头文件等<winsock2.h> ,#pragma comment(lib,“ws2_32.lib”)
  2. 确定版本信息,确定socket版本,IPV4与IPV6
  3. 创建socket
  4. 初始化协议地址族
  5. 链接
  6. 开始通讯(发送send或者接受recv数据)
  7. 关闭socket

代码演示如下

//1.头文件
#include <stdio.h>
#include <Winsock2.h>
#pragma comment (lib,"ws2_32.lib")


int main()
{
	//2.确定版本信息
	WSADATA wsaData;
	//异步套接字的启动命令
	//参数一:请求哪个版本,高阶字段指的修订版本,低阶字段指的是主版本号
	//参数二:是一个结构体,用来接受socket实现的细节
	WSAStartup(MAKEWORD(2, 2),  //版本请求,2.2
		&wsaData);      //wsaData保存版本信息的结构体
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		printf("请求版本失败!");
		return -1;
	}
	printf("请求版本成功!");
	
    //3.创建socket
	//参数一:协议族,表示当前socket要用什么类型的地址和端口,AF_INET表示用IPV4地址(32位),端口号16位
	//参数二:表示类型,需要指定为SOCK_STREAM,流式SOCKET,面向连接
	//参数三:制定协议,IPPROTO_TCP,表示TCP协议
	SOCKET clientScoket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == clientScoket)
	{
		printf("创建套接字失败!\n");
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("创建套接字成功!\n");
	
    //4.初始化协议地址族
	SOCKADDR_IN clientrAddr = { 0 };  //初始化协议地址
	clientrAddr.sin_family = AF_INET; //此参数必须和上步骤中创建socket一致
									  //端口的形式网络传输中和PC的端口存储方式不一致
									  //小端:先存低位再存高位   PC中存储是如此
									  //大端:先存高位再存低位   网络传输如此
									  //所以需要通过函数htons把PC端口的数值转换为网络端口的数值
	clientrAddr.sin_port = htons(8888);//指定端口号
									  //因为IP是点分格式的字符串,所以需要用inet_addr来转换为整数
	clientrAddr.sin_addr.S_un.S_addr = inet_addr("192.168.2.103"); //指定服务端IP   客户端链接的也是服务端的IP

	//5.链接
	if (SOCKET_ERROR == connect(clientScoket, (SOCKADDR *)&clientrAddr, sizeof(clientrAddr)))
	{
		printf("链接失败!\n");
		closesocket(clientScoket);//关闭客户端socket
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("链接成功!\n");
	//接受服务器发送的数据
	/*int len = sizeof(clientrAddr);
	SOCKET servertSocket = accept(clietnScoket, (sockaddr*)&clientrAddr, &len);*/
	//6.发送
	char sendBuff[1024] = { 0 };
	char recvBuff[1024] = { 0 };
	while (true)
	{
        memset(sendBuff, 0, sizeof(sendBuff));
		scanf_s("%s", sendBuff, sizeof(sendBuff) - 1);
        //发送数据到服务器
		if (send(clientScoket, sendBuff, strlen(sendBuff), 0) > 0)
		{
			
		}
		else
		{
			break;
		}
		memset(recvBuff, 0, sizeof(recvBuff));
        //从服务器接受数据
		if (recv(clientScoket, recvBuff, sizeof(recvBuff) - 1, 0) > 0)
		{
			printf("服务器说:%s\n", recvBuff);
		}
		
	
	}

	//7.关闭链接
	closesocket(clientScoket);//关闭客户端socket
	WSACleanup();             //关闭套接字请求

	return 0;
}

TCP的11种状态

单工:A可以发B,B不能发给A,类似收音机

半双工:A可以发B,B可以发A,但不能同时,同时会冲突,类似对讲机

双工:A可以发B,B可以发A,且可以同时互相发送,类似电话

链接:3次握手

  1. 客户端向服务端发送信息(报文),SYN,同步标识

  2. 服务端把上步客户端发送的信息SYN发回客户端,再发送一个报文ACK(确认报文)

  3. 客户端回复服务端ACK代表收到

注:服务端是不会主动向客户端发送链接

退出:4次挥手

  1. 客户端发FIN(结束标识)
  2. 服务端收到FIN,发回ACK
  3. 服务端关闭客户端的连接,并再发一个FIN给客户端
  4. 客户端发回ACK确认断开链接

11种状态如下

图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KqZgffA3-1588004270823)(L:\C++\TCP.png)]

简单解释:

l CLOSED:初始状态,表示TCP连接是“关闭着的”或“未打开的”。

l LISTEN :表示服务器端的某个SOCKET处于监听状态,可以接受客户端的连接。

l SYN_RCVD :表示服务器接收到了来自客户端请求连接的SYN报文。在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat很难看到这种状态,除非故意写一个监测程序,将三次TCP握手过程中最后一个ACK报文不予发送。当TCP连接处于此状态时,再收到客户端的ACK报文,它就会进入到ESTABLISHED 状态。

l SYN_SENT :这个状态与SYN_RCVD 状态相呼应,当客户端SOCKET执行connect()进行连接时,它首先发送SYN报文,然后随即进入到SYN_SENT 状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT 状态表示客户端已发送SYN报文。

l ESTABLISHED :表示TCP连接已经成功建立。

l FIN_WAIT_1 :这个状态得好好解释一下,其实FIN_WAIT_1 和FIN_WAIT_2 两种状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET进入到FIN_WAIT_1 状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2 状态。当然在实际的正常情况下,无论对方处于任何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1 状态一般是比较难见到的,而FIN_WAIT_2 状态有时仍可以用netstat看到。

l FIN_WAIT_2 :上面已经解释了这种状态的由来,实际上FIN_WAIT_2状态下的SOCKET表示半连接,即有一方调用close()主动要求关闭连接。注意:FIN_WAIT_2 是没有超时的(不像TIME_WAIT 状态),这种状态下如果对方不关闭(不配合完成4次挥手过程),那这个 FIN_WAIT_2 状态将一直保持到系统重启,越来越多的FIN_WAIT_2 状态会导致内核crash。

l TIME_WAIT :表示收到了对方的FIN报文,并发送出了ACK报文。 TIME_WAIT状态下的TCP连接会等待2*MSL(Max Segment Lifetime,最大分段生存期,指一个TCP报文在Internet上的最长生存时间。每个具体的TCP协议实现都必须选择一个确定的MSL值,RFC 1122建议是2分钟,但BSD传统实现采用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本机的这个值),然后即可回到CLOSED 可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(这种情况应该就是四次挥手变成三次挥手的那种情况)

l CLOSING :这种状态在实际情况中应该很少见,属于一种比较罕见的例外状态。正常情况下,当一方发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING 状态表示一方发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?那就是当双方几乎在同时close()一个SOCKET的话,就出现了双方同时发送FIN报文的情况,这是就会出现CLOSING 状态,表示双方都正在关闭SOCKET连接。

l CLOSE_WAIT :表示正在等待关闭。怎么理解呢?当对方close()一个SOCKET后发送FIN报文给自己,你的系统毫无疑问地将会回应一个ACK报文给对方,此时TCP连接则进入到CLOSE_WAIT状态。接下来呢,你需要检查自己是否还有数据要发送给对方,如果没有的话,那你也就可以close()这个SOCKET并发送FIN报文给对方,即关闭自己到对方这个方向的连接。有数据的话则看程序的策略,继续发送或丢弃。简单地说,当你处于CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。

l LAST_ACK :当被动关闭的一方在发送FIN报文后,等待对方的ACK报文的时候,就处于LAST_ACK 状态。当收到对方的ACK报文后,也就可以进入到CLOSED 可用状态了。


UDP通信

面向非链接,少量的数据传输,速度快。

服务端步骤

  1. 准备工作(头文件和所必须的库等)
  2. 确定版本信息
  3. 创建SOCKET
  4. 初始化协议地址族
  5. 绑定
  6. 通讯
  7. 关闭链接

服务端代码演示如下

//1.头文件
#include <stdio.h>
#include <Winsock2.h>
#pragma comment (lib,"ws2_32.lib")


int main()
{
	//2.确定版本信息
	WSADATA wsaData;
	//异步套接字的启动命令
	//参数一:请求哪个版本,高阶字段指的修订版本,低阶字段指的是主版本号
	//参数二:是一个结构体,用来接受socket实现的细节
	WSAStartup(MAKEWORD(2, 2),  //版本请求,2.2
		&wsaData);      //wsaData保存版本信息的结构体
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		printf("请求版本失败!\n");
		return -1;
	}
	printf("请求版本成功!\n");

	//3.创建socket
	//参数一:协议族,表示当前socket要用什么类型的地址和端口,AF_INET表示用IPV4地址(32位),端口号16位
	//参数二:表示类型,需要指定为SOCK_DGRAM,数据报文式socket,帧传输
	//参数三:制定协议,需要指定为IPPROTO_UDP,表示UDP协议
	SOCKET serverScoket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (INVALID_SOCKET == serverScoket)
	{
		printf("创建套接字失败!\n");
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("创建套接字成功!\n");

	//4.初始化协议地址族
	SOCKADDR_IN serverAddr = { 0 };   //初始化协议地址
	serverAddr.sin_family = AF_INET;  //此参数必须和上步骤中创建socket一致
									  //端口的形式网络传输中和PC的端口存储方式不一致
									  //小端:先存低位再存高位   PC中存储是如此
									  //大端:先存高位再存低位   网络传输如此
									  //所以需要通过函数htons把PC端口的数值转换为网络端口的数值
	serverAddr.sin_port = htons(8898);//指定端口号
									  //因为IP是点分格式的字符串,所以需要用inet_addr来转换为整数
	serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.2.103"); //指定服务端IP

																  //5.绑定
																  //参数一:socket的名字
																  //参数二:协议地址族的首地址
																  //参数三:协议地址族的长度
																  //返回值:返回绑定状态
	if (SOCKET_ERROR == bind(serverScoket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)))
	{
		printf("绑定失败!\n");
		closesocket(serverScoket);//关闭套接字socket
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("绑定成功!\n");

	//6.通讯
	char recvBuff[1024] = {}; //接受数据
	char sendBuff[1024] = {}; //发送数据
	SOCKADDR_IN clientAddr = { 0 }; //用来保存客户端的信息
	int len = sizeof(clientAddr);
	while (true)
	{
		//接受信息
		if (recvfrom(serverScoket, recvBuff, sizeof(recvBuff) - 1, 0, (sockaddr*)&clientAddr, &len)>0)
		{
			printf("发送信息的ip为:%s\n", inet_ntoa(clientAddr.sin_addr));
			printf("发送的数据:%s\n", recvBuff);
			memset(recvBuff, 0, sizeof(recvBuff));
		}
		//发送数据给客户端
		memset(sendBuff, 0, sizeof(sendBuff));
		scanf_s("%s", sendBuff, sizeof(sendBuff) - 1);
		//发送数据到服务器
		//sendto的第5,6个参数必须写,因为没有链接
		if (sendto(serverScoket, sendBuff, strlen(sendBuff), 0, (sockaddr*)&clientAddr, sizeof(clientAddr)) > 0)
		{
			printf("数据发送成功!\n");
		}
		else
		{
			break;
		}
	}


	//7.关闭链接
	closesocket(serverScoket);//关闭服务端socket
	WSACleanup();             //关闭套接字请求

	return 0;
}

客户端步骤

  1. 准备工作(头文件和所必须的库等)
  2. 确定版本信息
  3. 创建SOCKET
  4. 初始化协议地址族
  5. 通讯
  6. 关闭链接

代码演示如下

//1.头文件
#include <stdio.h>
#include <Winsock2.h>
#pragma comment (lib,"ws2_32.lib")


int main()
{
	//2.确定版本信息
	WSADATA wsaData;
	//异步套接字的启动命令
	//参数一:请求哪个版本,高阶字段指的修订版本,低阶字段指的是主版本号
	//参数二:是一个结构体,用来接受socket实现的细节
	WSAStartup(MAKEWORD(2, 2),  //版本请求,2.2
		&wsaData);      //wsaData保存版本信息的结构体
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		printf("请求版本失败!\n");
		return -1;
	}
	printf("请求版本成功!\n");

	//3.创建socket
	//参数一:协议族,表示当前socket要用什么类型的地址和端口,AF_INET表示用IPV4地址(32位),端口号16位
	//参数二:表示类型,需要指定为SOCK_DGRAM,数据报文式socket,帧传输
	//参数三:制定协议,需要指定为IPPROTO_UDP,表示UDP协议
	SOCKET clientScoket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (INVALID_SOCKET == clientScoket)
	{
		printf("创建套接字失败!\n");
		WSACleanup();             //关闭套接字请求
		return -1;
	}
	printf("创建套接字成功!\n");

	//4.初始化协议地址族
	SOCKADDR_IN serverAddr = { 0 };   //初始化协议地址
	serverAddr.sin_family = AF_INET;  //此参数必须和上步骤中创建socket一致
									  //端口的形式网络传输中和PC的端口存储方式不一致
									  //小端:先存低位再存高位   PC中存储是如此
									  //大端:先存高位再存低位   网络传输如此
									  //所以需要通过函数htons把PC端口的数值转换为网络端口的数值
	serverAddr.sin_port = htons(8898);//指定端口号
									  //因为IP是点分格式的字符串,所以需要用inet_addr来转换为整数
	serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.2.103"); //指定服务端IP

																  //5.通讯
	char recvBuff[1024] = {}; //接受数据
	char sendBuff[1024] = {}; //发送数据

							  //发送信息
	while (true)
	{
		memset(recvBuff, 0, sizeof(recvBuff));
		scanf_s("%s", sendBuff, sizeof(sendBuff) - 1);
		//发送数据到服务器
		//sendto的第5,6个参数必须写,因为没有链接
		if (sendto(clientScoket, sendBuff, strlen(sendBuff), 0, (sockaddr*)&serverAddr, sizeof(serverAddr)) > 0)
		{
			printf("数据发送成功!\n");
		}
		else
		{
			break;
		}
		//接受信息
		if (recvfrom(clientScoket, recvBuff, sizeof(recvBuff) - 1, 0, 0, 0)>0)
		{
			printf("发送信息的ip为:%s\n", inet_ntoa(serverAddr.sin_addr));
			printf("收到的数据:%s\n", recvBuff);
			memset(recvBuff, 0, sizeof(recvBuff));
		}


	}

	//7.关闭链接
	closesocket(clientScoket);//关闭客户端socket
	WSACleanup();             //关闭套接字请求

	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值