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 <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nShowCmd)
{
return 0;
}
第一步:创建窗口结构体:WNDCLASSEX(这一步不能少设置属性,否则会在第三步失败)
WNDCLASSEX wndc;
wndc.cbClsExtra = 0;
wndc.cbSize = sizeof(WNDCLASSEX);
wndc.cbWndExtra = 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;
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
int regid = RegisterClassEx(&wndc);
if (regid ==0)
{
int RegisterClassExerr = GetLastError();
}
第三步:创建窗口:CreateWindowEx
HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,"emptywnd","窗口标题",WS_OVERLAPPEDWINDOW,100,100,640,480,NULL,NULL,hInstance,NULL);
if (hWnd == NULL)
{
int CreateWindowExerr = GetLastError();
return 0;
}
第四步:显示窗口:ShowWindow
ShowWindow(hWnd,SW_NORMAL);
UpdateWindow(hWnd);
第五步:消息循环:GetMessage、TranslateMessage、DispatchMessage
MSG msg;
while (GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
第六步:创建回调函数
LRESULT CALLBACK callBackProc(HWND hWnd, UINT msgID, WPARAM wparam, LPARAM lparam)
{
switch (msgID)
{
case WM_DESTROY:
{
PostQuitMessage(0);
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 具体代码:
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 sockServer = (SOCKET)wparam;
服务器SOCKET中操作码的信息可以从回调函数中的lparam里面读取。lparam的低位保存的是操作码(LOWORD(lparam)),高位保存的是错误码(HIWORD(lparam))。 由于win32的窗口项目不能使用printf,这里我们可以创建HDC,然后用TextOut来显示信息。每次移动y轴坐标后显示信息。
case UM_ASYNCSELECTMSG:
{
SOCKET socketServer = (SOCKET)wparam;
if (HIWORD(lparam) == 0)
{
HDC hdc = GetDC(hWnd);
if (FD_READ == LOWORD(lparam))
{
TextOut(hdc,10,y,"FD_READ执行ING!",sizeof("FD_READ执行ING!")-1);
y+=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};
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))
{
TextOut(hdc,10,y,"FD_WRITE执行ING!",sizeof("FD_WRITE执行ING!")-1);
y+=16;
}
ReleaseDC(hWnd,hdc);
}
break;
}
非裸窗口的创建
第一步:创建窗口结构体:WNDCLASSEX(这一步不能少设置属性,否则会在第三步失败) 第二步:注册窗口结构体:RegisterClassEx
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);
return RegisterClassEx(&wcex);
第三步:创建窗口:CreateWindowEx 第四步:显示窗口:ShowWindow
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance;
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
第五步:消息循环:GetMessage、TranslateMessage、DispatchMessage
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
第六步:创建回调函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
非裸窗口的异步选择模型
1-5
SOCKET代码加在消息循环之前:
while (GetMessage(&msg, NULL, 0, 0))
和之前一样,异步选择模型服务器端的SOCKET代码套路的前面部分是一样的: 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、校验版本
if (2!=HIBYTE(wsaDATA.wVersion)|| 2!=LOBYTE(wsaDATA.wVersion))
{
printf("版本有问题!");
WSACleanup();
return 0;
}
4、创建SOCKET
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;
si.sin_port = htons(9527);
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
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;
case UM_ASYNCSELECTMSG:
{
SOCKET socketServer = (SOCKET)wparam;
if (HIWORD(lparam) == 0)
{
HDC hdc = GetDC(hWnd);
if (FD_READ == LOWORD(lparam))
{
TextOut(hdc,10,y,"FD_READ执行ING!",sizeof("FD_READ执行ING!")-1);
y+=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};
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))
{
TextOut(hdc,10,y,"FD_WRITE执行ING!",sizeof("FD_WRITE执行ING!")-1);
y+=16;
}
ReleaseDC(hWnd,hdc);
}
break;
}
非空项目附录
把VS2019自动生成非裸窗口项目的readme放上来: 项目名.vcxproj 这是使用应用程序向导生成的 VC++ 项目的主项目文件,其中包含生成该文件的 Visual C++ 的版本信息,以及有关使用应用程序向导选择的平台、配置和项目功能的信息。
项目名.vcxproj.filters 这是使用“应用程序向导”生成的 VC++ 项目筛选器文件。它包含有关项目文件与筛选器之间的关联信息。在 IDE 中,通过这种关联,在特定节点下以分组形式显示具有相似扩展名的文件。例如,“.cpp”文件与“源文件”筛选器关联。
主文件名.cpp 这是主应用程序源文件。
/ 应用程序向导创建了下列资源:
项目名.rc 这是程序使用的所有 Microsoft Windows 资源的列表。它包括 RES 子目录中存储的图标、位图和光标。此文件可以直接在 Microsoft Visual C++ 中进行编辑。
Resource.h 这是标准头文件,可用于定义新的资源 ID。Microsoft Visual C++ 将读取并更新此文件。
非空项目异步选择.ico 这是用作应用程序图标 (32x32) 的图标文件。此图标包括在主资源文件 非空项目异步选择.rc 中。
small.ico 这是一个图标文件,其中包含应用程序的图标的较小版本 (16x16)。此图标包括在主资源文件 非空项目异步选择.rc 中。
/ 其他标准文件:
StdAfx.h, StdAfx.cpp 这些文件用于生成名为 非空项目异步选择.pch 的预编译头 (PCH) 文件和名为 StdAfx.obj 的预编译类型文件。
/
MFC窗口的创建
选择哪个都行,不过第三个的代码会少一些,后面都用默认即可 运行效果: 代码很多,找到UDPMFCAsynSelectDlg.cpp 找到消息映射代码(消息映射有很多段)
BEGIN_MESSAGE_MAP(CUDPMFCAsynSelectDlg, CDialog)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_UM_SELECTMSG(UM_ASYNCSELECTMSG,&CUDPMFCAsynSelectDlg::OnMyMsg);
END_MESSAGE_MAP()
在这里加上自定义消息映射,里面内含两个参数,一个是消息ID,一个操作函数(这里是OnMyMsg)
#define UM_ASYNCSELECTMSG WM_USER+1
操作函数需要在头文件中添加操作函数声明:
public:
LRESULT OnMyMsg(WPARAM wParam, LPARAM lParam);
可以看到这个操作函数和回调函数差不多,就少了两个参数。
LRESULT CUDPMFCAsynSelectDlg::OnMyMsg(WPARAM wParam, LPARAM lParam)
{
}
然后往里面放异步选择模型
MFC的异步选择模型
1-5
将网络的初始化步骤放在OnInitDialog()中,当然也可以放构造函数里面 放提示语下面就好
1、包含网络头文件网络库 2、打开网络库 3、校验版本 4、创建SOCKET 5、绑定地址与端口
6、异步选择
6.1绑定消息和SOCKET
这块代码接着上面的代码放就好
if (WSAAsyncSelect(socketServer,m_hWnd,UM_ASYNCSELECTMSG,FD_READ|FD_WRITE)==SOCKET_ERROR)
{
int WSAAsyncSelecterr = WSAGetLastError();
printf("WSAAsyncSelect失败错误码为:%d\n",WSAAsyncSelecterr);
closesocket(socketServer);
WSACleanup();
return 0;
}
6.2 根据操作码进行处理
这块代码放在操作函数里面:
LRESULT CUDPMFCAsynSelectDlg::OnMyMsg(WPARAM wParam, LPARAM lParam)
{
if (HIWORD(lParam) == 0)
{
HDC hdc = ::GetDC(m_hWnd);
if (FD_READ == LOWORD(lParam))
{
TRACE("%s\n","FD_READ");
TextOut(hdc,10,y,"FD_READ执行ING!",sizeof("FD_READ执行ING!")-1);
y+=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};
TRACE("%d\n",err);
TextOut(hdc,10,y,_itoa(err,&strerr,10),sizeof(_itoa(err,&strerr,10))-1);
y+=16;
}
TRACE("%s\n",strRecvBuff);
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};
TRACE("%d\n",err);
TextOut(hdc,10,y,_itoa(err,&strerr,10),sizeof(_itoa(err,&strerr,10))-1);
y+=16;
}
}
if (FD_WRITE == LOWORD(lParam))
{
TRACE("%s\n", "FD_WRITE");
TextOut(hdc,10,y,"FD_WRITE执行ING!",sizeof("FD_WRITE执行ING!")-1);
y+=16;
}
::ReleaseDC(m_hWnd,hdc);
}
return 0;
}
这里有几个小地方要注意: 1、可以用TRACE来打印东西,用法和printf一样,但是只能在调试模式下看到; 2、TextOut没有去掉,因为更加直观一些,他的y坐标变量在头文件加了声明,在OnInitDialog()赋初始值; 3、服务器SOCKET句柄可以从参数wParam里面取也可以直接用全局变量里面的句柄。
释放句柄
这个最好在析构函数里面做: 头文件加析构函数定义后,主文件加:
CUDPMFCAsynSelectDlg::~CUDPMFCAsynSelectDlg()
{
closesocket(socketServer);
WSACleanup();
}
|