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 dwError | WSAGetOverlappedResult 的错误码就是回调函数产生的错误码 |
DWORD cbTransferred | LPDWORD lpcbTransfer |
DWORD dwFlags | LPDWORD lpdwFlags |
LPWSAOVERLAPPED lpOverlapped | LPWSAOVERLAPPED 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个 |
---|---|---|---|---|---|---|---|---|---|---|
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
如果当前元素的第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模型:完成例程的主要内容,如果未能解决你的问题,请参考以下文章