1. Window下网络I/O模型
Window系统提供了另外一些网络模型,除了兼容Linux上的阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型(只兼容select)外,还包括了WSAAsyncSelect模型、WSAEventSelect模型、Overlapped I/O 事件通知模型、Overlapped I/O 完成例程模型、IOCP模型。对于与Linux类似的网络模型,就不在这里叙述了。只讨论一些其特有的形式。1.1 WSAAsyncSelect模型
Windows上最有特色的就是其消息机制了,所有的消息传送都依赖于窗口。通过消息机制,Windows做到了用单线程来同时处理UI和I/O数据。本质上,Windows的消息机制是一个轮询机制,而用户向窗口注册消息并关联对应的消息函数的过程可以理解成一个向Window系统设置回调函数的过程。当Windows收到某个消息,便会调用相应的用户处理函数。WSAAsyncSelect函数就是在这个背景下的一个产物,它把Socket和窗口关联了起来,非常适合于Windows编程使用。WSAAsyncSelect函数原型如下:
- int WSAAsyncSelect(
- SOCKET s, //标识一个需要事件通知的套接口描述符
- HWND hWnd, //标识一个在网络事件发生时要想收到消息的窗口或对话框的句柄
- u_int wMsg, //在网络事件发生时要接收的消息,该消息会投递到由hWnd句柄指定的窗口或对话框
- long lEvent //位屏蔽码,用于指明应用程序感兴趣的网络事件集合
- );
当应用程序对一个套接口s调用了WSAAsyncSelect()函数时,那么套接口s的模式会自动从阻塞模式变成非阻塞模式。
如果应用程序同时对多个网络事件感兴趣,那么只需对各种类型的网络事件执行按位或(OR)的运算即可。可用的网络事件类型如下:
FD_READ 欲接收读准备好的通知.
FD_WRITE 欲接收写准备好的通知.
FD_OOB 欲接收带边数据到达的通知.
FD_ACCEPT 欲接收将要连接的通知.
FD_CONNECT 欲接收已连接好的通知.
FD_CLOSE 欲接收套接口关闭的通知.
调用WSAAsyncSelect()需要注意到是,进行一次WSAAsyncSelect()调用,将使为同一个套接口启动的所有以前的WSAAsyncSelect()调用作废。如果要取消所有的通知,也就是指出Windows Sockets的实现不再在套接口上发送任何和网络事件相关的消息,则把lEvent字段置为0,然后调用WSAAsyncSelect()。
当某一套接口s上发生了一个已命名的网络事件时,应用程序窗口hWnd会接收到消息wMsg。其中应用程序窗口例程的wParam参数标识了网络事件发生的套接口。lParam参数的低位字指明了发生的网络事件,用户可以通过宏WSAGETSELECTEVENT(lParam)来获取。而高位字则含有一个错误代码,错误代码可以是Winsock2.h中定义的任何错误,同样用户可以通过另外一个宏WSAGETSELECTERROR(lParam)来获取。其定义如下:
- #define WSAGETSELECTERROR(lParam) HIWORD(lParam)
- #define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
WSAAsyncSelect使用过程大致如下(以服务端Win32下为例):
- LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
- {
- switch(uMsg)
- {
- case WM_USER_SOCKET:
- {
- SOCKET sock=wParam; //wParam参数标志了网络事件发生的套接口
- if (WSAGETSELECTERROR(lParam))
- {
- closesocket(sock);
- return 0;
- }
- switch (WSAGETSELECTEVENT(lParam))
- {
- case FD_ACCEPT: //连接请求到来
- {
- sockaddr_in add;
- int len=sizeof(add);
- SOCKET sNew=accept(sock,(sockaddr*)&add,&len);
- WSAAsyncSelect(sNew,hwnd,WM_USER_SOCKET,FD_READ|FD_CLOSE);
- }
- break;
- case FD_READ: //数据发送来
- {
- recv(...);
- ...
- send(...);
- }
- break;
- case FD_CLOSE: //关闭连接
- {
- closesocket(sock);
- }
- break;
- }
- }
- break;
- case WM_CLOSE:
- DestroyWindow(hwnd);
- break;
- case WM_DESTROY:
- PostQuitMessage(0);
- break;
- default:
- return DefWindowProc(hwnd,uMsg,wParam,lParam);
- }
- return 0;
- }
- int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
- {
- // 初始化网络库
- WSADATA wsaData;
- WORD wVersionRequested=MAKEWORD(2,2);
- WSAStartup(wVersionRequested,&wsaData);
- // 创建窗口
- HWND hwnd=CreateWindow(...);
- // 创建Socket
- SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
- // 绑定Socket
- sockaddr_in sin;
- sin.sin_family=AF_INET;
- sin.sin_port=htons(2148);
- sin.sin_addr.S_un.S_addr=inet_addr("192.168.1.1");
- bind(s,(sockaddr*)&sin,sizeof(sin));
- // 监听
- listen(s,1);
- // 关联窗口和Socket消息
- WSAAsyncSelect(s,hwnd,WM_USER_SOCKET,FD_ACCEPT|FD_CLOSE);
- //---消息循环----
- MSG msg;
- while (GetMessage(&msg,0,0,0))
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- // 关闭Socket
- closesocket(s);
- // 库饭初始化
- WSACleanup();
- }
WSAAsyncSelect模式非常适应windows消息驱动环境,但有一些局限,第一在没有窗口的情况下无法使用,第二通过窗口的消息轮询去处理I/O数据的效率不高。
1.2 WSAEventSelec模型
WSAEventSelect()函数解决了WSAAsyncSelect()函数的第一个问题,在没有窗口的情况下,也可以使用。事实上这就是这两个函数的区别,当一个FD_XXX网络事件发生时,WSAEventSelect()函数将导致应用程序指定的一个事件对象将被设置,即将网络事件投递到一个事件对象句柄,而WSAAsyncSelect()函数则将网络事件(消息)投递至一个窗口句柄上。WSAEventSelec模型涉及以下函数:- // 创建事件
- WSAEVENT WSAAPI WSACreateEvent(
- VOID
- );
同Windows核心对象一样,WSACreateEvent创建的事件也有激发状态和非激发状态。激发状态和非激发状态之间的改变也存在手动模式和自动模式。手动模式下两种状态的改变分别由WSAResetEvent和WSASetEvent完成。二者定义如下:
将指定的事件对象状态重新设置为未置信号
- BOOL WSAAPI WSAResetEvent(
- WSAEVENT hEvent //事件句柄
- );
将指定的事件对象状态设置为有信号
- BOOL WSAAPI WSASetEvent(
- WSAEVENT hEvent //事件句柄
- );
销毁事件
- BOOL WSAAPI WSACloseEvent(
- WSAEVENT hEvent // hEvent:标识一个开放的事件对象句柄。
- );
事件绑定
- int WSAEventSelect(
- SOCKET s, //一个标识套接口的描述字
- WSAEVENT hEventObject, //是一个由WSACreateEvent(?)函数创建的事件对象句柄,用于标识与所提供的FD_XXX网络事件集合相关的一个事件对象
- long lNetworkEvents //指定应用程序感兴趣的各种网络事件(FD_XXX)的组合
- );
其中参数 lNetworkEvents可以用以下数值进行OR操作
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知
等待套接口上网络事件的发生
- DWORD WSAWaitForMultipleEvents(
- DWORD cEvents, //指定下面lpEvents所指的数组中事件对象句柄的个数,事件对象句柄的最大值为WSA_MAXIMUM_WAIT_EVENTS。
- const WSAEVENT* lphEvents, //指向一个事件对象句柄的数组
- BOOL fWaitAll, //指定是否等待所有的事件对象都变成受信状态(为TRUE:是;FALSE:否)
- DWORD dwTimeout, //指定要等待的时间,可以为WSA_INFINITE
- BOOL fAlertable //指定当系统将一个输入/输出完成例程放入队列以供执行时,函数是否返回。
- //若为真TRUE,则函数返回且执行完成例程。若为假FALSE,函数不返回,不执行完成例程。
- );
检测所指定的套接口上网络事件的发生
- int WSAEnumNetworkEvents(
- SOCKET s, //标识套接口的描述字。
- WSAEVENT hEventObject, //hEventObject 参数则是可选的; 它指定了一个事件句柄,对应于打算重设的那个事件对象,令其自动重置为非激发状态
- //如果不想用 hEventObject 参数来重设事件,那么可使用 WSAResetEvent函数手动重置
- LPWSANETWORKEVENTS lpNetworkEvents, //代表一个指针,指向 WSANETWORKEVENTS 结构,用于接收套接字上发生的网络事件类型以及可能出现的任何错误代码。
- LPINT lpiCount //数组中的元素数目。在返回时,本参数表示数组中的实际元素数目;
- //如果返回值是WSAENOBUFS,则表示为获取所有网络事件所需的元素数目。
- );
LPWSANETWORKEVENTS结构体定义如下:
- typedef struct _WSANETWORKEVENTS {
- long lNetworkEvents; //指定了一个值,对应于套接字上发生的所有网络事件类型(FD_READ、FD_WRITE 等)
- int iErrorCode[FD_MAX_EVENTS]; //参数指定的是一个错误代码数组,同 lNetworkEvents 中的事件关联在一起.
- //针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的名字类似,需要在事件名字后面添加一个"_BIT"
- } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
WSAEventSelec的使用如下(同样以服务器为例):
- int main()
- {
- //初始化库
- WSADATA wsaData;
- WSAStartup(MAKEWORD(2,2),&wsaData);
- //创建套接字
- SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
- //设置套接口s"非阻塞模式"
- u_long u1=1;
- ioctlsocket(s,FIONBIO,(u_long*)&u1);
- //绑定本地地址
- struct sockaddr_in Sadd;
- Sadd.sin_family=AF_INET;
- Sadd.sin_port=htons(1928);
- Sadd.sin_addr.S_un.S_addr=inet_addr("192.168.1.1");
- bind(s,(sockaddr*)&Sadd,sizeof(Sadd));
- //监听
- listen(s,5);
- //创建事件对象
- WSAEVENT NewEvent=WSACreateEvent();
- //网络事件注册
- WSAEventSelect(s,NewEvent,FD_ACCEPT|FD_CLOSE);
- //准备工作
- int t=1;
- WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
- SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS];
- int n=0;
- eventArray[n]=NewEvent;
- sockArray[n]=s;
- n++;
- //循环处理
- while (true)
- {
- //等待事件对象
- int nIndex=WSAWaitForMultipleEvents(n,eventArray,FALSE,40000,FALSE);
- if (nIndex==WSA_WAIT_FAILED)
- {
- //调用失败
- break;
- }
- else if (nIndex==WSA_WAIT_TIMEOUT)
- {
- //超时
- break;
- }
- else
- {
- //网络事件触发事件对象句柄的工作状态
- WSANETWORKEVENTS event;//该结构记录网络事件和对应出错代码
- //网络事件查询
- WSAEnumNetworkEvents(sockArray[nIndex-WSA_WAIT_EVENT_0],NULL,&event);
- WSAResetEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]);
- if ((event.lNetworkEvents&FD_ACCEPT)!=0)
- {
- //处理FD_ACCEPT通知消息
- if (event.iErrorCode[FD_ACCEPT_BIT]==0)
- {
- if (n>WSA_MAXIMUM_WAIT_EVENTS)
- {
- //连接超上限
- break;
- }
- SOCKET sNew=accept(sockArray[nIndex-WSA_WAIT_EVENT_0],NULL,NULL);
- NewEvent=WSACreateEvent();
- WSAEventSelect(sNew,NewEvent,FD_READ|FD_CLOSE);
- eventArray[n]=NewEvent;
- sockArray[n]=sNew;
- n++;
- }
- }
- else if (event.lNetworkEvents&FD_READ)
- {
- //处理FD_READ通知消息
- if (event.iErrorCode[FD_READ_BIT]==0)
- {
- char buf[256];
- memset(buf,0,256);
- int nRecv=recv(sockArray[nIndex-WSA_WAIT_EVENT_0],buf,sizeof(buf),0);
- }
- }
- else if (event.lNetworkEvents&FD_CLOSE)
- {
- //处理FD_CLOSE通知消息
- if (event.iErrorCode[FD_CLOSE_BIT]==0)
- {
- closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]);
- WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]);
- }
- else
- {
- if (event.iErrorCode[FD_CLOSE_BIT]==10053)
- {
- closesocket(sockArray[nIndex-WSA_WAIT_EVENT_0]);
- WSACloseEvent(eventArray[nIndex-WSA_WAIT_EVENT_0]);
- }
- }
- for (int j=nIndex-WSA_WAIT_EVENT_0;j<n-1;j++)
- {
- sockArray[j]=sockArray[j+1];
- eventArray[j]=eventArray[j+1];
- }
- n--;
- }
- }
- }
- //关闭socket
- closesocket(s);
- //卸载库
- WSACleanup();
- return 0;
- }
流程如下:
1、定义一个socket数组和event数组
2、调用WSAEventSelect为每个socket操作关联一个event对象
3、调用WSAWaitForMultipleEvents函数等待事件的触发
4、调用WSAEnumNetworkEvents函数查看是哪个一个事件,根据事件找到相应的socket,然后进行相应的处理:需要将event重置为无信号状态。
5、循环步骤3和4,直到服务器退出。
WSAEventSelec模型可以在没有窗口的条件下使用,但受到WSAWaitForMultipleEvents限制,只能接收64个socket的限制。如果和多线程配合使用,能够使接收数目增加,但由于核心对象的增加,性能会下降,故不推荐在服务器端使用。