TCP.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操作后,会系统会自动调用绑定的回调函数,AcceptEx还是和事件通知的一样,这个函数是没有绑定回调函数的功能的(名字看上去就和其他两个不一样,其实是因为在Accept操作完成后没有什么必要的后续操作,因此也就没有绑定回调函数):
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:错误码,TCP要额外判断10054的错误码,表示客户端点×退出
参数2:发送或者接收到的字节数,如果该值为0表示客户端正常退出
参数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;
		}
	}
	
	//无论是非正常dwError == 10054或者正常退出cbTransferred == 0都需要做相同操作
	//需要删除客户端和对应重叠IO结构体,这一块的代码和事件通知是一样的
	if (dwError == 10054 || 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句柄对应的garr_olpAll[0]进行判断,不需要循环garr_olpAll数组所有元素,而且这里只会发生客户端请求连接信号,上节中判断客户端退出的功能已在recv的回调函数中处理,这里不需要再处理,将上节代码删除部分,变成:

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_INFINITE(无限等待,直到有信号为止),原来的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,因此每次执行成功(bRes == TRUE)就continue重复循环,如果是延迟完成或出错先退出循环。

//投递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;
}

错误处理

错误 C2440 “=”: 无法从“const char [XXX]”转换为“char *”
原因是新版编译器中,不能直接把 const char* 赋值给 char*
简单粗暴解决:
找到项目属性,将下面选项设置为否即可。
在这里插入图片描述

完整代码(有bug)

Bug描述:开多个客户端,然后每个客户端发送多个消息服务器无回复


#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

#include <Winsock2.h>
#include <mswsock.h>

#include <string.h>
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Mswsock.lib")

#define MAX_COUNT  1024
#define MAX_RECV_COUNT  1024

SOCKET g_allSock[MAX_COUNT];
OVERLAPPED g_allOlp[MAX_COUNT];
int g_count;
WSABUF wsabuf;

//接收缓冲区
//char g_strRecv[MAX_RECV_COUNT];

int PostAccept();
int PostRecv(int index);
int PostSend(int index);

void Clear()
{
	for (int i = 0; i < g_count; i++)
	{
		closesocket(g_allSock[i]);
		WSACloseEvent(g_allOlp[i].hEvent);
	}
}


BOOL WINAPI fun(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT:
		//释放所有socket
		Clear();

		break;
	}

	return TRUE;
}


int main(void)
{
	wsabuf.len = MAX_RECV_COUNT;
	wsabuf.buf = (char*)malloc(MAX_RECV_COUNT * sizeof(char));
	memset(wsabuf.buf, 0, MAX_RECV_COUNT);
	SYSTEM_INFO systemProcessorsCount;
	GetSystemInfo(&systemProcessorsCount);
	int nProcessorsCount = systemProcessorsCount.dwNumberOfProcessors;

	SetConsoleCtrlHandler(fun, TRUE);
	WORD wdVersion = MAKEWORD(2, 2);
	WSADATA wdScokMsg;
	int nRes = WSAStartup(wdVersion, &wdScokMsg);

	if (0 != nRes)
	{
		switch (nRes)
		{
		case WSASYSNOTREADY:
			printf("重启下电脑试试,或者检查网络库");
			break;
		case WSAVERNOTSUPPORTED:
			printf("请更新网络库");
			break;
		case WSAEINPROGRESS:
			printf("请重新启动");
			break;
		case WSAEPROCLIM:
			printf("请尝试关掉不必要的软件,以为当前网络运行提供充足资源");
			break;
		}
		return  0;
	}

	//校验版本
	if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))
	{
		//说明版本不对
		//清理网络库
		WSACleanup();
		return 0;
	}

	SOCKET socketServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
	//int a = WSAGetLastError();
	if (INVALID_SOCKET == socketServer)
	{
		int a = WSAGetLastError();
		//清理网络库
		WSACleanup();
		return 0;
	}

	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_port = htons(9527);
	si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//int a = ~0;
	if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr *)&si, sizeof(si)))
	{
		//出错了
		int a = WSAGetLastError();
		//释放
		closesocket(socketServer);
		//清理网络库
		WSACleanup();
		return 0;
	}

	if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
	{
		//出错了
		int a = WSAGetLastError();
		//释放
		closesocket(socketServer);
		//清理网络库
		WSACleanup();
		return 0;
	}

	g_allSock[g_count] = socketServer;
	g_allOlp[g_count].hEvent = WSACreateEvent();
	g_count++;

	if (0 != PostAccept())
	{
		Clear();
		//清理网络库
		WSACleanup();
		return 0;
	}

	while (1)
	{
		int nRes = WSAWaitForMultipleEvents(1, &(g_allOlp[0].hEvent), FALSE, WSA_INFINITE, TRUE);
		if (WSA_WAIT_FAILED == nRes || WSA_WAIT_IO_COMPLETION == nRes)
		{
			continue;
		}

		//信号置空
		WSAResetEvent(g_allOlp[0].hEvent);
		//PostSend(g_count);
		printf("accept\n");
		//接收链接完成了
		//投递recv
		PostRecv(g_count);
		//根据情况投递send
		//PostSend(g_count);
		//客户端适量++
		g_count++;
		//投递accept
		PostAccept();
	}

	Clear();
	//清理网络库
	WSACleanup();

	system("pause");
	return 0;
}

int PostAccept()
{
	while (1)
	{
		g_allSock[g_count] = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
		g_allOlp[g_count].hEvent = WSACreateEvent();

		char str[1024] = { 0 };
		DWORD dwRecvcount;

		BOOL bRes = AcceptEx(g_allSock[0], g_allSock[g_count], str, 0, sizeof(struct sockaddr_in) + 16,
			sizeof(struct sockaddr_in) + 16, &dwRecvcount, &g_allOlp[0]);

		if (TRUE == bRes)
		{
			//立即完成了
			//投递recv
			PostRecv(g_count);
			//根据情况投递send
			//客户端适量++
			g_count++;
			//投递accept
			//PostAccept();
			continue;
		}
		else
		{
			int a = WSAGetLastError();
			if (ERROR_IO_PENDING == a)
			{
				//延迟处理
				break;
			}
			else
			{
				break;
			}
		}
	}
	return 0;
}

void CALLBACK RecvCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
	//int i = 0;
	//for (i; i < g_count; i++)
	//{
	//	if (lpOverlapped->hEvent == g_allOlp[i].hEvent)
	//	{
	//		break;
	//	}
	//}

	int i = lpOverlapped - &g_allOlp[0];

	if (10054 == dwError || 0 == cbTransferred)
	{
		//删除客户端
		printf("close\n");
		//客户端下线
		//关闭
		closesocket(g_allSock[i]);
		WSACloseEvent(g_allOlp[i].hEvent);
		//从数组中删掉
		g_allSock[i] = g_allSock[g_count - 1];
		g_allOlp[i] = g_allOlp[g_count - 1];
		//个数减-1
		g_count--;
	}
	else
	{
		printf("RecvCall%s\n", wsabuf.buf);
		memset(wsabuf.buf, 0, MAX_RECV_COUNT);
		//根据情况投递send
		
		//对自己投递接收
		PostRecv(i);
	}
}

int PostRecv(int index)
{

	DWORD dwRecvCount;
	DWORD dwFlag = 0;
	int nRes = WSARecv(g_allSock[index], &wsabuf, 1, &dwRecvCount, &dwFlag, &g_allOlp[index], RecvCall);
	if (0 == nRes)
	{
		//立即完成的
		//打印信息
		printf("立即完成的%s\n", wsabuf.buf);
		memset(wsabuf.buf, 0, MAX_RECV_COUNT);
		PostSend(index);
		//根据情况投递send
		//对自己投递接收
		PostRecv(index);
		return 0;
	}
	else
	{
		int a = WSAGetLastError();
		if (ERROR_IO_PENDING == a)
		{
			//延迟处理
			return 0;
		}
		else
		{
			return a;
		}
	}
}

void CALLBACK SendCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
	printf("send over\n");
	memset(wsabuf.buf, 0, MAX_RECV_COUNT);
}

int PostSend(int index)
{
	WSABUF wsasendbuf;
	wsasendbuf.buf = "你好";
	wsasendbuf.len = MAX_RECV_COUNT;

	DWORD dwSendCount;
	DWORD dwFlag = 0;
	int nRes = WSASend(g_allSock[index], &wsasendbuf, 1, &dwSendCount, dwFlag, &g_allOlp[index], SendCall);

	if (0 == nRes)
	{
		//立即完成的
		//打印信息
		printf("send成功\n");
		return 0;
	}
	else
	{
		int a = WSAGetLastError();
		if (ERROR_IO_PENDING == a)
		{
			//延迟处理
			return 0;
		}
		else
		{
			return a;
		}
	}
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

oldmao_2000

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

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

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

打赏作者

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

抵扣说明:

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

余额充值