WINSOCK.04.异步选择模型

Posted oldmao_2001

tags:

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


https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于TCP/IP的网络编程有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型
这节讲:异步选择模型
这个模型和上节的事件选择模型有点类似,具体内容上节有讲,这里不展开。

异步选择模型简介

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

创建窗口

创建窗口要使用Win32项目,这里如果不适用空项目窗口创建后会带有菜单什么的,很多乱七八糟用不上的代码,这里根据步骤自己创建一个空白窗口。
第一步:创建窗口结构体:WNDCLASSEX(这一步不能少设置属性,否则会在第三步失败)
第二步:注册窗口结构体:RegisterClassEx
第三步:创建窗口:CreateWindowEx
第四步:显示窗口:ShowWindow、GetMessage
第五步:消息循环:TranslateMessage、DispatchMessage
第六步:创建回调函数
具体代码如下:

#include <windows.h>//窗口头文件

//回调函数定义
LRESULT CALLBACK callBackProc(HWND hWnd,UINT msgID, WPARAM wparam,LPARAM lparam);

//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.
//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)
{
	//1.创建窗口结构体https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
	WNDCLASSEX wndc;
	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;//少这个窗口就不能创建成功,报1407错误,但是可以注册成功
	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

	//2.注册窗口结构体
	int regid = RegisterClassEx(&wndc);
	if (regid ==0)//如果注册失败
	{
		int RegisterClassExerr = GetLastError();

	}

	//3.创建窗口https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa
	//参数1是窗口的显示属性,例如总在最前等,可设置多个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
	//通过调用这个函数会得到创建的窗口的句柄
	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;
	}
	
	//4.显示窗口
	//参数1:要显示的窗口句柄
	//参数2:显示窗口的形式,例如:最大化、最小化
	ShowWindow(hWnd,SW_NORMAL);

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

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


	return 0;
}


//定义回调函数
//参数1:窗口句柄
//参数2:消息代号
//参数3:包含SOCKET句柄的参数
//参数4:SOCKET要进行的事件操作和错误码
LRESULT CALLBACK callBackProc(HWND hWnd,UINT msgID, WPARAM wparam,LPARAM lparam)
{
	switch (msgID)
	{
	case WM_DESTROY:
		{
			PostQuitMessage(0);
			break;
		}
//	default:
//		break;
	}
	//对未处理的消息进行默认处理
	return DefWindowProc(hWnd,msgID,wparam,lparam);
}


由于以上代码只是创建窗口的最简化的代码,因此,还有很多不足,例如没有设置刷新重画,因此拖动窗口大小后会花屏。

添加异步选择模型服务器端

SOCKET初始化

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

绑定消息和SOCKET

使用函数为:

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:要绑定的操作,服务器一般是accept,客户端是write,read,close等,可以使用按位【或】同时绑定的多个操作。
关于操作码的含义,和上节一样:

操作码(信号)发生原因绑定操作
FD_READ有客户端消息绑定客户端SOCKET句柄
FD_WRITE可以可客户端发送消息绑定客户端SOCKET句柄,FD_ACCEPT成功后会自动产生这个信号
FD_OOB有带外数据一般不使用
FD_ACCEPT有客户端连接请求绑定客户端SOCKET句柄
FD_CONNECT在客户端编写,绑定服务器端SOCKET句柄
FD_CLOSE客户端下线(正常、强制均可)绑定客户端SOCKET句柄
FD_QOS套接字服务质量状态发生变化网络发生拥堵时发生该事件,获取服务质量状态可用WSAloctl
FD_GROUP_QOS保留操作码
FD_ROUTING_ INTERFACE_CHANGE路由接口改变(动态路由?)重叠I/O模型专用,要先WSAloctl注册才能生效
FD_ADDRESS_ LIST_CHANGE地址列表改变同上
0取消操作码绑定

这个函数放置的位置可以在SOCKET初始化完毕后,消息循环前,也可以放在回调函数的窗口初始化里面(后者麻烦一点,要考虑变量怎么送过去)
然后在回调函数中的switch分支加入消息UM_ASYNCSELECTMSG 的判断。
补充说明:
1.从异步选择模型开始,这里需要对失败进行判断,如果出现失败,要及时关闭对应的socket,由于要分别在主函数和回调函数里面操作,因此我们额外定一个SOCKET数组来辅助关闭操作。当然我们也可以使用FD_SET来完成该操作。
2.由于win32的窗口项目不能使用printf,这里我们可以创建HDC,然后用TextOut来显示信息。

//获取当前窗口的上下文句柄(就是除了菜单和工具栏的其他区域)
	//获取它可用TextOut显示一些信息,因为这里printf没法用
	//要记得释放
	HDC hdc = GetDC(hWnd);
	
	//参数1:当前窗口的上下文句柄
	//参数2,3:要显示的位置坐标,左上角是0,0
	//参数4:要显示的字符串
	//参数5:参数4的长度,这里不用加后面的/0,因此可以sizeof要减一,或者直接用strlen
	TextOut(hdc,10,10,"accept执行",sizeof("accept执行")-1);
	
	ReleaseDC(hWnd,hdc);//释放hdc

根据操作码进行处理

在SOCKET中需要的一些额外参数或者信息,例如:客户端SOCKET句柄。可以从回调函数中的wparam里面读取。SOCKET中操作码之类的信息可以从回调函数中的lparam里面读取。lparam的低位保存的是操作码,高位保存的是错误码。

//获取回调函数带来的SOCKET,注意转定义
			SOCEKT sock = (SOCKET)wparam;
			
			//获取操作码,注意lparam的低位是操作码,高位是错误码
			if (HIWORD(lparam) != 0)
			{
				break;
			}

			//具体操作码
			switch(LOWWORD(lparam))
			{
			case FD_ACCEPT:
				{
					break;
				}
			case FD_READ:
				{
					break;
				}
			case FD_WRITE:
				{
					break;
				}
			case FD_CLOSE:
				{
					;//最后一个分支不用break
				}
			}

然后我们根据操作码来进行不同的处理

FD_ACCEPT

SOCKET socketClient = accept(socksever,NULL,NULL);//获取客户端SOCKET句柄
					if(socketClient == INVALID_SOCKET)//出错拿错误码
					{
						int accepterr = WSAGetLastError();
						break;
					}
					//没错则将事件和客户端SOCKET句柄装消息队列并投递到操作系统
					if (WSAAsyncSelect(socketClient,hWnd,UM_ASYNCSELECTMSG,FD_READ|FD_WRITE|FD_CLOSE)==SOCKET_ERROR)
					{
						int WSAAsyncSelecterr = WSAGetLastError();
						closesocket(socketClient);
					}
					
					//成功则装进SOCKET数组,便于最后释放
					garr_sockAll[gi_sockCout] = socketClient;
					gi_sockCout++;

					break;

这里记得要讲accept的客户端SOCKET句柄弄到数组里面去,便于最后批量释放。

FD_READ

	TextOut(hdc,10,y,"read执行",sizeof("read执行")-1);
					y+=15;

					char str[1000] = {0};
					
					//接收消息
					if (recv(socksever,str,999,0) == SOCKET_ERROR)
					{
						int recverr = WSAGetLastError();
						break;
					}
					//显示消息
					TextOut(hdc,10,y,str,strlen(str));
					y+=15;
					
					break;

FD_WRITE

从代码执行结果上看,一旦客户端连接上服务器,就会一共产生两个消息:
accept
write
这个逻辑和事件选择模型一样的

TextOut(hdc,10,y,"wirte执行",sizeof("wirte执行")-1);
					y+=15;
					//窗口还没地方写消息,写个死的先
					if(send(socksever,"异步选择模型连接成功~",sizeof("异步选择模型连接成功~"),0)==SOCKET_ERROR)
					{
						int FD_WRITEsenderr = WSAGetLastError();						
					}
					break;

FD_CLOSE

在事件选择模型中,关闭SOCKET可以直接在事件和SOCKET数组中直接删除一个数组中的一个元素,然后用最后的元素进行替补即可,不需要去通知系统取消事件监听;在异步选择模型这里,我们关闭SOCEKT需要显示的调用函数来关闭系统对消息的监管。

TextOut(hdc,10,y,"close执行",sizeof("close执行")-1);
					y+=15;
					//通过将后面两个参数设置为0,即可关闭该socket上的消息
					WSAAsyncSelect(sock,hWnd,0,0);
					//关闭socket
					closesocket(sock);
					//记录数组中删除该socket
					for(int i = 0;i < gi_sockCout;i++)
					{
						if(garr_sockAll[i] == sock)
						{
							garr_sockAll[i] = garr_sockAll[gi_sockCout-1];//没有顺序要求,直接用最后一个元素补位
							gi_sockCout --;
							break;
						}
					}
					//最后一个分支不用break

小结

代码

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

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


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

#define UM_ASYNCSELECTMSG WM_USER+1
#define MAX_SOCKET_COUNT 1024

SOCKET garr_sockAll[MAX_SOCKET_COUNT];//为了方便释放SCOKET句柄而定义的数组
int gi_sockCout = 0;

//回调函数定义
LRESULT CALLBACK callBackProc(HWND hWnd,UINT msgID, WPARAM wparam,LPARAM lparam);

//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.
//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)
{
	//1.创建窗口结构体https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
	WNDCLASSEX wndc;
	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

	//2.注册窗口结构体
	int regid = RegisterClassEx(&wndc);
	if (regid ==0)//如果注册失败
	{
		int RegisterClassExerr = GetLastError();

	}

	//3.创建窗口https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa
	//参数1是窗口的显示属性,例如总在最前等,可设置多个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
	//通过调用这个函数会得到创建的窗口的句柄
	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;
	}
	
	//4.显示窗口
	//参数1:要显示的窗口句柄
	//参数2:显示窗口的形式,例如:最大化、最小化
	ShowWindow(hWnd,SW_NORMAL);

	//optional.更新窗口,showwindow后没变化可以不用update
	UpdateWindow(hWnd);
	
	/******************这里放SOCKET初始化的内容******************/
	/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
	WORD wdVersion=MAKEWORD(2,2);
	int a=*((char*)&wdVersion); 
	int b=*((char*)&wdVersion+1);

	WSADATA wdScokMsg;
	int nRes = WSAStartup(wdVersion,&wdScokMsg);


	if (0 != nRes)
	{
		switch(nRes)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。\\n");
				break; 
			case WSAVERNOTSUPPORTED: 
				break; 
			case WSAEINPROGRESS: 
				break; 
			case WSAEPROCLIM: 
				break; 
			case WSAEFAULT:
				break;
		}
		return 0;
	
	}

	//校验版本	
	if (2!=HIBYTE(wdScokMsg.wVersion)|| 2!=LOBYTE(wdScokMsg.wVersion))
	{
			printf("版本有问题!\\n");
			WSACleanup();
			return 0;
	}

	SOCKET socketServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);


	if(INVALID_SOCKET == socketServer)
	{
		int err=WSAGetLastError();
		
		
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_port = htons(12345);//用htons宏将整型转为端口号的无符号整型

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

		return 0;
	}
	printf("服务器端bind成功!\\n");

	if(SOCKET_ERROR==listen(socketServer,SOMAXCONN))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器监听失败错误码为:%d\\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	
	printf("服务器端监听成功!\\n");
	/******************SOCKET初始化代码结束******************/

	//异步选择模型1.绑定消息和服务器SOCKET,并投递给操作系统
	if (WSAAsyncSelect(socketServer,hWnd,UM_ASYNCSELECTMSG,FD_ACCEPT)==SOCKET_ERROR)//失败处理
	{
		int WSAAsyncSelecterr = WSAGetLastError();
		closesocket(socketServer);
		WSACleanup();
		return 0;
	}
	
	//成功则装进SOCKET数组,便于最后释放
	garr_sockAll[gi_sockCout] = socketServer;
	gi_sockCout++;

	
	//5.消息循环,窗口创建成功后,窗口的消息队列也将同步创建,此时对窗口的所有操作都会产生相应的消息。
	//GetMessage()除非捕获退出消息会返回0,其他消息都不为0
	//参数1:消息结构体地址
	//参数2:获取指定窗口的消息,如果填NULL就是获取应用程序(多个窗口)的消息
	//参数34:处理消息的范围,都填0表示不限制消息的范围
	MSG msg;
	while (GetMessage(&msg以上是关于WINSOCK.04.异步选择模型的主要内容,如果未能解决你的问题,请参考以下文章

UDP.4.异步选择模型

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

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

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

网络IO模型-异步选择

python常用代码