UDP.5.重叠IO模型:事件通知
Posted oldmao_2001
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UDP.5.重叠IO模型:事件通知相关的知识,希望对你有一定的参考价值。
文章目录
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于UDP的网络编程还有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型
这节讲基于UDP的重叠IO模型。
重叠IO模型介绍
重叠IO是Windows提供的一种异步读写文件的机制。(前面讲的是事件机制和消息机制,重叠IO也是一种机制)
如果我们把网络发送消息,读取消息中的消息看成文件,那么SOCKET的本质就是文件操作。正常的读文件,例如recv,是阻塞的,等待协议缓冲区中的全部复制到我们自己定义的buff中,函数才能结束并返回复制的内容,如果多次调用recv函数,那么这些recv函数是依次一个个执行的。写(send)的过程也一样,同一时间只能执行一个send函数,其他的操作只能等。
重叠IO机制则是把上面描述的过程做成非阻塞操作,将的指令以及我们自定义的buff投递给操作系统,然后函数直接返回,由操作系统独立打开一个线程,将数据复制到buff,这个过程,我们的应用可以做别的事情,也就意味我们可以同时投递多个读或写指令,同时进行多个读写操作。
从代码上看,就是将原来的recvfrom、sendto函数,转化为可非阻塞执行的WSARecvFrom、WSASendTo函数。
重叠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.调用WSARecvFrom WSASendTo投递
2.操作系统将被完成的操作,事件信号置成有信号
3.调用WSAGetOverlappedResult获取事件信号
对于完成例程而言:
1.调用WSARecvFrom WSASendTo投递
2.完成后自动调用回调函数
这节先讲事件通知。
由于UDP不用ACCEPT,因此它的逻辑比TCP要简单很多:
重叠IO模型:事件通知代码逻辑
io1.创建事件(optional)、SOCKET(1个),重叠结构体(1个),UDP里面不用数组,只有服务器句柄
io2.创建重叠IO模型使用的SOCKET:WSASocket
3.投递AcceptEx
3.1立即完成,此时有客户端连接
io3.对客户端套接字投递WSARecvFrom
io3.1.1有客户端消息,系统空闲,立即完成,跳io3
io3.1.2无客户端消息,跳io3.3
3.1.3如果需要连接客户端跳3
io3.2延迟完成,跳io3.3
io3.3循环等待信号(WSAWaitForMultipleEvents:当前线程挂起,不占用CPU时间片)
io3.3.1 没信号,继续等
io3.3.2 有信号,先获取重叠结构上的信息(WSAGetOverlappedResult)
io3.3.3 信号置空
io3.3.4 分类处理
3.3.2.1 如果信号是服务器端上检测到有客户端连接,跳转3(这个步骤和3.3.2.2不可以调换顺序)
3.3.2.2 如果信号是客户端退出,则关闭客户端SOCKET,并从数组删除客户端的信息
io3.3.2.1 如果信号是需要接收信息,跳转io3
io3.3.2.2 延迟完成,跳转io4
io4根据需求对客户端套接字投递WSASend,可以在收到信息后调用
io4.1有消息要发送,系统空闲,立即完成,跳io4
io4.2无消息要发送,延迟完成跳3.3
说明:由于WSARecv WSASend每调用一次,只会处理一次,要处理多次就要多次调用,因此上面有跳转。
重叠IO模型:事件通知代码实现
1.-5.
和之前一样,重叠IO模型服务器端的SOCKET代码套路的前面部分是一样的:
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET 这里要用WSASocket来创建(io2)
再次强调:WSA是windows socket async的缩写
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa
SOCKET WSAAPI WSASocketA(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
具体代码:
socketServer = WSASocket(AF_INET,SOCK_DGRAM,IPPROTO_UDP,NULL,0,WSA_FLAG_OVERLAPPED);
5、绑定地址与端口
io3.对客户端套接字投递WSARecvFrom
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom
int WSAAPI WSARecvFrom(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
sockaddr *lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
参数1:服务器端SOCKET句柄
参数2:接收客户端信息的BUFF,这里不能用自定义的字符串数组,要用:
该结构体有两个成员,一个是长度,一个char型指针
typedef struct _WSABUF {
ULONG len;
CHAR *buf;
} WSABUF, *LPWSABUF;
参数3:有几个参数2,这里一般是1个,尤其是UDP,一次只接一个数据报,直接填1
参数4:接收成功,则这里可以设置接收到的信息长度,如果参数6设置的重叠结构体不为空,这里也可以设置为NULL,表示不获取接收到的信息长度
参数5:和recv函数中的参数4意思一样,用于设置WSARecvFrom的标注,把前面的内容拷贝过来:
数据的读取方式。默认是0即可。正常情况下recv根据参数3读取数据缓冲区指定长度的数据后(指定长度大于数据长度则全部读取),数据缓冲区中被读取的数据会清除,把空间留出来给别的消息进来(不清理的话时间长了内存会溢出,数据缓冲区数据结构相当于队列)。
例如数据缓冲区中有如下数据:
a | b | c | d | e | f |
---|
调用recvfrom(socket,buff,2,0);从数据缓冲区读取两个字节的数据得到a,b。则变成
c | d | e | f |
---|
这个时候再调用recv(socket,buff,2,0);从数据缓冲区读取两个字节的数据得到c,d。
懂得正常逻辑后我们可以看下其他几种模式。
数值 | 含义 |
---|---|
0(默认值) | 从数据缓冲区读取数据后清空被读取的数据 |
MSG_PEEK(不建议使用,内存会爆) | 从数据缓冲区读取数据后不清空被读取的数据 |
MSG_OOB | 接收带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,因此也不推荐使用 |
MSG_PARTIAL | 从数据缓冲区读取的数据是客户端发送的部分数据,程序员应该读取完整数据后再进行处理,该标识是由客户端设置的 |
如果使用MSG_PEEK模式,那么调用recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据得到a,b。由于不清空被读取的数据,缓冲区还是不变:
a | b | c | d | e | f |
---|
如果再次执行recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据还是得到a,b。
WSARecvFrom比WSARecv函数要少2个模式
参数6:UDP才有TCP没有,从哪里接收的数据,包括IP和端口
参数7:UDP才有TCP没有,参数6的长度
参数8:重叠IO结构体指针
参数9:回调函数,重叠IO模型:完成例程里面才能用,这里设置为NULL
返回值:
0:表示执行成功(立即完成)。
SOCKET_ERROR :出错,用int wasrecverr = WSAGetLastError()获取错误码并处理
情况1:wasrecverr ==ERROR_IO_PENDING,表示执行成功,但处于异步等待状态,或者此时还没有客户端请求连接;
情况2:其他错误码要根据情况解决。
//io3.投递WSARecvFrom
int PostRecvFrom()
{
WSABUF wbsBuff;
wbsBuff.buf = recvBuff;
wbsBuff.len = 547;//留出/0的位置
DWORD dwRecvCont = 0;//成功接收到的字节数
DWORD flag = 0;
struct sockaddr_in si;
int nlen = sizeof(si);
int nRet = WSARecvFrom(socketServer,&wbsBuff,1,&dwRecvCont,&flag,(struct sockaddr*)&si,&nlen,&wolSever,NULL);
if(0 == nRet)
{
printf("%s\\n",recvBuff);
PostRecvFrom();//io3.1.1有客户端消息,系统空闲,立即完成,跳io3
}
else
{
int wasrecverr = WSAGetLastError();
if(ERROR_IO_PENDING == wasrecverr)//io3.2延迟完成,跳io3.3
{
return 1;
}
}
return 0;
}
io4.根据需求对客户端套接字投递WSASend
主要函数看这里:https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasendto
int WSAAPI WSASendTo(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const sockaddr *lpTo,
int iTolen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
参数1:目标客户端SOCKET句柄
参数2:WSABUF结构体,参考WSARecv
参数3:WSABUF结构体的个数,一般为1
参数4:发送成功后,获取到已发送消息长度的字节数
参数5:同WSARecvFrom参数5,不过不是地址,而是指示标识
参数6:UDP才有TCP没有,从哪里接收的数据,包括IP和端口
参数7:UDP才有TCP没有,参数6的长度
参数8:重叠IO结构体地址
参数9:回调函数,重叠IO模型:完成例程才用到,先设置为NULL
//io4根据需求对客户端套接字投递WSASend
int PostSentTo(struct sockaddr_in* si)
{
WSABUF wbsBuff;
wbsBuff.buf = "调用PostSentTo发送给客户端\\n";
wbsBuff.len = 547;//留出/0的位置
DWORD dwRecvCont = 0;//成功接发送的字节数
int nlen = sizeof(struct sockaddr);
int nRet = WSASendTo(socketServer,&wbsBuff,1,&dwRecvCont,0,(struct sockaddr*)&si,nlen,&wolSever,NULL);
if(0 == nRet)
{
printf("WSASendTo执行完毕\\n");//io4.1有消息要发送,系统空闲,立即完成,跳io4
}
else
{
int wassendtoerr = WSAGetLastError();
if(ERROR_IO_PENDING == wassendtoerr)//io4.2延迟完成跳3.3
{
return 1;
}
}
return 0;
}
io3.3循环等待信号
整体逻辑:
io3.3循环等待信号
WSAWaitForMultipleEvents:当前线程挂起,不占用CPU时间片。
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsawaitformultipleevents
DWORD WSAAPI WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT *lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
参数1:要等待/查询的事件数量
参数2:要等待/查询的事件句柄
参数3:如果查询一组/多个事件,当参数3为TRUE的时候,要等所有事件都有信号才返回,填FALSE则只要有一个事件有信号就返回,由于UDP只有一个事件,所以填啥都一样,这里用FALSE
参数4:查询没有信号等待的时间,不等待就写0
参数5:设置线程是否进入alertable wait state,跟多线程有关,主要用于重叠IO模型:完成例程,目前单线程且是事件通知先FALSE
返回值
整型,有好几种情况,具体看
宏 | 含义 |
---|---|
WSA_WAIT_EVENT_0 | 如果有多组事件,且参数3是等待所有事件(TRUE),这里返回表示所有事件都有信号; 如果有多组事件,且参数3是等待某个事件有信号(FALSE),此时只有一个事件有信号,这里返回值减去WSA_WAIT_EVENT_0就可得到有信号的事件的数组下标; 如果有多组事件,且参数3是等待某个事件有信号(FALSE),此时有多个事件有信号,这里返回值减去WSA_WAIT_EVENT_0就可得到所有有信号的事件的数组下标最小的那个 |
WSA_WAIT_IO_COMPLETION | 如果参数5设置为TRUE,且当前事件没有信号产生,只能用在完成例程 |
WSA_WAIT_TIMEOUT | 如果超过了参数4设置的等待事件,就会返回该宏 |
失败:WSA_WAIT_FAILED
io3.3.1 没信号,继续等
用一个while(1)死循环实现
io3.3.2 有信号,获取重叠结构上的信息(WSAGetOverlappedResult)
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句柄。UDP就只有服务器SOCKET句柄
参数2:有信号的SOCKET句柄对应的重叠结构的地址。UDP就只有服务器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,装WSARecvFrom的参数5:lpflags,具体看上面的表。该参数用于异步执行过程传递一些信息
返回值:
TRUE:成功
FALSE:失败,具体错误码看
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsagetoverlappedresult
这里要注意,如果错误码是10054表示点×关闭窗口,要单独判断释放客户端SOCKET。
DWORD dwTranLen;//传输的长度
DWORD dwParam;//异步传输的参数
//io3.3.2 有信号,获取重叠结构上的信息
if(FALSE==WSAGetOverlappedResult(socketServer,&wolServer,&dwTranLen,TRUE,&dwParam))
{
continue;
}
io3.3.3 信号置空
虽然有些新版本的获取事件WSAGetOverlappedResult函数里面自带信号置空的功能,但是多写这个步骤可以有更好的兼容性,防止信号不置空会使得事件一直处于有信号状态,那么就会在处理信号中死循环。
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaresetevent
BOOL WSAAPI WSAResetEvent(
WSAEVENT hEvent
);
具体代码:
//io3.3.3 信号置空
WSAResetEvent(wolServer.hEvent);
io3.3.4 分类处理
//io3.3.4 分类处理
if (dwTranLen>0)//表示有处理数据
{
//根据mark法2判断是否接收数据
if (0 == recvBuff)
{
printf("WSASendTo执行完毕\\n");
}
else
{
printf("%s\\n",recvBuff);
PostSentTo(&gsi);
//memset(recvBuff,0,548);//mark法1:清空接收缓存,逐位设置为0
recvBuff[0] = 0;//mark法2:为第一位设置特殊值
PostRecvFrom();//io3.1.1有客户端消息,系统空闲,立即完成,跳io3
}
}
补充内容
1.在发送数据的时候,由于要为字符串的/0留出一个位置,因此wbsBuff的长度设置的是547
WSABUF wbsBuff;
wbsBuff.buf = recvBuff;
wbsBuff.len = 547;//留出/0的位置
因此在客户端的recvfrom里面要设置接收长度为547,否则在WSAGetOverlappedResult会报10040的错误码。
后来由于调试的原因,又把长度都统一改会548了,在发送信息小于547的时候运行也没问题。
2.客户端如果没有输入信息,直接回车会将/0直接发送给服务器,而/0我们已经用来标识服务器端recvBuff的第一位,用来判断是否是有信息要收取,因此在客户端加一段代码屏蔽回车键:
gets(strSendBuff);
//用户如果没有输入信息,直接回车就忽略
if (strSendBuff[0] == '\\0')
{
continue;
}
3.vc6.0玩不下去了,换成vs2019后,有两处修改,第一处:
lsi.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
需要添加一个宏,不然有警告:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
第二处:就是常量字符串要做转化后才能赋值给buf。具体可以参考这里:https://blog.csdn.net/rongrongyaofeiqi/article/details/52442169
WSABUF wbsBuff;
//wbsBuff.buf = "调用PostSentTo发送给客户端\\n";
wbsBuff.buf = const_cast <char*> ("调用PostSentTo发送给客户端\\n");
wbsBuff.len = 548;//按理留出/0的位置,设置为547,客户端要做对应修改
以上是关于UDP.5.重叠IO模型:事件通知的主要内容,如果未能解决你的问题,请参考以下文章