TCP.05.重叠IO模型:事件通知


https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于TCP/IP的网络编程有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型

这次先讲第四种。

重叠IO模型介绍

重叠IO是Windows提供的一种异步读写文件的机制。(前面讲的是事件机制和消息机制,重叠IO也是一种机制)
如果我们把网络发送消息,读取消息中的消息看成文件,那么SOCKET的本质就是文件操作。正常的读文件,例如recv,是阻塞的,等待协议缓冲区中的全部复制到我们自己定义的buff中,函数才能结束并返回复制的内容,如果多次调用recv函数,那么这些recv函数是依次一个个执行的。写(send)的过程也一样,同一时间只能执行一个send函数,其他的操作只能等。
重叠IO机制则是把上面描述的过程做成非阻塞操作,将的指令以及我们自定义的buff投递给操作系统,然后函数直接返回,由操作系统独立打开一个线程,将数据复制到buff,这个过程,我们的应用可以做别的事情,也就意味我们可以同时投递多个读或写指令,同时进行多个读写操作。
从代码上看,就是将原来的accept、recv、send函数,转化为可非阻塞执行的AcceptEx、WSARecv、WSASend函数。


关于异步读写这个我记得当年教SOCKET的时候有一个钓鱼的例子,不过书没找见了。


重叠IO的由来是由其结构体WSAOVERLAPPED得来
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaoverlapped

typedef struct _WSAOVERLAPPED {
  DWORD    Internal;
  DWORD    InternalHigh;
  DWORD    Offset;
  DWORD    OffsetHigh;
  WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

该结构体的前四个成员是保留给系统使用的,第五个成员WSAEVENT hEvent是事件对象的句柄。操作完事件句柄后,系统将WSAOVERLAPPED结构体中的第五个成员设置为有信号。
在使用的时候,我们就是将SOCKET和WSAOVERLAPPED绑定后,投递给操作系统,系统会以重叠IO机制处理反馈,反馈方式有两种:事件通知、完成例程。
对于事件通知而言:
1.调用AcceptEx WSARecv WSASend投递
2.操作系统将被完成的操作,事件信号置成有信号
3.调用WSAGetOverlappedResult获取事件信号

对于完成例程而言:
1.调用AcceptEx WSARecv WSASend投递
2.完成后自动调用回调函数

重叠IO模型:事件通知代码逻辑

先看事件通知的反馈方式
1.创建事件(optional)、SOCKET数组,重叠结构体数组(根据下标来进行对应,相同下标是一组)
2.创建重叠IO模型使用的SOCKET:WSASocket
3.投递AcceptEx
3.1立即完成,此时有客户端连接
3.1.1对客户端套接字投递WSARecv
3.1.1.1有客户端消息,系统空闲,立即完成,跳3.1.1
3.1.1.2无客户端消息,跳3.3

3.1.2根据需求对客户端套接字投递WSASend
3.1.2.1有消息要发送,系统空闲,立即完成,跳3.1.2
3.1.2.2无消息要发送,跳3.3

3.1.3如果需要连接客户端跳3

3.2延迟完成,此时没有客户端连接,跳3.3
3.3循环等待信号(WSAWaitForMultipleEvents)
3.3.1 没信号,继续等
3.3.2 有信号,先获取重叠结构上的信息(WSAGetOverlappedResult)
3.3.2.1 如果信号是服务器端上检测到有客户端连接,跳转3(这个步骤和3.3.2.2不可以调换顺序)
3.3.2.2 如果信号是客户端退出,则关闭客户端SOCKET,并从数组删除客户端的信息
3.3.2.3 如果信号是需要接收信息,跳转3.1.1
3.3.2.4 如果信号是需要发送信息,跳转3.1.2

说明:由于AcceptEx WSARecv WSASend每调用一次,只会处理一次,要处理多次就要多次调用,因此上面有跳转。

重叠IO模型:事件通知代码实现

1.-5.

和之前一样,重叠IO模型服务器端的SOCKET代码套路的前面部分是一样的:
1.打开网络库
2.校验版本
3.创建SOCKET 这里要用WSASocket来创建
再次强调:WSA是windows socket async的缩写

SOCKET WSAAPI WSASocketA(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOA lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

前面几个参数可以参考第一篇简单通信模型,这里不展开介绍。
参数1:地址类型
参数2:套接字类型
参数3:协议类型
参数4:协议属性设置,不用则设置为NULL,可以设置的内容包括:
发送数据是否需要连接
是否保证数据完整到达(选择TCP面向连接的协议或UDP面向传输的协议的处理方式)
参数3填0,那么匹配哪个协议在这里设置,貌似是一个列表,按列表的先后顺序进行匹配
设置传输接收字节数限制
设置套接字权限,设置后进行相应套接字操作的时候会有相应的提示,例如发送数据会触发某个操作,具体操作在参数6里面设置WSA_FLAG_ACCESS_SYSTEM_SECURITY
其他很多保留字段,供以后拓展使用
以上参数4的设置,实际是修改WSAPROTOCOL _INFO结构的指针,具体可以看:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaprotocol_infoa
参数5:保留参数,默认写0(看名字应该是组ID,可以一次操作多个socket,多/组播?)
参数6:指定套接字属性/设置。
这里用:WSA_FLAG_OVERLAPPED,表示创建重叠IO模型的SOCKET
还有别的一些宏:
WSA_FLAG_ACCESS_SYSTEM_SECURITY:套接字权限设置,配合参数4中的设置一起使用
WSA_FLAG_NO_HANDLE_INHERIT:套接字不可继承,这个在多线程SOCKET里面用。在多线程开发中,子线程会继承父线程的SOCKET,即主线程创了一个SOCKET,那么子线程有两种使用方式:

方式方法
共享直接用父类的,父子都使用这一个SOCKET(只用关闭一次)
继承(默认)把父类的这个SOCKET复制一份,自己用,这两父子用两个SOCKET,但是本质一样(要关闭两次)

多播(1vs.n)用的:
WSA_FLAG_MULTIPOINT_C_ROOT
WSA_FLAG_MULTIPOINT_C_LEAF
WSA_FLAG_MULTIPOINT_D_ROOT
WSA_FLAG_MULTIPOINT_D_LEAF

返回值:
成功返回可用的SOCKET句柄
失败返回INVALID_SOCKET
具体看:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa


重叠IO的名字由来:之前用Socket创建的句柄是阻塞的,因此recv,send操作都是轮流调用,二者之间没有交集;现在使用WSASocket创建的句柄是非阻塞的,对应的接收函数WSARecv可以被多次调用,发出接收缓冲区数据指令,准备接收到来的数据。发送函数WSASend也可以被多次调用,组成一个发送缓冲区队列,也就是发送操作和接收操作可以重叠使用,这也是为什么叫重叠IO的原因。


4.绑定地址与端口
5.开始监听
再往下的步骤就和基本框架有很大不同,详细写

6.投递AcceptEx

这个玩意的流程在上面有写,方便看就直接copy下来:
3.投递AcceptEx
3.1立即完成,此时有客户端连接
3.1.1对客户端套接字投递WSARecv
3.1.1.1有客户端消息,系统空闲,立即完成,跳3.1.1
3.1.1.2无客户端消息,跳3.3

3.1.2根据需求对客户端套接字投递WSASend
3.1.2.1有消息要发送,系统空闲,立即完成,跳3.1.2
3.1.2.2无消息要发送,跳3.3

3.1.3如果需要连接客户端跳3

3.2延迟完成,此时没有客户端连接,跳3.3
3.3循环等待信号(WSAWaitForMultipleEvents)
3.3.1 没信号,继续等
3.3.2 有信号,先获取重叠结构上的信息(WSAGetOverlappedResult)
3.3.2.1 如果信号是服务器端上检测到有客户端连接,跳转3(这个步骤和3.3.2.2不可以调换顺序)
3.3.2.2 如果信号是客户端退出,则关闭客户端SOCKET,并从数组删除客户端的信息
3.3.2.3 如果信号是需要接收信息,跳转3.1.1
3.3.2.4 如果信号是需要发送信息,跳转3.1.2

由于要对多个SOCKET进行操作,因此先要创建SOCKET数组和重叠结构体数组,套路和前面两节的选择模型一样的,数组+个数

#define MAX_COUNT 1024

SOCKET garr_sockAll[MAX_COUNT];//SOCKET数组
OVERLAPPED garr_olpAll[MAX_COUNT];//重叠结构体数组
int gi_count;//数组中SOCEKT的个数,全局变量默认值是0

然后把上面创建好的SOCKET放到数组里面去,事件也要初始化:

	garr_sockAll[gi_count] = socketServer;//初始化后的SOCKET放到SOCKET数组里面
	garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
	gi_count++;

接下来的投递代码为了简洁,单独将其写成一个函数。先来看看AcceptEx函数

//投递服务器SOCKET句柄,异步接收客户端连接
BOOL AcceptEx(
  SOCKET       sListenSocket,
  SOCKET       sAcceptSocket,
  PVOID        lpOutputBuffer,
  DWORD        dwReceiveDataLength,
  DWORD        dwLocalAddressLength,
  DWORD        dwRemoteAddressLength,
  LPDWORD      lpdwBytesReceived,
  LPOVERLAPPED lpOverlapped
);

参数1:服务器SOCKET句柄
参数2:接收的客户端SOCKET句柄,这个句柄要通过Socket或者WSASocket来创建,然后放在AcceptEx这里相当于原始模型中的accept+bind操作,将客户端的IP和端口号绑定到参数2上。
参数3:缓冲区的指针,接收在新连接上发送的第一个数据。意思是这里可以直接拿到客户端第一次send过来的数据,第二次和之后send的数据就要用WSARecv来接收了。这个参数是一个字符数组,不能设置为NULL。
参数4:上面参数3功能估计是程序猿打瞌睡弄出来bug,太恶搞了,都有recv干嘛还要设置这个功能,难道每个客户端都和服务器YYQ,只发送一次数据,然后为了偷懒弄出来的?因此参数4进行补救,如果参数4设置为0,那么参数3无效,但是不管参数4是否设置为0,参数3都不可设置为NULL。如果要使得参数3生效,参数4则要设置为参数3的长度,此时,客户端连接服务器并发送了一条消息后服务器才产生信号,才能recv到数据。
参数5:The number of bytes reserved for the local address information. This value must be at least 16 bytes more than the maximum address length for the transport protocol in use.因此固定写成:sizeof(struct sockaddr _in)+16
参数6:和参数5一样,只不过存在Remote端,固定写成:sizeof(struct sockaddr _in)+16
简要分析:由于SOCKET操作可以看做是文件操作,因此参数5可以看做是本地硬盘用的地址,参数6可以看做是内存用的地址。
参数7:配合参数3和参数4使用,如果第一次是在这里接收信息,而且立即执行(步骤3.1)接收到了信息,那么参数7将得到信息的长度。
如果不想获取信息的长度,填写NULL即可;否则填写DWORD变量的地址即可。
参数8:服务器SOCKET句柄对应的重叠结构体。注意不要写错为客户端的重叠结构体

返回值:
TRUE:表示执行完成就有客户端到了,accept成功
FALSE:出错,用int acceptexerr = WSAGetLastError()获取错误码并处理
情况1:acceptexerr ==ERROR_IO_PENDING,表示执行成功,但处于异步等待状态,或者此时还没有客户端请求连接;
情况2:其他错误码要根据情况解决。
注意:AcceptEx要加载自己的头文件和库文件:

#include <mswsock.h>//这个头文件要在#include <WinSock2.h>下面
#pragma comment(lib, "mswsock.lib")

//投递AcceptEx
int PostAccept()
{
	garr_sockAll[gi_count]=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
	garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化

	char str[1024] = {0};

	DWORD dwRecvCount = 0;

	BOOL bRes = AcceptEx(garr_sockAll[0],garr_sockAll[gi_count],str,0,sizeof(struct sockaddr_in)+16,sizeof(struct sockaddr_in)+16,&dwRecvCount,garr_olpAll[0]);
	if (bRes == TRUE)
	{
		gi_count++;
		//这里执行3.1
		//执行成功,并连接成功
		
		return 0;
	}
	else
	{
		int acceptexerr = WSAGetLastError();
		if (acceptexerr == ERROR_IO_PENDING)
		{
			//延迟处理
			return 0;
		}
		else
		{
			//出错处理
		}
	}
}

3.1

接下来写上面自定义函数中3.1部分,这里acceptEx执行成功,并有客户端进行连接上来。
这里要用到WSARecv函数,该函数用来投递异步Recv消息

int WSAAPI WSARecv(
  SOCKET                             s,
  LPWSABUF                           lpBuffers,
  DWORD                              dwBufferCount,
  LPDWORD                            lpNumberOfBytesRecvd,
  LPDWORD                            lpFlags,
  LPWSAOVERLAPPED                    lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

参数1:客户端SOCKET句柄
参数2:接收客户端信息的BUFF,这里不能用自定义的字符串数组,要用:

typedef struct _WSABUF {
  ULONG len;
  CHAR  *buf;
} WSABUF, *LPWSABUF;

参数3:有几个参数2,这里一般是1个
参数4:接收成功,则这里可以设置接收到的信息长度,如果参数6设置的重叠结构体不为空,这里也可以设置为NULL,表示不获取接收到的信息长度
参数5:和recv函数中的参数4意思一样,用于设置WSARecv的标注,把前面的内容拷贝过来:
数据的读取方式。默认是0即可。正常情况下recv根据参数3读取数据缓冲区指定长度的数据后(指定长度大于数据长度则全部读取),数据缓冲区中被读取的数据会清除,把空间留出来给别的消息进来(不清理的话时间长了内存会溢出,数据缓冲区数据结构相当于队列)。
例如数据缓冲区中有如下数据:

abcdef

调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到a,b。则变成

cdef

这个时候再调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到c,d。
懂得正常逻辑后我们可以看下其他几种模式。

数值含义
0(默认值)从数据缓冲区读取数据后清空被读取的数据
MSG_PEEK(不建议使用,内存会爆)从数据缓冲区读取数据后不清空被读取的数据
MSG_OOB接收带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,因此也不推荐使用
MSG_WAITALL等待知道系统缓冲区字节数大于等于参数3所指定的字节数,才开始读取

如果使用MSG_PEEK模式,那么调用recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据得到a,b。由于不清空被读取的数据,缓冲区还是不变:

abcdef

如果再次执行recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据还是得到a,b。
看了一下MSDN,WSARecv比Recv函数要多了2个模式

数值含义
MSG_PUSH_IMMEDIATE该标识只能用在tream-oriented sockets,尽快处理请求,不做延迟,该标识在接收数据较小的时候比较有用,但是当数据较大进行分包传输后,使用该标识会造成后面传输的包会被丢弃,导致传输失败!
MSG_PARTIAL从数据缓冲区读取的数据是客户端发送的部分数据,程序员应该读取完整数据后再进行处理,该标识是由客户端设置的

参数6:重叠IO结构体指针
参数7:回调函数,先设置为NULL

返回值:
0:表示执行成功。这里要注意的是和AcceptEx不一样,AcceptEx返回的TRUE
SOCKET_ERROR :出错,用int wasrecverr = WSAGetLastError()获取错误码并处理
情况1:wasrecverr ==ERROR_IO_PENDING,表示执行成功,但处于异步等待状态,或者此时还没有客户端请求连接;
情况2:其他错误码要根据情况解决。

//投递RecvEx
//参数socketIndex是当前SOCKET数组下标
int PostRecv(int socketIndex)
{
	WSABUF wsabuff;	
	wsabuff.buf = gc_recvbuff;
	wsabuff.len= MAX_RECV_LENGTH;

	DWORD dwRecvedLength;
	DWORD dwRecvFlag=0;

	int iret = WSARecv(garr_sockAll[socketIndex],&wsabuff,1,&dwRecvedLength,&dwRecvFlag,&garr_olpAll[socketIndex],NULL);
	
	if (iret == 0)
	{
		//立即完成,执行成功
		//收取信息后返回
		printf("%s\n",wsabuf.buf);
		memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff

		//根据情况投递send
		
		//跳3.1.1继续投递Recv
		PostRecv(socketIndex);

		return 0;
	}
	else
	{
		int wasrecverr = WSAGetLastError();
		if (wasrecverr == ERROR_IO_PENDING)
		{
			//延迟处理
			return 0;
		}
		else
		{
			//出错处理
		}
	}
	return 0;
}

3.3循环等待/查询事件

DWORD WSAAPI WSAWaitForMultipleEvents(
  DWORD          cEvents,
  const WSAEVENT *lphEvents,
  BOOL           fWaitAll,
  DWORD          dwTimeout,
  BOOL           fAlertable
);

参数1:要等待/查询的事件数量
参数2:要等待/查询的事件句柄
参数3:如果查询一组/多个事件,当参数3为TRUE的时候,要等所有事件都有信号才返回,填FALSE则只要有一个事件有信号就返回
参数4:查询没有信号等待的时间,不等待就写0
参数5:设置线程是否进入alertable wait state,跟多线程有关,目前单线程且是事件通知先FALSE
返回值
整型,具体看
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsawaitformultipleevents

//accept成功就循环等待事件发生
	while(1)
	{
		for(int i = 0; i < gi_count; i++)
		{
			//笨方法,一次查询一个事件是否有信号
			int nRes=WSAWaitForMultipleEvents(1,&(garr_olpAll[i].hEvent), FALSE,0, FALSE); 
			if(nRes==WSA_WAIT_FAILED || nRes==WSA_WAIT_TIMEOUT)//查询失败或者超时
			{
				continue;
			}
		}
	}

3.3.2有信号

这时要获取socket上的具体信号
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsagetoverlappedresult

BOOL WSAAPI WSAGetOverlappedResult(
  SOCKET          s,
  LPWSAOVERLAPPED lpOverlapped,
  LPDWORD         lpcbTransfer,
  BOOL            fWait,
  LPDWORD         lpdwFlags
);

参数1:有信号的SOCKET句柄
参数2:有信号的SOCKET句柄对应的重叠结构的地址
参数3:发送或者接收到的实际字节数,如果得到0,表示客户端下线
参数4:当重叠操作选择了基于事件的完成通知时,设置为TRUE,这里就默认TRUE,设置false的解释如下:
If FALSE and the operation is still pending, the function returns FALSE and the WSAGetLastError function returns WSA_IO_INCOMPLETE.
参数5:不可为NULL,装WSARecv的参数5:lpflags,具体看上面的表
返回值:
TRUE:成功
FALSE:失败,具体错误码看
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsagetoverlappedresult
这里要注意,如果错误码是10054表示点×关闭窗口,要单独判断释放客户端SOCKET。

//经过查询有信号
			//3.3.2有信号
			DWORD dwTransfer;//接收或发送的数据长度
			DWORD dwFlagrecvpara5;//和recv中的参数5一致
			BOOL bret = WSAGetOverlappedResult(garr_sockAll[i],&garr_olpAll[i],&dwTransfer,TRUE,&dwFlagrecvpara5);
			
			if (bret == FALSE)//获取信号失败则跳过
			{
				continue;
			}

			//获取信号成功,则分类处理

			

			//0号位代表服务器SOCKET,说明接受连接完成
			if(i == 0)
			{
				
			}
			//长度为0表示客户端正常下线
			if(dwTransfer == 0)
			{

			}
			//发送或者接收数据成功
			if(dwTransfer != 0)
			{

			}

注意:WSAGetOverlappedResult没有自带重置信号的功能,要在执行它后面接下面代码重置信号:

WSAResetEvent(garr_olpAll[i].hEvent);

3.3.2.X

下面处理上面代码中if判断里面的内容,这里对应伪代码的3.3.2.X的内容。

3.3.2.1

如果信号是客户端连接,跳转3
实际上这里就是要先收数据,因为连接成功后必定要先收,然后再投递accept

	//0号位代表服务器SOCKET,说明接受连接完成,且刚连接上的客户端SOCKET在数组的第gi_count位上
			if(i == 0)
			{
				//执行成功,并连接成功
				//走流程3.1的两种情况
				//投递recv
				PostRecv(gi_count);

				gi_count++;//注意这里gi_count++的位置
				//再次投递AcceptEx
				PostAccept();
				continue;//一次处理一个响应,处理完跳出循环后面不用看了
			}

3.3.2.2

如果信号是客户端退出,则关闭客户端SOCKET,并从数组删除客户端的信息

//长度为0表示客户端下线
			if(dwTransfer == 0)
			{
				//关闭客户端SOCKET和事件句柄
				closesocket(garr_sockAll[i]);
				WSACloseEvent(garr_olpAll[i].hEvent);
				//从数组中删除客户端SOCKET和事件,这里思路用数组最后一位替换当前元素
				garr_sockAll[i] = garr_sockAll[gi_count-1];
				garr_olpAll[i] = garr_olpAll[gi_count-1];
				gi_count--;
				i--;//循环控制变量i回退一位,重新循环当前替换的新元素
			}

3.3.2.3、3.3.2.4

3.3.2.3如果信号是需要接收信息,跳转3.1.1
3.3.2.4 如果信号是需要发送信息,跳转3.1.2
这里用gc_recvbuff全局变量来判断是接受还是发送信息

//发送或者接收数据成功
			if(dwTransfer != 0)
			{
				//根据全局变量gc_recvbuff是否为空来判断是否发送数据
				if(gc_recvbuff[0] != 0)//不空说明收到数据,应该是recv
				{
					//立即完成,执行成功
					//收取信息后返回
					printf("%s\n",gc_recvbuff);
					memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff

					//根据情况投递send
					
					//跳3.1.1继续投递Recv
					PostRecv(i);
				}
				else//发送数据
				{
					//send
				}
			}

PostSend

上面的代码中还缺少对发送数据的操作进行处理,这里也将其写成一个函数的形式。

主要函数看这里:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasend

int WSAAPI WSASend(
  SOCKET                             s,
  LPWSABUF                           lpBuffers,
  DWORD                              dwBufferCount,
  LPDWORD                            lpNumberOfBytesSent,
  DWORD                              dwFlags,
  LPWSAOVERLAPPED                    lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

参数1:目标客户端SOCKET句柄
参数2:WSABUF结构体,参考WSARecv
参数3:WSABUF结构体的个数,一般为1
参数4:发送成功后,获取到已发送消息长度的字节数
参数5:同WSARecv参数5,不过不是地址,而是指示标识
参数6:重叠IO结构体地址
参数7:回调函数,先设置为NULL

整体代码

#include <WinSock2.h>
#include <mswsock.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "mswsock.lib")

#define MAX_COUNT 1024//这里上万个SOCKET应该没压力
#define MAX_RECV_LENGTH 1000//接收缓冲区一次最多接收字符串长度
#define MAX_SEND_LENGTH 1000//接收缓冲区一次最多发送字符串长度

SOCKET garr_sockAll[MAX_COUNT];//SOCKET数组
OVERLAPPED garr_olpAll[MAX_COUNT];//重叠结构体数组
int gi_count;//数组中SOCEKT的个数,全局变量默认值是0

char gc_recvbuff[MAX_RECV_LENGTH];//接收缓冲区全局变量,也可以放到main函数中定义,然后PostRecv传址调用

//函数声明
int PostAccept();
int PostRecv(int socketIndex);
int PostSend(int socketIndex);

//清理两个数组,逐个关闭SOCKET和事件
void ClearArr()
{
	for(int i = 0;i < gi_count;i++)
	{
		closesocket(garr_sockAll[i]);//关闭SOCKET
		WSACloseEvent(garr_olpAll[i].hEvent);//关闭事件
	}
}


BOOL WINAPI cls(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT :
		//释放数组中的事件和SOCKET句柄
		ClearArr();
		WSACleanup();

		break;

	}

	return TRUE;
}


int main(void)
{
	SetConsoleCtrlHandler(cls,TRUE);

	/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
	WORD wdVersion=MAKEWORD(2,2);

	WSADATA wdScokMsg;
	int nRes = WSAStartup(wdVersion,&wdScokMsg);


	if (0 != nRes)
	{
		switch(nRes)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启电脑。。。\n");
				break; 
			case WSAVERNOTSUPPORTED: 
				printf("请更新网络库\n");
				break; 
			case WSAEINPROGRESS: 
				printf("请重启程序\n");
				break; 
			case WSAEPROCLIM: 
				printf("请关闭空闲软件,释放资源来运行程序\n");
				break; 
			case WSAEFAULT:
				break;
		}
		return 0;
	
	}

	//校验版本	
	if (2!=HIBYTE(wdScokMsg.wVersion)|| 2!=LOBYTE(wdScokMsg.wVersion))
	{
			printf("版本有问题!\n");
			WSACleanup();
			return 0;
	}

	//这里和前面不一样,要把Socket初始化换成WSASocket
	SOCKET socketServer=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);


	if(INVALID_SOCKET == socketServer)
	{
		int err=WSAGetLastError();
		
		printf("WSASocket初始化失败,错误码是:%d\n",err);
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型

	si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
	
	if(SOCKET_ERROR==bind(socketServer,(const struct sockaddr *)&si,sizeof(si)))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	printf("服务器端bind成功!\n");

	if(SOCKET_ERROR==listen(socketServer,SOMAXCONN))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器监听失败错误码为:%d\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	
	printf("服务器端监听成功!\n");


	//6.投递AcceptEx
	garr_sockAll[gi_count] = socketServer;//初始化后的SOCKET放到SOCKET数组里面
	garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
	gi_count++;

	if (PostAccept() != 0)//出错
	{
		ClearArr();
		WSACleanup();

		return 0;
	}

	//accept成功就循环等待事件发生
	//3.3循环等待/查询事件
	while(1)
	{
		for(int i = 0; i < gi_count; i++)
		{
			//笨方法,一次查询一个事件是否有信号
			int nRes=WSAWaitForMultipleEvents(1,&(garr_olpAll[i].hEvent), FALSE,0, FALSE); 
			if(nRes==WSA_WAIT_FAILED || nRes==WSA_WAIT_TIMEOUT)//查询失败或者超时
			{
				continue;
			}

			//经过查询有信号
			//3.3.2有信号
			DWORD dwTransfer;//接收或发送的数据长度
			DWORD dwFlagrecvpara5;//和recv中的参数5一致
			//获取socket上的具体信号
			BOOL bret = WSAGetOverlappedResult(garr_sockAll[i],&garr_olpAll[i],&dwTransfer,TRUE,&dwFlagrecvpara5);
			
			//获取到信号后要重置信号
			WSAResetEvent(garr_olpAll[i].hEvent);

			if (bret == FALSE)//获取信号失败则跳过
			{
				int a = WSAGetLastError();
				if (a ==10054)//代表直接点×关闭窗口
				{
					printf("客户端点×关闭下线,数组共有元素");
					//关闭客户端SOCKET和事件句柄
					closesocket(garr_sockAll[i]);
					WSACloseEvent(garr_olpAll[i].hEvent);
					//从数组中删除客户端SOCKET和事件,这里思路用数组最后一位替换当前元素
					garr_sockAll[i] = garr_sockAll[gi_count-1];
					garr_olpAll[i] = garr_olpAll[gi_count-1];
					gi_count--;//数组元素个数减一
					i--;//循环控制变量i回退一位,重新循环当前替换的新元素
					printf("数组共有元素:%d\n",gi_count);
				}
				continue;
			}

			//获取信号成功,则按情况分类处理

			//情况1:
			//0号位代表服务器SOCKET,说明接受连接完成
			//刚连接上的客户端SOCKET在数组的第gi_count位上
			if(i == 0)
			{
				printf("情况1:接受连接完成\n");
				//执行成功,并连接成功
				//走流程3.1的两种情况
				//对连接上的客户端send消息
				PostSend(gi_count);
				//投递recv
				PostRecv(gi_count);

				gi_count++;//注意这里gi_count++的位置
				//再次投递AcceptEx
				PostAccept();
				continue;//一次处理一个响应,处理完跳出循环后面不用看了
			}

			//情况2:长度为0表示客户端下线
			if(dwTransfer == 0)
			{
				printf("情况2:客户端下线\n");
				//关闭客户端SOCKET和事件句柄
				closesocket(garr_sockAll[i]);
				WSACloseEvent(garr_olpAll[i].hEvent);
				//从数组中删除客户端SOCKET和事件,这里思路用数组最后一位替换当前元素
				garr_sockAll[i] = garr_sockAll[gi_count-1];
				garr_olpAll[i] = garr_olpAll[gi_count-1];
				gi_count--;//数组元素个数减一
				i--;//循环控制变量i回退一位,重新循环当前替换的新元素
				continue;//一次处理一个响应,处理完跳出循环后面不用看了
			}

			

			//情况3:有发送或者接收数据信号
			if(dwTransfer != 0)
			{
				//根据全局变量gc_recvbuff是否为空来判断是否发送数据
				if(gc_recvbuff[0] != 0)//不空说明收到数据,应该是recv
				{
					printf("情况3:捕获接收信号from:%d\n",i);
					//立即完成,执行成功
					//收取信息后返回
					printf("%s\n",gc_recvbuff);
					memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff

					//根据情况投递send
					
					//跳3.1.1继续投递Recv
					PostRecv(i);
				}
				else//发送数据
				{
					printf("捕获发送信号\n");
					//send
					PostSend(i);
					
				}
			}
		}
	}
	
	

	ClearArr();
	WSACleanup();


	system("pause");
	return 0;
}

//投递AcceptEx
int PostAccept()
{
	//客户端句柄加到数组里面,注意gi_count++的位置
	garr_sockAll[gi_count]=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
	garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化

	char str[1024] = {0};

	DWORD dwRecvCount = 0;

	//AcceptEx涉及的SOCKET句柄和重叠事件结构体都是针对服务器的
	BOOL bRes = AcceptEx(garr_sockAll[0],garr_sockAll[gi_count],str,0,sizeof(struct sockaddr_in)+16,
		sizeof(struct sockaddr_in)+16,&dwRecvCount,&garr_olpAll[0]);
	printf("PostAccept\n");
	if (bRes == TRUE)
	{
		printf("PostAccept Success\n");
		//PostSend(gi_count);
		//执行成功,并连接成功
		//走流程3.1的两种情况		
		//投递recv
		PostRecv(gi_count);
		gi_count++;//注意这里gi_count++的位置
		//再次投递AcceptEx
		PostAccept();
		return 0;
	}
	else
	{
		int acceptexerr = WSAGetLastError();
		if (acceptexerr == ERROR_IO_PENDING)
		{
			//延迟处理
			return 0;
		}
		else
		{
			//出错处理
			printf("PostAccept出错,错误码是:%d\n",acceptexerr);
			return acceptexerr;
		}
	}

}


//投递WSASend
//参数socketIndex是当前SOCKET数组下标
int PostSend(int socketIndex)
{
	WSABUF wsabuff;	//接收数据专用
	wsabuff.buf = "这是重叠IO模型服务器消息~!";
	wsabuff.len= MAX_SEND_LENGTH;

	DWORD dwSendedLength;
	DWORD dwSendFlag=0;//这里要初始化,否则有错
	
	int iret = WSASend(garr_sockAll[socketIndex],&wsabuff,1,&dwSendedLength,dwSendFlag,&garr_olpAll[socketIndex],NULL);
	printf("PostSendto:%d\n",socketIndex);
	if (iret == 0)
	{
		//立即完成,执行成功
		
		printf("WSASend发送给:%d成功\n",socketIndex);
		//memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff

		//根据情况投递send,不需要循环调用	

		return 0;
	}
	else
	{
		int wassenderr = WSAGetLastError();
		if (wassenderr == ERROR_IO_PENDING)
		{
			//延迟处理
			return 0;
		}
		else
		{
			printf("WSASend发送失败,错误码是:%d\n",wassenderr);
			//出错处理
			return wassenderr;
		}
	}

}

//投递WSARecv
//参数socketIndex是当前SOCKET数组下标
int PostRecv(int socketIndex)
{
	WSABUF wsabuff;	//接收数据专用
	wsabuff.buf = gc_recvbuff;
	wsabuff.len= MAX_RECV_LENGTH;

	DWORD dwRecvedLength;
	DWORD dwRecvFlag=0;//这里要初始化,否则有错

	int iret = WSARecv(garr_sockAll[socketIndex],&wsabuff,1,&dwRecvedLength,&dwRecvFlag,&garr_olpAll[socketIndex],NULL);
	printf("PostRecv from id:%d\n",socketIndex);
	if (iret == 0)
	{
		//立即完成,执行成功
		//收取信息后返回
		printf("%s\n",wsabuff.buf);
		memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff

		//根据情况投递send
		
		//跳3.1.1继续投递Recv
		PostRecv(socketIndex);

		return 0;
	}
	else
	{
		int wasrecverr = WSAGetLastError();
		if (wasrecverr == ERROR_IO_PENDING)
		{
			//延迟处理
			return 0;
		}
		else
		{
			printf("WSARecv接收失败,错误码是:%d\n",wasrecverr);
			//出错处理
			return wasrecverr;
		}
	}
}

未完成工作及问题

在上面的代码中,循环对事件数组进行轮询时(就是while下面的那个for循环),是一个个对事件数组中的元素进行遍历判断是否有信号,如果数组元素较多,例如:10000个,那么会轮询会造成较大的延迟,这个时候,可以考虑开多线程来进行优化,将10000个元素分十组,每组由一个线程来进行处理。
无论是异步选择模型还是重叠IO模型,都存在一个相同的问题,就是当客户端多次向服务器端发送数据(调用多次send),服务器会产生多个recv信号,但是在第一次接收消息的时候就会收完所有数据。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值