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读取数据缓冲区指定长度的数据后(指定长度大于数据长度则全部读取),数据缓冲区中被读取的数据会清除,把空间留出来给别的消息进来(不清理的话时间长了内存会溢出,数据缓冲区数据结构相当于队列)。
例如数据缓冲区中有如下数据:

abcdef

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

cdef

这个时候再调用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。由于不清空被读取的数据,缓冲区还是不变:

abcdef

如果再次执行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模型:事件通知的主要内容,如果未能解决你的问题,请参考以下文章

UDP.6.重叠IO模型:完成例程

Socket编程模型之完毕port模型

win32网络模型之重叠I/O

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

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

linux五种IO模型与事件驱动模型