UDP套接字可以用异步选择选择模型吗

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UDP套接字可以用异步选择选择模型吗相关的知识,希望对你有一定的参考价值。

参考技术A 可以,异步选择模型只是以事件的形式告诉你套接字是否可读写状态

UDP.2.SELECT模型


https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于UDP的网络编程还有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型
这节讲基于UDP的SELECT模型。
对于TCP而言,TCP的recv、accept每次只能处理一个客户端,其他客户端都处于等待状态,服务器不能同时响应多个客户端的,SELECT可以有效处理TCP的recv、accept的 等待阻塞的问题。但是对于UDP而言,UDP没有accept函数,UDP的recvfrom本来就是针对多个客户端的,因此,SELECT模型对于UDP的性能提升其实不多。

SELECT模型的逻辑

这里主要以服务器端为例,客户端基本没有什么变化
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET
5、绑定地址与端口
6、SELECT
可以看到,前面5个步骤和基本模型是一样的,所以主要看SELECT处理:
6.1、创建SOCKET句柄集合 fd_set
6.2、SELECT查询,看哪个句柄有信号
6.3、根据查询结果进行处理
大概步骤如上所示。

6.1

这里我们不能使用自己创建的SOCKET句柄数组,因为SELECT函数只接收fd_set集合

typedef struct fd_set {
  u_int  fd_count;
  SOCKET fd_array[FD_SETSIZE];
} fd_set, FD_SET, *PFD_SET, *LPFD_SET;

集合的四个操作前面有,这里拷贝过来:
对于fd_set 数组,WINSOCK也相应的提供了4个函数来进行操作:

//清空fd_set数组
	FD_ZERO();

其原理并不去删除数组内容,而是直接把fd_set中的fd_count设置为0。

//添加一个SOCKET句柄
	FD_SET(socketServer,&clientSockets)

FD_SET添加相同的句柄会被忽略,如果数组满了则无法添加。

//删除指定SOCKET句柄
	FD_CLR(socketServer,&clientSockets)

FD_CLR删除指定SOCKET句柄后,会将在该句柄后面所有的元素依次向前移动一位。但是SOCKET句柄内存需要手动释放(要接着调用closesocket)。

//判断某个SOCKET句柄是否在数组中
	FD_ISSET(socketServer,&clientSockets);

这里具体代码如下:

//6.1、创建SOCKET句柄集合 fd_set
		fd_set fd;
		FD_ZERO(&fd);//清空
		FD_SET(socketServer,&fd);//添加服务器句柄

6.2

这里主要是SELECT查询,看哪个句柄有信号,用到的函数是:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select

int WSAAPI select(
  int           nfds,
  fd_set        *readfds,
  fd_set        *writefds,
  fd_set        *exceptfds,
  const timeval *timeout
);

参数1nfds:为兼容Berkeley socket标准而保留的无用参数,默认写0即可。
参数2:*readfds,检查是否有可读的SOCKET,当select函数调用时,将要轮询的SOCKET数组放到FD_SET(这里是传址调用)后丢给系统,
TCP协议下:系统将有请求的SOCKET句柄(需要服务器recv)再填到FD_SET里面丢回来(当然这里的请求有两种,前面提到过,如果是服务器句柄,就需要accept,如果是客户端句柄就需要recv);
UDP协议下:没有连接这个东西,因此只剩下判断那个客户端句柄需要收数据。
参数3: *writefds,检查是否有可写的SOCKET,和上面功能类似,只不过针对send,调用select后,这个地址里面就是可以send数据的客户端SOCKET句柄,但是由于对于send这个功能,服务器是没有执行阻塞的(我想怎么发送就怎么发送消息,无需经过客户端同意)因此整个参数用得不多。
对于UDP而言,服务器也不知道客户端的状态(面向非连接),因此这个参数对于UDP而言没什么用,这里填NULL。
参数4:*exceptfds,检查是否有出错的SOCKET,当select函数调用时,将要轮询的SOCKET数组放到FD_SET(这里是传址调用)后丢给系统,系统将有错误的SOCKET句柄再填到FD_SET里面丢回来。然后可以用下面这个函数得到具体错误码:

int WSAAPI getsockopt(
  SOCKET s,
  int    level,
  int    optname,
  char   *optval,
  int    *optlen
);

这个参数对于TCP而言,可以检测客户端出错的SOCKET句柄,但是UDP没有连接状态的客户端,因此该参数也无用,如果只针对服务器SOCKET,如果有问题,我们可以直接用WSAGetLastError直接取错误就好,这里填NULL。
参数5:*timeout,是timeval结构体,用来设置轮询FD_SET完无请求情况下的最大等待时间间隔(最大的含义就是最多等这么久,如果等待中间发生请求则终止等待,直接返回)

typedef struct timeval {
  long tv_sec;
  long tv_usec;
} TIMEVAL, *PTIMEVAL, *LPTIMEVAL;

两个成员都是用来设置时间间隔的,第一个是秒;第二个是微秒(百万分之一秒,千分之一毫秒)

状态*timeout设置值select状态
非阻塞0 0轮询FD_SET完无请求后,无等待,立刻返回
半阻塞4 2轮询FD_SET完无请求后,最多等待4秒2微秒后返回(等待期间有请求则立刻返回)
全阻塞NULL轮询FD_SET完无请求后,不返回,等待有请求后才返回

select的全阻塞状态并不代表服务器在等待某个客户端请求,而是等待FD_SET里面所有的客户端请求。select函数除了有等待之外,每个状态都有执行阻塞。
在UDP协议下,如果select设置等待时间为NULL,和recvfrom功能一模一样。

select返回值:
0:客户端在等待时间内无请求,可以进行下一次select操作(continue;);
>0:有客户端请求,此时要分服务器或者客户端分别进行判断;
SOCKET_ERROR:发生错误,利用WSAGetLastError()获取错误码。

注意:select函数的2.3.4的参数是传址引用,因此不能直接把要轮询的FD_SET丢进去,否则会被覆盖。

//6.2、SELECT查询,看哪个句柄有信号
		struct timeval ta;
		ta.tv_sec = 3;
		ta.tv_usec = 0;
		int nRet = select(0,&fd,NULL,NULL,&ta);
		if(nRet ==0)
		{
			printf("select超时\\n");
			continue;
		}
		
		if(nRet ==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("select失败错误码为:%d\\n",err);
			continue;
		}

6.3

这里没啥好说,就是直接照搬基本模型的代码,外层套一个判断即可:

//6.3、根据查询结果进行处理
		//没有什么问题就是可以收信息
		if(nRet > 0)
		{
			//收
			struct sockaddr sa;
			int iSaLen = sizeof(sa);
			char strRecvBuff[548]={0};
			
			if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
			{
				int err = WSAGetLastError();//取错误码
				printf("服务器recvfrom失败错误码为:%d\\n",err);
				continue;
			}
			printf("服务器recvfrom消息是:%s\\n",strRecvBuff);
			
			//发
			if(sendto(socketServer,"This is a message from server~!",sizeof("This is a message from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
			{
				int err = WSAGetLastError();//取错误码
				printf("服务器sendto失败错误码为:%d\\n",err);
				continue;
			}
		}

整体代码

#include <stdio.h>
//1、包含网络头文件网络库
# include <WinSock2.h>
# pragma comment(lib, "Ws2_32.lib")

SOCKET socketServer;

//处理强制关闭事件
BOOL WINAPI CtrlFun(DWORD dwType)
{
	switch (dwType)
	{
	case CTRL_CLOSE_EVENT:
		//关闭socket
		closesocket(socketServer);
		//关闭网络库
		WSACleanup();
		break;
	}
	return FALSE;
}

int main()
{
	//投递关闭事件
	SetConsoleCtrlHandler(CtrlFun, TRUE);

	WORD wVersionRequested = MAKEWORD(2,2);//版本
	WSADATA wsaDATA;

	//2、打开网络库
	int iret = WSAStartup(wVersionRequested,&wsaDATA);
	if (iret!=0)
	{
		//有错
		switch(iret)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。");
				break; 
			case WSAVERNOTSUPPORTED: 
				printf("解决方案:更新网络库");
				break; 
			case WSAEINPROGRESS: 
				printf("解决方案:重启。。。");
				break; 
			case WSAEPROCLIM: 
				printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
				break; 
			case WSAEFAULT:
				printf("解决方案:程序有误");
				break;
		}
		getchar();
		return 0;
	}

	//3、校验版本,只要有一个不是2,说明系统不支持我们要的2.2版本	
	if (2!=HIBYTE(wsaDATA.wVersion)|| 2!=LOBYTE(wsaDATA.wVersion))
	{
			printf("版本有问题!");
			WSACleanup();//关闭网络库
			return 0;
	}

	// 4、创建SOCKET
	socketServer = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	if(INVALID_SOCKET == socketServer)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	//5、绑定地址与端口
	struct sockaddr_in si;
	si.sin_family = AF_INET;//这里要和创建SOCKET句柄的参数1类型一样
	si.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型
	si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

	if(bind(socketServer,(struct sockaddr*)&si,sizeof(si))==SOCKET_ERROR)
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
	}

	
	while(1)
	{
		//6、SELECT
		//6.1、创建SOCKET句柄集合 fd_set
		fd_set fd;
		FD_ZERO(&fd);//清空
		FD_SET(socketServer,&fd);//添加服务器句柄

		//6.2、SELECT查询,看哪个句柄有信号
		struct timeval ta;
		ta.tv_sec = 3;
		ta.tv_usec = 0;
		int nRet = select(0,&fd,NULL,NULL,&ta);
		if(nRet ==0)
		{
			printf("select超时\\n");
			continue;
		}
		
		if(nRet ==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("select失败错误码为:%d\\n",err);
			continue;
		}

		//6.3、根据查询结果进行处理
		//没有什么问题就是可以收信息
		if(nRet > 0)
		{
			//收
			struct sockaddr sa;
			int iSaLen = sizeof(sa);
			char strRecvBuff[548]={0};
			
			if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
			{
				int err = WSAGetLastError();//取错误码
				printf("服务器recvfrom失败错误码为:%d\\n",err);
				continue;
			}
			printf("服务器recvfrom消息是:%s\\n",strRecvBuff);
			
			//发
			if(sendto(socketServer,"This is a message from server~!",sizeof("This is a message from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
			{
				int err = WSAGetLastError();//取错误码
				printf("服务器sendto失败错误码为:%d\\n",err);
				continue;
			}
		}

	}
	

	closesocket(socketServer);//与4、创建SOCKET对应,如果有创建客户端SOCKET句柄,也要关闭
	//关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

以上是关于UDP套接字可以用异步选择选择模型吗的主要内容,如果未能解决你的问题,请参考以下文章

UDP和Socket通信步骤

多个套接字可以与 UDP 的同一端口相关联吗?

在 Iphone SDK 上接收带有异步 Udp 套接字的 UPS 包时出现问题

设置 UDP 套接字的源 IP

UDP.4.异步选择模型

Python网络编程—UDP套接字广播