TCP.04.异步选择模型


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

异步选择模型简介

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

回调函数

回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
以上解释来自百度百科。
用通俗的例子来理解:
callback本意是回电。你可以打电话(call)给别人,也可以留下电话号码,让别人回电话(callback),更具体一点:
我到图书馆借一本《毛老师的网络编程秘籍》,而这本书图书馆还没有,于是我在馆员这里留下我的电话,等图书采购到货后通知我,然后我就先走了,等我接到电话后,到图书馆里成功借到了书。
在这个例子里,我的电话号码是回调函数,留电话是登记回调函数,图书采购到货是触发回调关联事件,接到电话通知是调用回调函数,我成功借到书是响应回调事件。
这里面如果我把电话号码留错了,那我当然不会收到电话通知,也无法借到书了,因此定义回调函数和调用回调函数的名称要一致。

在等待电话过程中,我并没有在图书馆傻等,而是该干嘛干嘛,这个就是非阻塞。
这里需要注意,回调函数调用是自动执行的,只要满足特定条件(图书采购采购)。

创建窗口

创建窗口要使用Win32项目:
在这里插入图片描述

如果有报字符集错误:
无法将参数 1 从“const char [***]”转换为“LPCWSTR”
可以修改以下项目属性设置:
在这里插入图片描述

这里如果不使用空项目窗口创建后会带有菜单什么的,很多乱七八糟用不上的代码,这里根据步骤自己创建一个空白窗口。

第一步:创建窗口结构体: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.可理解为应用程序的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)
{
	//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;
}


//6.定义回调函数
//参数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取消操作码绑定

返回:
成功:0
失败:SOCKET_ERROR
这个函数放置的位置可以在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,NULL,0,0))
	{
		TranslateMessage(&msg);//将消息转换为可识别的代号
		DispatchMessage(&msg);//分发消息,让回调函数来处理
		
	}
	
	//关闭数组的sockets
	for(int i = 0;i < gi_sockCout;i++)
	{
		closesocket(garr_sockAll[i]);
	}
	
	WSACleanup();//清理网络库

	return 0;
}

int y =0;//显示消息的y坐标,每次显示消息后y坐标+15,避免消息重复在同一个区域显示


//定义回调函数
//参数1:窗口句柄
//参数2:消息代号
//参数3:包含SOCKET句柄的参数
//参数4:SOCKET要进行的事件操作和错误码
LRESULT CALLBACK callBackProc(HWND hWnd,UINT msgID, WPARAM wparam,LPARAM lparam)
{
	//获取当前窗口的上下文句柄(就是除了菜单和工具栏的其他区域)
	//获取它可用TextOut显示一些信息,因为这里printf没法用
	//要记得释放
	HDC hdc = GetDC(hWnd);

	switch (msgID)
	{
	//异步选择模型2.根据操作码进行处理
	case UM_ASYNCSELECTMSG:
		{
			//https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
			//这里仅做测试用
			//MessageBox(NULL,"捕获SOCKET绑定消息","提示",MB_OK);
			
		
			//获取回调函数带来的SOCKET,注意转定义
			SOCKET sock = (SOCKET)wparam;
			
			//获取操作码,注意lparam的低位是操作码,高位是错误码
			if (HIWORD(lparam) != 0)
			{
				if (HIWORD(lparam) == WSAECONNABORTED)//直接关闭产生WSAECONNABORTED错误码
				{
					TextOut(hdc,10,y,"WSAECONNABORTED执行",sizeof("WSAECONNABORTED执行")-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;
						}
					}

					WSACleanup();//清理网络库
				}
					break;
			}

			//具体操作码
			switch(LOWORD(lparam))
			{
			case FD_ACCEPT:
				{
					//参数1:当前窗口的上下文句柄
					//参数2,3:要显示的位置坐标,左上角是0,0
					//参数4:要显示的字符串
					//参数5:参数4的长度,这里不用加后面的/0,因此可以sizeof要减一,或者直接用strlen
					TextOut(hdc,10,y,"accept执行",sizeof("accept执行")-1);
					y+=15;

					SOCKET socketClient = accept(sock,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;
				}
			case FD_READ:
				{
					TextOut(hdc,10,y,"read执行",sizeof("read执行")-1);
					y+=15;

					char str[1000] = {0};
					
					//接收消息
					if (recv(sock,str,999,0) == SOCKET_ERROR)
					{
						int recverr = WSAGetLastError();
						break;
					}
					//显示消息
					TextOut(hdc,10,y,str,strlen(str));
					y+=15;
					
					break;
				}
			case FD_WRITE:
				{
					TextOut(hdc,10,y,"wirte执行",sizeof("wirte执行")-1);
					y+=15;
					//窗口还没地方写消息,写个死的先
					if(send(sock,"异步选择模型连接成功~",sizeof("异步选择模型连接成功~"),0)==SOCKET_ERROR)
					{
						int FD_WRITEsenderr = WSAGetLastError();						
					}
					break;
				}
			case FD_CLOSE:
				{
					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;
						}
					}

					WSACleanup();//清理网络库
					//最后一个分支不用break
				}
			}

			break;
		}
	case WM_CREATE:
		{
			//窗口初始化执行代码放在这里,有代码只需要执行一次的也放这里
			//WSAAsyncSelect也可以放这里执行
			break;
		}
	case WM_DESTROY:
		{
			PostQuitMessage(0);
			break;
		}
//	default:
//		break;
	}

	ReleaseDC(hWnd,hdc);//释放hdc

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


优化思路

虽然MSDN上没有指定异步选择模型一个窗口处理的SOCKET句柄的上限,但是根据事件选择模型的经验,我们知道由于硬件方面的原因,还是应该要设置一个上限(64个),另外,我们还可以创建多个线程,每个线程维护一个窗口的消息。

现有问题

当客户端多次send发送字符串,服务器会收到多个字符串,但是第一次接收消息,会一次收完所有字符串。就是一次FD_READ,接收多个字符串。因为发送到服务器的字符串都是放在buff里面的。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值