异步选择模型
异步选择(WSAAsyncSelect)模型是一个有用的异步 I/O 模型。利用这个模型,应用程序可在一个套接字上,接收以 Windows 消息为基础的网络事件通知。具体
的做法是在建好一个套接字后,调用WSAAsyncSelect函数。该模型的核心即是WSAAsyncSelect函数。
WSAAsyncSelect函数定义如下:
int WSAAsyncSelect(
__in SOCKET s, //指定的是我们感兴趣的那个套接字。
__in HWND hWnd, //指定一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口。
__in unsigned int wMsg, //指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。
__in long lEvent //指定一个位掩码,对应于一系列网络事件的组合
);
注意:
1、wMsg参数指定的消息通常是我们自定义的消息,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与系统预定义的标准窗
口消息发生混淆与冲突。
2、lEvent参数指定的网络类型为:FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等。当然,到底使用FD_ACCEPT,还是使用
FD_CONNECT类型,要取决于应用程序的身份是客户端,还是服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算就OK。
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知
3、多个事件务必在套接字上一次注册!另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,或者由应用程序针对那
个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有
网络事件通知。
4、若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“锁定”变成“非锁定”。这样一来,如果调用了像WSARecv这样的Winsock函数,
但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的
用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。
应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。窗口例程通常
定义如下:
LRESULT CALLBACK WindowProc(
HWND hwnd, //指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。
UINT uMsg, //指定需要对哪些消息进行处理。这里我们感兴趣的是WSAAsyncSelect调用中定义的消息。
WPARAM wParam, //指定在其上面发生了一个网络事件的套接字。(假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。)
LPARAM lParam //包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何
//错误代码。
);
流程:网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在网络错误。这里有一个特殊的宏: WSAGETSELECTERROR,可用
它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,具体的做法便是读取lParam低字位的内容。此时可
使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。
FD_WRITE 事件通知:只有在三种条件下,才会发出 FD_WRITE 通知:
1、使用 connect 或 WSAConnect,一个套接字首次建立了连接。
2、使用 accept 或 WSAAccept,套接字被接受以后。
3、若 send、WSASend、sendto 或 WSASendTo 操作失败,返回了 WSAEWOULDBLOCK 错误,而且缓冲区的空间变得可用。
下面,我们来看看具体的演示,客户端是用select模型实现的,服务器端是用WSAAsyncSelect实现的,两者界面都相当的简洁
服务器界面如下:
客户端界面如下:
其中,多个客户端可以同时与服务器端实现了,由于使用WSAAsyncSelect模型及我只起一个工作线程的缘故,故最多只能接受64个客户端。
理论知识与演示完之后,我们来看看服务器端具体的代码实现
1、当然是初始化socket了,没什么好说的
WSAData data; int error; error = WSAStartup(MAKEWORD(2, 2), &data); if (0 != error) { return FALSE; } if(HIBYTE(data.wVersion) != 2 && LOBYTE(data.wVersion))
{
WSACleanup();
return FALSE;
}
return TRUE;
2、定义一个自定义消息,前面理论知识已讲过就不再重复了
#define WM_SOCKET WM_USER + 0x01
3、开启一个工作线程,初始化socket,开始监听。其实,这个线程可以封装成一个函数,具体看代码就知道了,封装成线程只是个人习惯哈。
UINT ThreadProc(LPVOID lpParameter) { CWSAAsyncSelectDlg *pDlg = (CWSAAsyncSelectDlg*)lpParameter; ASSERT(pDlg != NULL); pDlg->m_listenSocket = socket(AF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == pDlg->m_listenSocket) { return FALSE; } char ipbuf[1024] = {0}; wcstombs(ipbuf, pDlg->GetIpAddress(), pDlg->GetIpAddress().GetLength()); const char *p = ipbuf; sockaddr_in serverAddress; serverAddress.sin_addr.S_un.S_addr = inet_addr(p); serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(pDlg->m_iPort); if (SOCKET_ERROR == bind(pDlg->m_listenSocket, (sockaddr*)&serverAddress, sizeof(sockaddr_in))) { return FALSE; } WSAAsyncSelect(pDlg->m_listenSocket, pDlg->GetSafeHwnd(), WM_SOCKET, FD_ACCEPT | FD_CLOSE); listen(pDlg->m_listenSocket, SOMAXCONN); pDlg->ShowText(_T("系统消息:服务器开始监听。。。")); return TRUE; }
4、WSAAsyncSelect模型的核心就是这个窗口过程,但是格式也是一样一样的。。。
LRESULT CWSAAsyncSelectDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { // TODO: 在此添加专用代码和/或调用基类 TCHAR buf[1024] = {0}; CString cstrMsg; CString cstrIP; szClientItem clientItem; sockaddr_in clientAddr = {0}; int iLength = sizeof(sockaddr_in); switch (message) { case WM_SOCKET: if (WSAGETASYNCERROR(lParam)) { vector<szClientItem>::iterator iter = m_ClientSockets.begin(); for (vector<szClientItem>::size_type i = 0; i < m_ClientSockets.size(); i++) { if (m_ClientSockets.at(i).m_socket == (SOCKET)wParam) { ShowText(_T("系统消息: ") + m_ClientSockets.at(i).m_cstrIP + _T("客户端已经关闭。")); closesocket(wParam); m_ClientSockets.erase(iter + i); break; } } } switch(WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: clientItem.m_socket = accept(wParam, (sockaddr*)&clientAddr, &iLength); cstrIP.Format(_T("%d.%d.%d.%d"),clientAddr.sin_addr.S_un.S_un_b.s_b1,clientAddr.sin_addr.S_un.S_un_b.s_b2,clientAddr.sin_addr.S_un.S_un_b.s_b3,clientAddr.sin_addr.S_un.S_un_b.s_b4); clientItem.m_cstrIP = cstrIP; WSAAsyncSelect(clientItem.m_socket, m_hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE); m_ClientSockets.push_back(clientItem); ShowText(_T("系统消息: 客户端 ")+ cstrIP + _T(" 已经连接成功")); break; case FD_READ: recv(wParam, (char *)buf, 1024, 0); cstrMsg = buf; for (vector<szClientItem>::size_type i = 0; i < m_ClientSockets.size(); i++) { if (m_ClientSockets.at(i).m_socket == (SOCKET)wParam) { ShowText(_T("Client: ") + m_ClientSockets.at(i).m_cstrIP + _T(">") + cstrMsg); break; } } break; case FD_WRITE: break; case FD_CLOSE:
break; } default:break; } return CDialogEx::WindowProc(message, wParam, lParam); }
具体核心代码我想应该就是这些了,这个模型相当简单,操作无非是这几步:
1、定义一个自定义消息
2、WSAAsyncSelect函数关联自定义消息并选择感兴趣的网络事件
3、定义一个窗口过程,WSAAsyncSelect函数绑定到这个窗口过程
4、在窗口过程捕获感兴趣的网络事件并做相应的处理。
是不是很简单。。。。