UDP.4.异步选择模型

Posted oldmao_2001

tags:

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


https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于UDP的网络编程还有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型
这节讲基于UDP的异步选择模型。

异步选择模型简介

操作系统为每个窗口创建一个消息队列并且维护,因此异步选择模型是基于窗口的异步模型(只能Windows上玩,别的系统可能不是基于消息机制的)。该模型的思路是:
1.将SOCKET句柄绑定在消息上,并投递给系统,(事件选择模型是绑定在事件上投递给系统)由系统来维护消息队列;
2.查询消息队列,取出队头消息进行处理。与TCP不一样的是,UDP只用处理2种消息。

裸窗口的创建

创建窗口要使用Win32项目,这里如果不适用空项目窗口创建后会带有菜单什么的,很多乱七八糟用不上的代码,这里根据步骤自己创建一个空白窗口。
先加载头文件和主函数:

//#include <windows.h>//窗口头文件与WinSock2.h有重复定义,使用一个即可

#include <WinSock2.h>
#include <stdio.h>


#pragma comment(lib, "Ws2_32.lib")
//hInstance is something called a "handle to an instance" or "handle to a module." The operating system uses this value to identify the executable (EXE) when it is loaded in memory. The instance handle is needed for certain Windows functions—for example, to load icons or bitmaps.可理解为应用程序的ID,是一个整型
//hPrevInstance has no meaning. It was used in 16-bit Windows, but is now always zero.已废弃
//pCmdLine contains the command-line arguments as a Unicode string.命令行传入的参数
//nCmdShow is a flag that says whether the main application window will be minimized, maximized, or shown normally.窗口显示方式:最大化、最小化、正常等
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nShowCmd)
{
	return 0;
}

第一步:创建窗口结构体:WNDCLASSEX(这一步不能少设置属性,否则会在第三步失败)

//w1.创建窗口结构体https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
	WNDCLASSEX wndc;//WNDCLASSEX比WNDCLASS多两个属性
	wndc.cbClsExtra = 0;//窗口类额/带外数据,不用就写0
	wndc.cbSize = sizeof(WNDCLASSEX);//窗口类大小
	wndc.cbWndExtra = 0;//窗口额/带外数据,不用就写0,第一个是窗口类,这个是窗口
	wndc.hbrBackground = NULL;//背景颜色,用默认白色
	wndc.hCursor = NULL;//默认光标
	wndc.hIcon = NULL;//默认窗口图标
	wndc.hIconSm = NULL;//默认任务栏图标
	wndc.hInstance = hInstance;//当前程序句柄,少这个就不能创建成功
	wndc.lpfnWndProc = callBackProc;//回调函数名称,要和定义的回调函数名字一样,由系统调用
	wndc.lpszClassName = "emptywnd";//窗口类名
	wndc.lpszMenuName = NULL;//窗口菜单名称
	wndc.style = CS_HREDRAW|CS_VREDRAW;//水平刷新+垂直刷新https://docs.microsoft.com/zh-cn/windows/win32/winmsg/window-class-styles

C95标准化了两种表示大型字符集的方法:宽字符(wide character,Unicode字符集,该字符集内每个字符使用相同的位长,2个字符)以及多字节字符(multibyte character,每个字符可以是一到多个字节不等,例如中文占2字符,英文占1字符)。
这里如果是VS2019这里在窗口类名这里可能会报字符集转换出错,有三种解决方案将宽字符集转多字符集:
1.在项目属性里面吧Unicode字符集换成多字符集;
2.将"emptywnd"前面加L,变成L"emptywnd";
3.加头文件#include <tchar.h>,然用:_T(“emptywnd”)或者TEXT(“emptywnd”)
第二步:注册窗口结构体:RegisterClassEx

	//w2.注册窗口结构体
	int regid = RegisterClassEx(&wndc);//上面是WNDCLASSEX,这里就不能用RegisterClass
	if (regid ==0)//如果注册失败
	{
		int RegisterClassExerr = GetLastError();

	}

第三步:创建窗口:CreateWindowEx

//w3.创建窗口https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa
	//参数1是EX窗口的显示属性,例如总在最前等,可用【|】设置多个https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
	//参数2要和上面创建窗口结构处的窗口类名一模一样
	//参数3是窗口显示的标题名称,就是左上角的名称
	//参数4是窗口的风格,例如是否有最大化最小化按钮,是否有滚动条等,可以设置多个https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
	//参数56是窗口位置坐标,以像素为单位
	//参数78是窗口尺寸,以像素为单位
	//参数9是指定父窗口句柄,不指定就用NULL
	//参数10是指定窗口的菜单句柄,要和上面的一样,没有就用NULL
	//参数11是当前应用的句柄,用winmain的参数hInstance就可以
	//参数12是给回调函数传的参数,这里没有就用NULL
	//返回:成功,窗口的句柄;失败,NULL
	HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,"emptywnd","窗口标题",WS_OVERLAPPEDWINDOW,100,100,640,480,NULL,NULL,hInstance,NULL);

	if (hWnd == NULL)//如果创建失败
	{
		int CreateWindowExerr = GetLastError();
		//MessageBox(0,"注册窗口失败", "提示", MB_OK);
		return 0;
	}

第四步:显示窗口:ShowWindow

//w4.显示窗口
	//参数1:要显示的窗口句柄
	//参数2:显示窗口的形式,例如:最大化、最小化
	ShowWindow(hWnd,SW_NORMAL);

	//optional.更新窗口,showwindow后没变化可以不用update
	UpdateWindow(hWnd);

第五步:消息循环:GetMessage、TranslateMessage、DispatchMessage

//w5.消息循环,窗口创建成功后,窗口的消息队列也将同步创建,此时对窗口的所有操作都会产生相应的消息。
	//GetMessage()除非捕获退出消息会返回0,其他消息都不为0
	//参数1:消息结构体地址,传址调用
	//参数2:获取指定窗口的消息,如果填NULL就是获取应用程序(多个窗口)的消息
	//参数34:处理消息的范围,都填0表示不限制消息ID的范围
	MSG msg;
	while (GetMessage(&msg,NULL,0,0))
	{
		TranslateMessage(&msg);//将消息转换为可识别的代号
		DispatchMessage(&msg);//分发消息,让回调函数来处理
		
	}

第六步:创建回调函数

//w6.定义回调函数
//参数1:窗口句柄
//参数2:消息代号
//参数3:包含SOCKET句柄的参数
//参数4:SOCKET要进行的事件操作和错误码
LRESULT CALLBACK callBackProc(HWND hWnd, UINT msgID, WPARAM wparam, LPARAM lparam)
{
	switch (msgID)
	{
	case WM_DESTROY:
		{
			PostQuitMessage(0);//传递退出消息,跳出w5.消息循环
			break;
		}
//	default:
//		break;
	}

	//对未处理的消息进行默认处理
	return DefWindowProc(hWnd,msgID,wparam,lparam);
}

裸窗口的异步选择模型

SOCKET初始化

有了最基本的窗口之后,我们就可以在这个基础上增加相应的SOCKET代码。和之前一样,异步选择模型服务器端的SOCKET代码套路的前面部分是一样的:
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET
5、绑定地址与端口
以上代码属于网络的初始化操作,因此不能放到窗口的第五步消息循环里面,而是放在第五步消息循环之前执行即可,当然也可以弄成一个函数的形式然后调用。
另外一个要注意的是,把SOCKET库文件拷贝过来的时候,需要把windows.h注释掉,因为二者有重复定义。
准备工作好了以后,下面就是异步选择模型的重点代码了

6、异步选择

6.1绑定消息和SOCKET

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaasyncselect

int WSAAPI WSAAsyncSelect(
  SOCKET s,
  HWND   hWnd,
  u_int  wMsg,
  long   lEvent
);

其作用是将消息和SOCKET绑定后,投递给操作系统。
参数1:SOCKET句柄
参数2:窗口句柄,指消息和SOCKET要投递到的指定窗口句柄中(每个窗口都有自己的消息队列)
参数3:SOCKET绑定的消息ID,不可与已有的系统消息(WM_开头)重复,可以用WM_USER+X来定义该消息ID,使用UM_开头。

#define UM_ASYNCSELECTMSG WM_USER+1//用户自定义消息

参数4:要绑定的操作,UDP这里就两个:FD_READ|FD_WRITE
返回:
成功:0
失败:SOCKET_ERROR
具体代码:

	//6、异步选择模型
	//6.1.绑定消息和服务器SOCKET,并投递给操作系统
	if (WSAAsyncSelect(socketServer,hWnd,UM_ASYNCSELECTMSG,FD_READ|FD_WRITE)==SOCKET_ERROR)//失败处理
	{
		int WSAAsyncSelecterr = WSAGetLastError();
		printf("WSAAsyncSelect失败错误码为:%d\\n",WSAAsyncSelecterr);
		closesocket(socketServer);
		WSACleanup();
		return 0;
	}

6.2根据操作码进行处理

这里要进入回调函数中处理,如果消息队列里面传送过来我们自定义的UM_ASYNCSELECTMSG消息,那么这个时候就要根据我们设置的FD_READ|FD_WRITE两个操作进行处理。
这里先看下UDP协议下传进来的参数,由于UDP只有服务器SOCKET句柄,客户端没有连接操作,因此,回调函数中的参数3就是服务器SOCKET句柄,获取代码如下:

	//获取回调函数带来的SOCKET,注意转定义
	SOCKET sockServer = (SOCKET)wparam;

服务器SOCKET中操作码的信息可以从回调函数中的lparam里面读取。lparam的低位保存的是操作码(LOWORD(lparam)),高位保存的是错误码(HIWORD(lparam))。
由于win32的窗口项目不能使用printf,这里我们可以创建HDC,然后用TextOut来显示信息。每次移动y轴坐标后显示信息。

//异步选择模型6.2.根据操作码进行处理
	case UM_ASYNCSELECTMSG:
		{
			//https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
			//这里仅做测试用
			//MessageBox(NULL,"捕获SOCKET绑定消息","提示",MB_OK);
			
		
			//获取回调函数带来的SOCKET,注意转定义
			SOCKET socketServer = (SOCKET)wparam;
			
			//获取操作码,注意lparam的低位是操作码,高位是错误码
			if (HIWORD(lparam) == 0)
			{
				//获取当前窗口的上下文句柄(就是除了菜单和工具栏的其他区域)
				//获取它可用TextOut显示一些信息,因为这里printf没法用
				//要记得释放
				HDC hdc = GetDC(hWnd);
				if (FD_READ == LOWORD(lparam))
				{
					TextOut(hdc,10,y,"FD_READ执行ING!",sizeof("FD_READ执行ING!")-1);
					y+=16;//往下挪16个像素再输出

					//这里是基本模型里面的内容
					//收
					struct sockaddr sa;
					int iSaLen = sizeof(sa);
					char strRecvBuff[548]={0};

					if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
					{
						int err = WSAGetLastError();//取错误码
						char strerr = {0};
						//_itoa是整型转字符,最后一个参数代表进制
						TextOut(hdc,10,y,_itoa(err,&strerr,10),sizeof(_itoa(err,&strerr,10))-1);
						y+=16;
					}
					
					TextOut(hdc,10,y,strRecvBuff,strlen(strRecvBuff));
					y+=16;

					//发
					if(sendto(socketServer,"This is a asyncmessage from server~!",sizeof("This is a asyncmessage from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
					{
						int err = WSAGetLastError();//取错误码
						char strerr = {0};
						TextOut(hdc,10,y,_itoa(err,&strerr,10),sizeof(_itoa(err,&strerr,10))-1);
						y+=16;
					}

				}
				
				if (FD_WRITE == LOWORD(lparam))//FD_WRITE操作
				{
					TextOut(hdc,10,y,"FD_WRITE执行ING!",sizeof("FD_WRITE执行ING!")-1);
					y+=16;
				}
				ReleaseDC(hWnd,hdc);//释放hdc

			}			

			break;
		}

非裸窗口的创建

第一步:创建窗口结构体:WNDCLASSEX(这一步不能少设置属性,否则会在第三步失败)
第二步:注册窗口结构体:RegisterClassEx

//w1.创建窗口结构体:WNDCLASSEX
	WNDCLASSEX wcex;
	
	wcex.cbSize = sizeof(WNDCLASSEX); 
	
	wcex.style			= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= (WNDPROC)WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon			= LoadIcon(hInstance, (LPCTSTR)IDI_UDPNONEMPTYASYNSELECT);
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName	= (LPCSTR)IDC_UDPNONEMPTYASYNSELECT;
	wcex.lpszClassName	= szWindowClass;
	wcex.hIconSm		= LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
	
	//w2.注册窗口结构体:RegisterClassEx
	return RegisterClassEx(&wcex);

第三步:创建窗口:CreateWindowEx
第四步:显示窗口:ShowWindow

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	HWND hWnd;
	
	hInst = hInstance; // Store instance handle in our global variable
	
	//w3.创建窗口:CreateWindowEx
	hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
	
	if (!hWnd)
	{
		return FALSE;
	}
	
	//w4.显示窗口:ShowWindow
	ShowWindow(hWnd, nCmdShow);
	UpdateWindow(hWnd);
	
	return TRUE;
}

第五步:消息循环:GetMessage、TranslateMessage、DispatchMessage

// w5.消息循环:GetMessage、TranslateMessage、DispatchMessage
	// Main message loop:
	while (GetMessage(&msg, NULL, 0, 0)) 
	{
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	

第六步:创建回调函数

//w6.创建回调函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

非裸窗口的异步选择模型

1-5

SOCKET代码加在消息循环之前:

while (GetMessage(&msg, NULL, 0, 0)) 

和之前一样,异步选择模型服务器端的SOCKET代码套路的前面部分是一样的:
1、包含网络头文件网络库

//1、包含网络头文件网络库
#include <WinSock2.h>
#include <stdio.h>


#pragma comment(lib, "Ws2_32.lib")

#define UM_ASYNCSELECTMSG WM_USER+1//用户自定义消息

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、校验版本

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

4、创建SOCKET

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

5、绑定地址与端口

//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();//清理网络库
	}

6、异步选择

6.1绑定消息和SOCKET

	//6、异步选择模型
	//6.1.绑定消息和服务器SOCKET,并投递给操作系统
	if (WSAAsyncSelect(socketServer,hWnd,UM_ASYNCSELECTMSG,FD_READ|FD_WRITE)==SOCKET_ERROR)//失败处理
	{
		int WSAAsyncSelecterr = WSAGetLastError();
		printf("WSAAsyncSelect失败错误码为:%d\\n",WSAAsyncSelecterr);
		closesocket(socketServer);
		WSACleanup();
		return 0;
	}

6.2 根据操作码进行处理

在主窗口回调函数的switch下面加一个case,坐标变量在函数外面定义一下

int y =0;//显示消息的y坐标,每次显示消息后y坐标+16,避免消息重复在同一个区域显示
//异步选择模型6.2.根据操作码进行处理
	case UM_ASYNCSELECTMSG:
		{
			//https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
			//这里仅做测试用
			//MessageBox(NULL,"捕获SOCKET绑定消息","提示",MB_OK);
			
		
			//获取回调函数带来的SOCKET,注意转定义
			SOCKET socketServer = (SOCKET)wparam;
			
			//获取操作码,注意lparam的低位是操作码,高位是错误码
			if (HIWORD(lparam) == 0)
			{
				//获取当前窗口的上下文句柄(就是除了菜单和工具栏的其他区域)
				//获取它可用TextOut显示一些信息,因为这里printf没法用
				//要记得释放
				HDC hdc = GetDC(hWnd);
				if (FD_READ == LOWORD(lparam))
				{
					TextOut(hdc,10,y,"FD_READ执行ING!",sizeof("FD_READ执行ING!")-1);
					y+=16;//往下挪16个像素再输出

					//收
					struct sockaddr sa;
					int iSaLen = sizeof(sa);
					char strRecvBuff[548]={0};

					if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
					{
						int以上是关于UDP.4.异步选择模型的主要内容,如果未能解决你的问题,请参考以下文章

在MFC中能否使用异步IO的事件选择模型

在哪里以及如何使用片段填充我的标签

UEFI UDP 接收令牌

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

windows下的IO模型之异步选择(WSAAsyncSelect)模型

网络IO模型-异步选择