WINSOCK.06.重叠IO模型:完成例程

Posted oldmao_2001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WINSOCK.06.重叠IO模型:完成例程相关的知识,希望对你有一定的参考价值。


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

这次先讲第四种。
还是重叠IO模型,但是是基于完成例程的,例程可以理解为回调函数。
我们先把完成例程和事件通知两种重叠IO模型的思想厘清。

完成例程事件通知
相同1异步完成后调用;AcceptEx函数的使用异步完成后调用;AcceptEx函数的使用
相同2当客户端多次向服务器端发送数据(调用多次send),服务器会产生多个recv信号,但是在第一次接收消息的时候就会收完所有数据当客户端多次向服务器端发送数据(调用多次send),服务器会产生多个recv信号,但是在第一次接收消息的时候就会收完所有数据
不同1系统自动根据不同操作(WSASend、WSARecv等)完成后,自动调用对应的函数,WSASend、WSARecv有绑定回调函数根据事件类型有不同的信号,然后编写不同的代码
不同2系统根据具体事件自动调用回调函数,自动分类,性能好在WSAWaitForMultipleEvents自己判断信号,执行顺序无法保证,循环次数和客户端数量正比,下标越大的客户端延迟越大

貌似完成例程由系统直接根据操作来调用相应函数,不像事件通知还要手工判断信号然后进行相应处理,完成例程少了判断信号的步骤。
简而言之就是完成例程代码逻辑上更加简单,性能也更好(系统帮你干活效率高)。

重叠IO模型:完成例程代码逻辑

代码逻辑其实和事件通知一样的,不同的是在完成例程中的WSASend、WSARecv函数要额外绑定回调函数,在执行完WSASend、WSARecv操作后,会系统会自动调用绑定的回调函数:
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

重叠IO模型:完成例程代码实现

回调函数介绍

既然伪代码差不多,因此具体代码实现和上一节差不多,这里只看改什么东西。
下看WSARecv

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

最后一个参数,在事件通知中设置的是NULL,这里完成例程中要设置为要绑定的回调函数。
回调函数的定义为:

typedef
void
(CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
    DWORD dwError,
    DWORD cbTransferred,
    LPWSAOVERLAPPED lpOverlapped,
    DWORD dwFlags
    );

从名字上可以推断这是一个回调函数指针。具体可以看这里:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nc-winsock2-lpwsaoverlapped_completion_routine
稍微解释一下:
void:代表没有返回值
CALLBACK:代表这个函数是一个回调函数(具体的调用约定可以转到定义自己看),后面接一个自己起的函数名字。
参数1:错误码
参数2:发送或者接收到的字节数
参数3:重叠结构
参数4:函数执行方式,这个和WSARecv、WSASend的参数5意思是一样的。

需要说一下,这个回调函数是由系统自动调用的,是执行完下面这个函数自动调用:

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

因此二者有很多联系,注意看下面代码中参数的对应关系。

回调函数WSAGetOverlappedResult
DWORD dwErrorWSAGetOverlappedResult 的错误码就是回调函数产生的错误码
DWORD cbTransferredLPDWORD lpcbTransfer
DWORD dwFlagsLPDWORD lpdwFlags
LPWSAOVERLAPPED lpOverlappedLPWSAOVERLAPPED lpOverlapped

在回调函数中的处理流程和之前的接收到信号后的流程差不多:
1.dwError ==10054,表示客户端点击×关闭窗口,需要删除客户端和对应重叠IO结构体;
2.cbTransferred == 0,表示客户端正常退出, 需要删除客户端和对应重叠IO结构体;
cbTransferred != 0,接收数据成功,处理接收到的数据
3.其他情况,发送数据成功?

回调函数的代码实现

这里只写接收数据的回调函数:RecvCallBack,当然要记得把这个名字放到WSARecv的最后一个参数那里,否则系统就不知道自动调用哪个回调函数。

void CALLBACK RecvCallBack(DWORD dwError,DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped,DWORD dwFlags)
{
	int i = 0;//位置

	//循环遍历重叠IO结构体数组,找到当前重叠IO结构体在数组中的位置
	for(i; i < gi_count; i++)
	{
		//重叠IO结构体的事件句柄一样代表找到了
		if(garr_olpAll[i].hEvent=lpOverlapped->hEvent)
		{
			break;
		}
	}
	
	//无论是非正常或者正常退出都需要做相同操作
	//需要删除客户端和对应重叠IO结构体,这一块的代码和事件通知是一样的
	if (dwError == 15504 || cbTransferred == 0)
	{
		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--;//数组元素个数减一
		
		printf("数组共有元素:%d\\n",gi_count);
	}
	else//接收数据
	{
		printf("%s\\n",wsabuff.buf);
		memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff

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

发送数据的调用是按照需求来的,因此其对应的回调函数里面暂时没有写代码

等待循环的代码实现

这里不需要再对接收和发送进行判断(都放到回调函数中处理了),这里只需要对服务器SOCKET句柄进行判断,而且这里只会发生客户端请求连接信号,将上节代码删除部分,变成:

while(1)
	{
		//这里只用查询服务器SOCKET是否有事件,如果有信号,必定是请求连接事件
		//因此garr_olpAll的位置设置为0
		int nRes=WSAWaitForMultipleEvents(1,&(garr_olpAll[0].hEvent), FALSE,WSA_INFINITE, TRUE); 
		if(nRes==WSA_WAIT_FAILED || nRes==WSA_WAIT_IO_COMPLETION)//查询失败或者超时
		{
			continue;
		}

		//信号置空
		WSAResetEvent(garr_olpAll[0].hEvent);

		printf("情况1:接受连接完成\\n");
		//执行成功,并连接成功
		//走流程3.1的两种情况
		//对连接上的客户端send消息
		PostSend(gi_count);
		//投递recv
		PostRecv(gi_count);

		gi_count++;//注意这里gi_count++的位置
		//再次投递AcceptEx
		PostAccept();	
		
	}

这里需要注意的是,由于只需要等待服务器事件,因此WSAWaitForMultipleEvents的第四个参数可设置为:WSA_INFINITE,反正没有别的SOCKET事件需要处理,就一直等到服务器有事件信号;最后一个参数要设置为TRUE,因为完成例程必须要设置TRUE才能生效。这个函数设置为TRUE后,WSAWaitForMultipleEvents这个函数和完成例程就进入异步执行模式,完成例程的回调函数执行完成就会返回WSA_WAIT_IO_COMPLETION。
小结:WSAWaitForMultipleEvents最后一个参数设置为TRUE后,不但能够获得事件的信号通知,还能得到完成例程执行完毕的通知。原来的WSA_WAIT_TIMEOUT就不需要了。

优化

回调函数优化

这里主要针对寻找重叠IO结构体位置的代码进行优化:

	int i = 0;//位置
//循环遍历重叠IO结构体数组,找到当前重叠IO结构体在数组中的位置
	for(i; i < gi_count; i++)
	{
		//重叠IO结构体的事件句柄一样代表找到了
		if(garr_olpAll[i].hEvent==lpOverlapped->hEvent)
		{
			printf("RecvCallBack回调找到重叠IO结构体位置是:%d!\\n",i);
			break;
		}
	}

执行完上面的代码后,i就是当前重叠IO结构体在数组中的位置,但是如果数组里面的元素非常多,每次做这个循环效率就很低。可以看到lpOverlapped实际上是当前重叠IO结构体的地址,整个数组的地址我们也知道,那么我们可以用减法直接算出当前重叠IO结构体的位置。
小例子:
假如一个数组有10个元素,每个元素是1个字节大小,第一个元素的地址是0,那么:

数组元素第1个第2个第3个第4个第5个第6个第7个第8个第9个第10个
地址0123456789

如果当前元素的第7个,那么可以用第7个元素的地址减去第一个元素的地址:7-0=7得到当前是第几个元素。
如果元素大小即使不是1,上面的方法仍然适用,因此,上面的代码可以换成:

int i = lpOverlapped - &garr_olpAll[0];

时间复杂度从 O ( n ) O(n) O(n)变成 O ( 1 ) O(1) O(1)

投递函数递归转循环

递归调用虽然比较好理解,但是内存特别容易炸,玩过数据结构的塔罗牌的就知道。

//投递AcceptEx
int PostAccept()
{
	while(1)
	{
		//客户端句柄加到数组里面,注意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;
			continue;
		}
		else
		{
			int acceptexerr = WSAGetLastError();
			if (acceptexerr == ERROR_IO_PENDING)
			{
				//延迟处理
				//return 0;
				break;
			}
			else
			{
				//出错处理
				printf("PostAccept出错,错误码是:%d\\n",acceptexerr);
				//return acceptexerr;
				break;
			}
		}
	}
	
	return 0;
}

以上是关于WINSOCK.06.重叠IO模型:完成例程的主要内容,如果未能解决你的问题,请参考以下文章

关于重叠IO(overlapped)模型中完成例程使用的两点疑问

网络模型之重叠IO

Socket编程模型之完成端口模型

WINSOCK.053.重叠IO模型

windows的重叠IO模型

UDP.5.重叠IO模型:事件通知