Winsock共有五种类型的套接字I/O模型,可让Winsock应用程序对I/O进行管理,它们包括: select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)、以及completion port(完成端口)。
1、Select(选择)模型
利用select函数,判断套接字上是否存在数据,或者能否向一个套接字写入数据。目的是防止应用程序在套接字处于锁定模式时,调用recv(或send)从没有数据的套接字上接收数据,*进入阻塞状态。
select参数和返回值意义如下:
int select (
IN int nfds, //0,无意义
IN OUT fd_set* readfds, //检查可读性
IN OUT fd_set* writefds, //检查可写性
IN OUT fd_set* exceptfds, //例外数据
IN const struct timeval* timeout); //函数的返回时间,指针NULL一直等待
select返回fd_set中可用的套接字个数。
fd_set是一个SOCKET队列,以下宏可以对该队列进行操作:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.
Select工作流程
1:用FD_ZERO宏来初始化我们感兴趣的fd_set。
也就是select函数的第二三四个参数。
2:用FD_SET宏来将套接字句柄分配给相应的fd_set。
如果想要检查一个套接字是否有数据需要接收,可以用FD_SET宏把套接接字句柄加入可读性检查队列中
3:调用select函数。
如果该套接字没有数据需要接收,select函数会把该套接字从可读性检查队列中删除掉,
4:用FD_ISSET对套接字句柄进行检查。
如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操 作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。
#include <winsock2.h> #include <stdio.h> #define PORT 5150 #define MSGSIZE 1024 #pragma comment(lib, "ws2_32.lib") int g_iTotalConn1 = 0; int g_iTotalConn2 = 0; SOCKET g_CliSocketArr1[FD_SETSIZE]; SOCKET g_CliSocketArr2[FD_SETSIZE]; DWORD WINAPI WorkerThread1(LPVOID lpParam); int CALLBACK ConditionFunc1(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData); DWORD WINAPI WorkerThread2(LPVOID lpParam); int CALLBACK ConditionFunc2(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData); int main(int argc, char* argv[]) { WSADATA wsaData; SOCKET sListen, sClient; SOCKADDR_IN local, client; int iAddrSize = sizeof(SOCKADDR_IN); DWORD dwThreadId; // Initialize windows socket library WSAStartup(0x0202, &wsaData); // Create listening socket sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // Bind local.sin_family = AF_INET; local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_port = htons(PORT); bind(sListen, (sockaddr*)&local, sizeof(SOCKADDR_IN)); // Listen listen(sListen, 3); // Create worker thread CreateThread(NULL, 0, WorkerThread1, NULL, 0, &dwThreadId); // CreateThread(NULL, 0, WorkerThread2, NULL, 0, &dwThreadId); while (TRUE) { sClient = WSAAccept(sListen, (sockaddr*)&client, &iAddrSize, ConditionFunc1, 0); printf("1:Accepted client:%s:%d:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), g_iTotalConn1); g_CliSocketArr1[g_iTotalConn1++] = sClient; /* sClient = WSAAccept(sListen, (sockaddr*)&client, &iAddrSize, ConditionFunc2, 0); printf("2:Accepted client:%s:%d:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), g_iTotalConn2); g_CliSocketArr2[g_iTotalConn2++] = sClient; */ } return 0; } DWORD WINAPI WorkerThread1(LPVOID lpParam) { int i; fd_set fdread; int ret; struct timeval tv = {1, 0}; char szMessage[MSGSIZE]; while (TRUE) { FD_ZERO(&fdread); //1清空队列 for (i = 0; i < g_iTotalConn1; i++) { FD_SET(g_CliSocketArr1[i], &fdread); //2将要检查的套接口加入队列 } // We only care read event ret = select(0, &fdread, NULL, NULL, &tv); //3查询满足要求的套接字,不满足要求,出队 if (ret == 0) { // Time expired continue; } for (i = 0; i < g_iTotalConn1; i++) { if (FD_ISSET(g_CliSocketArr1[i], &fdread)) //4.是否依然在队列 { // A read event happened on g_CliSocketArr ret = recv(g_CliSocketArr1[i], szMessage, MSGSIZE, 0); if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // Client socket closed printf("1:Client socket %d closed.\n", g_CliSocketArr1[i]); closesocket(g_CliSocketArr1[i]); if (i < g_iTotalConn1-1) { g_CliSocketArr1[i--] = g_CliSocketArr1[--g_iTotalConn1]; } } else { // We reveived a message from client szMessage[ret] = '\0'; send(g_CliSocketArr1[i], szMessage, strlen(szMessage), 0); } } } } } int CALLBACK ConditionFunc1(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData) { if (g_iTotalConn1 < FD_SETSIZE) return CF_ACCEPT; else return CF_REJECT; } DWORD WINAPI WorkerThread2(LPVOID lpParam) { int i; fd_set fdread; int ret; struct timeval tv = {1, 0}; char szMessage[MSGSIZE]; while (TRUE) { FD_ZERO(&fdread); //1清空队列 for (i = 0; i < g_iTotalConn2; i++) { FD_SET(g_CliSocketArr2[i], &fdread); //2将要检查的套接口加入队列 } // We only care read event ret = select(0, &fdread, NULL, NULL, &tv); //3查询满足要求的套接字,不满足要求,出队 if (ret == 0) { // Time expired continue; } for (i = 0; i < g_iTotalConn2; i++) { if (FD_ISSET(g_CliSocketArr2[i], &fdread)) //4.是否依然在队列 { // A read event happened on g_CliSocketArr ret = recv(g_CliSocketArr2[i], szMessage, MSGSIZE, 0); if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // Client socket closed printf("2:Client socket %d closed.\n", g_CliSocketArr1[i]); closesocket(g_CliSocketArr2[i]); if (i < g_iTotalConn2-1) { g_CliSocketArr2[i--] = g_CliSocketArr2[--g_iTotalConn2]; } } else { // We reveived a message from client szMessage[ret] = '\0'; send(g_CliSocketArr2[i], szMessage, strlen(szMessage), 0); } } } } } int CALLBACK ConditionFunc2(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData) { if (g_iTotalConn2 < FD_SETSIZE) return CF_ACCEPT; else return CF_REJECT; }
服务器的几个主要动作如下:
1.创建监听套接字,绑定,监听;
2.创建工作者线程;
3.创建一个套接字数组,用来存放当前所有活动的客户端套接字,每accept一个连接就更新一次数组;
4.接受客户端的连接。
这里有一点需要注意的,就是我没有重新定义FD_SETSIZE宏,所以服务器最多支持的并发连接数为64。而且,这里决不能无条件的accept,服务器应该根据当前的连接数来决定是否接受来自某个客户端的连接。
工作者线程里面是一个死循环,一次循环完成的动作是:
1.将当前所有的客户端套接字加入到读集fdread中;
2.调用select函数;
3.查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)
除了需要有条件接受客户端的连接外,还需要在连接数为0的情形下做特殊处理,因为如果读集中没有任何套接字,select函数会立刻返回,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%。
关系到套接字列表的操作都需要使用循环,在轮询的时候,需要遍历一次,再新的一轮开始时,将列表加入队列又需要遍历一次.也就是说,Select在工作一次时,需要至少遍历2次列表,这是它效率较低的原因之一.在大规模的网络连接方面,还是推荐使用IOCP或EPOLL模型.但是Select模型可以使用在诸如对战类游戏上,比如类似星际这种,因为它小巧易于实现,而且对战类游戏的网络连接量并不大.
对于Select模型想要突破Windows 64个限制的话,可以采取分段轮询,一次轮询64个.例如套接字列表为128个,在第一次轮询时,将前64个放入队列中用Select进行状态查询,待本次操作全部结束后.将后64个再加入轮询队列中进行轮询处理.这样处理需要在非阻塞式下工作.以此类推,Select也能支持无限多个.
注意:
1.那个最大的连接数是指每一个线程可以处理的连接数,当你有多个线程时,连接数是可以无限增长的,不过此时的效率就比较低。
2.关于发送操作writefds的问题,当套接字成功连接或者一个套接字刚刚成功接收信息时都会调用。
3.我们通常会创建一个套接字来进行监听,之后用accept返回的套接字进行通信。这里要注意一点,用于监听的套接字在没有新连接时也会进行writefds的操作。
2、WSAAsyncSelect模型
WSAAsyncSelect模型允许应用程序以Windows消息的方式接收网络事件通知。许多对性能要求不高的网络应用程序都采用WSAAsyncSelect模型,MFC的CSocket类也使用了它。
WSAAsyncSelect自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句柄,当有网络事件发生时,便向这个窗口发送消息。
#include <winsock2.h> #include <tchar.h> #define PORT 5150 #define MSGSIZE 1024 #define WM_SOCKET WM_USER+0 #pragma comment(lib, "ws2_32.lib") LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = _T("AsyncSelect Model"); HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass(&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, // window class name TEXT ("AsyncSelect Model"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL) ; // creation parameters ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) //获取消息 { TranslateMessage(&msg) ; //转化键盘信息 DispatchMessage(&msg) ; //将信息发送到指定窗体 } return msg.wParam; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { WSADATA wsd; static SOCKET sListen; SOCKET sClient; SOCKADDR_IN local, client; int ret, iAddrSize = sizeof(client); char szMessage[MSGSIZE]; switch (message) { case WM_CREATE: // Initialize Windows Socket library WSAStartup(0x0202, &wsd); // Create listening socket sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // Bind local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_family = AF_INET; local.sin_port = htons(PORT); bind(sListen, (struct sockaddr *)&local, sizeof(local)); // Listen listen(sListen, 3); // Associate listening socket with FD_ACCEPT event WSAAsyncSelect(sListen, hwnd, WM_SOCKET, FD_ACCEPT); return 0; case WM_DESTROY: closesocket(sListen); WSACleanup(); PostQuitMessage(0); return 0; case WM_SOCKET: if (WSAGETSELECTERROR(lParam)) { closesocket(wParam); break; } switch (WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: // Accept a connection from client sClient = accept(wParam, (struct sockaddr *)&client, &iAddrSize); // Associate client socket with FD_READ and FD_CLOSE event WSAAsyncSelect(sClient, hwnd, WM_SOCKET, FD_READ | FD_CLOSE); break; case FD_READ: ret = recv(wParam, szMessage, MSGSIZE, 0); if (ret == 0 || ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET) { closesocket(wParam); } else { szMessage[ret] = '\0'; send(wParam, szMessage, strlen(szMessage), 0); } break; case FD_CLOSE: closesocket(wParam); break; } return 0; } //把我们不处理的信息交给系统作默认处理 return DefWindowProc(hwnd, message, wParam, lParam); }
在我看来,WSAAsyncSelect是最简单的一种WinsockI/O模型(之所以说它简单是因为一个主线程就搞定了)。使用Raw Windows API写过窗口类应用程序的人应该都能看得懂。这里,我们需要做的仅仅是:
1.在WM_CREATE消息处理函数中,初始化WindowsSocket library,创建监听套接字,绑定,监听,并且调用WSAAsyncSelect函数表示我们关心在监听套接字上发生的FD_ACCEPT事件;
2.自定义一个消息WM_SOCKET,一旦在我们所关心的套接字(监听套接字和客户端套接字)上发生了某个事件,系统就会调用WndProc并且message参数被设置为WM_SOCKET;
3.在WM_SOCKET的消息处理函数中,分别对FD_ACCEPT、FD_READ和FD_CLOSE事件进行处理;
4.在窗口销毁消息(WM_DESTROY)的处理函数中,我们关闭监听套接字,清除WindowsSocket library
下面这张用于WSAAsyncSelect函数的网络事件类型表可以让你对各个网络事件有更清楚的认识:
表1
FD_READ |
应用程序想要接收有关是否可读的通知,以便读入数据 |
FD_WRITE |
应用程序想要接收有关是否可写的通知,以便写入数据 |
FD_OOB |
应用程序想接收是否有带外(OOB)数据抵达的通知 |
FD_ACCEPT |
应用程序想接收与进入连接有关的通知 |
FD_CONNECT |
应用程序想接收与一次连接或者多点join操作完成的通知 |
FD_CLOSE |
应用程序想接收与套接字关闭有关的通知 |
FD_QOS |
应用程序想接收套接字“服务质量”(QoS)发生更改的通知 |
FD_GROUP_QOS |
应用程序想接收套接字组“服务质量”发生更改的通知(现在没什么用处,为未来套接字组的使用保留) |
FD_ROUTING_INTERFACE_CHANGE |
应用程序想接收在指定的方向上,与路由接口发生变化的通知 |
FD_ADDRESS_LIST_CHANGE |
应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知 |
3、WSAEventSelect模型
WSAEventSelect模型类似WSAAsynSelect模型,但最主要的区别是网络事件发生时会被发送到一个事件对象句柄,而不是发送到一个窗口。这样可能更加的好,对于服务器端的程序来说。
使用步骤如下:
a、创建事件对象来接收网络事件:
WSAEVENT WSACreateEvent( void );
该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信(signaled)和未传信(nonsignaled)以及两种工作模式:人工重设(manualreset)和自动重设(autoreset)。
默认未未传信的工作状态和人工重设模式。
b、将事件对象与套接字关联,同时注册事件,使事件对象的工作状态从未传信转变未已传信。
int WSAEventSelect( SOCKET s,WSAEVENT hEventObject,long lNetworkEvents );
参数介绍:
s为套接字
hEventObject为刚才创建的事件对象句柄
lNetworkEvents为掩码,定义如上面所述
c、I/O处理后,设置事件对象为未传信
BOOL WSAResetEvent( WSAEVENT hEvent );
hEvent为事件对象
成功返回TRUE,失败返回FALSE。
d、等待网络事件来触发事件句柄的工作状态:
DWORD WSAWaitForMultipleEvents( DWORD cEvents,const WSAEVENT FAR * lphEvents,BOOL fWaitAll,DWORD dwTimeout, BOOL fAlertable );
参数介绍:
lpEvent为事件句柄数组的指针
cEvent为为事件句柄的数目,其最大值为WSA_MAXIMUM_WAIT_EVENTS
fWaitAll指定等待类型:TRUE:当lphEvent数组重所有事件对象同时有信号时返回;
FALSE:任一事件有信号就返回。
dwTimeout为等待超时(毫秒)
fAlertable为指定函数返回时是否执行完成例程
nIndex=WSAWaitForMultipleEvents(…);
MyEvent=EventArray[Index- WSA_WAIT_EVENT_0];
e.枚举事件的类型:
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);
参数介绍:
s为有事件发生的Socket
hEventObject为与s相关联的事件句柄
lpNetworkEvents是一个结构体:
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
if(lpNetWorkEvents.lNetworkEvents&FD_READ)
{
if(lpNetWorkEvents[FD_READ_BIT]!=0)
//发送错误
else
//有数据可以接收
}
事件选择模型也比较简单,实现起来也不是太复杂,它的基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网络事件。一旦在某个套接字上
发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。程序定义了两个全局数组,一个套接字数组,一个WSAEVENT对象数组,其大小都是
MAXIMUM_WAIT_OBJECTS(64),两个数组中的元素一一对应。
同样的,这里的程序没有考虑两个问题:
一是不能无条件的调用accept,因为我们支持的并发连接数有限。解决方法是将套接字按MAXIMUM_WAIT_OBJECTS分组,每MAXIMUM_WAIT_OBJECTS个套接字一组,
每一组分配一个工作者线程;或者采用WSAAccept代替accept,并回调自己定义的ConditionFunction。
第二个问题是没有对连接数为0的情形做特殊处理,程序在连接数为0的时候CPU占用率为100%。
0连接时,g_iTotalConn= 0;
也即
WSAWaitForMultipleEvents(0, g_CliEventArr, FALSE, 1000, FALSE);
应该是一直循环ret ==WSA_WAIT_FAILED
WSAGetLastError()=WSA_INVALID_PARAMETER
SOCKET Socket[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT Event[WSA_MAXINUM_WAIT_EVENTS]; SOCKET Accept, Listen; DWORD EventTotal = 0; DWORD Index; //Set up a TCP socket for listening on port 5150 Listen = socket(PF_INET,SOCK_STREAM,0); InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(5150); bind(Listen,(PSOCKADDR) &InternetAddr,sizeof(InternetAddr)); NewEvent = WSACreateEvent(); WSAEventSelect(Listen,NewEvnet,FD_ACCEPT|FD_CLOSE); listen(Listen,5); Socket[EventTotal] = Listen; Event[EventTotal] = NewEvent; EventTotal++; while (TRUE) { //Wait for network events on all sockets Index = WSAWaitForMultipleEvents(EventTotal,EventArray,FALSE,WSA_INFINITE,FALSE); WSAEnumNewWorkEvents(SocketArray[Index-WSA_WAIT_EVENT_0], EventArray[Index-WSA_WAIT_EVENT_0], &NetworkEvents); //Check for FD_ACCEPT messages if (NetworkEvents.lNetworkEvents & FD_ACCEPT) { if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] !=0) { //Error break; } //Accept a new connection and add it to the socket and event lists Accept = accept(SocketArray[Index-WSA_WAIT_EVENT_0],NULL,NULL); //We cannot process more than WSA_MAXIMUM_WAIT_EVENTS sockets , //so close the accepted socket if (EventTotal > WSA_MAXIMUM_WAIT_EVENTS) { printf(".."); closesocket (Accept); break; } NewEvent = WSACreateEvent(); WSAEventSelect(Accept,NewEvent,FD_READ|FD_WRITE|FD_CLOSE); Event[EventTotal] = NewEvent; Socket[EventTotal]= Accept; EventTotal++; prinrt("Socket %d connect/n",Accept); } //Process FD_READ notification if (NetworkEvents.lNetwoAD)rkEvents & FD_RE { if (NetworkEvents.iErrorCode[FD_READ_BIT !=0]) { //Error break; } //Read data from the socket recv(Socket[Index-WSA_WAIT_EVENT_0],buffer,sizeof(buffer),0); } //process FD_WRITE notitication if (NetworkEvents.lNetworkEvents & FD_WRITE) { if (NetworkEvents.iErrorCode[FD_WRITE_BIT] !=0) { //Error break; } send(Socket[Index-WSA_WAIT_EVENT_0],buffer,sizeof(buffer),0); } if (NetworkEvents.lNetworkEvents & FD_CLOSE) { if(NetworkEvents.iErrorCode[FD_CLOSE_BIT] !=0) { //Error break; } closesocket (Socket[Index-WSA_WAIT_EVENT_0]); //Remove socket and associated event from the Socket and Event arrays and //decrement eventTotal CompressArrays(Event,Socket,& EventTotal); } }
4、overlapped(重叠) I/O
overlapped I/O是WIN32的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。在 Winsock 中,重叠 I/O(Overlapped I/O)模型能达到更佳的系统性能,高于select模型、WSAAsyncSelect(异步选择)模型和WSAEventSelect (事件选择)模型三种。重叠模型的基本设计原理便是让应用程序使
用一个重叠的数据结构(WSAOVERLAPPED),一次投递一个或多个 Winsock I/O 请求。针对这些提交的请求,在它们完成之后,我们的应用程序会收到通知,于是
我们就可以对数据进行处理了。
要想在一个套接字上使用重叠 I/O 模型,首先必须使用 WSA_FLAG_OVERLAPPED 这个标志,创建一个套接字。例如:
SOCKETs = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED);
创建套接字的时候,假如使用的是 socket 函数,那么会默认设置 WSA_FLAG_OVERLAPPED 标志。
成功建好一个套接字,同时将其与一个本地接口绑定到一起后,便可开始进行重叠 I/O 操作,为了要使用重叠结构,我们常用的 send、recv 等收发数据的函数也都要
被 WSASend、WSARecv 替换掉了,方法是调用下述的 Winsock 函数,同时指定一个 WSAOVERLAPPED 结构(可选):
WSASend()
WSASendTo()
WSARecv()
WSARecvFrom()
WSAIoctl()
AcceptEx()
TrnasmitFile()
WSA_IO_PENDING :最常见的返回值,这是说明我们的重叠函数调用成功了,但是 I/O 操作还没有完成。
若随一个WSAOVERLAPPED 结构一起调用这些函数,函数会立即完成并返回,无论套接字是否设为阻塞模式。那么我们如何来得知我们的 I/O 请求是否成功了呢?
方法有两个:
1、事件对象(有64个socket的限制)。
2、完成例程(传递回调函数地址获取通知)。
5、完成端口
“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!
从本质上说,完成端口模型要求我们创建一个Win32完成端口对象,通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。
使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任意数量的套接字句柄,管理多个I/O请求。要做到这一点,需要调用CreateCompletionPort函数。
该函数定义如下:
HANDLE CreateIoCompletionPort( HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads );
在我们深入探讨其中的各个参数之前,首先要注意该函数实际用于两个明显有别的目的:
1. 用于创建一个完成端口对象。
2. 将一个句柄同完成端口关联到一起。
最开始创建一个完成端口时,唯一感兴趣的参数便是NumberOfConcurrentThreads(并发线程的数量);前面三个参数都会被忽略。NumberOfConcurrentThreads参数的特殊之处在于,它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码创建一个I/O完成端口:
hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
该语句的作用是返回一个句柄,在为完成端口分配了一个套接字句柄后,用来对那个端口进行标定(引用)。
一、工作者线程与完成端口
成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字之前,首先必须创建一个或多个“工作者线程”,以便在I/O请求投递给完成端口对象后,为完成端口提供服务。在这个时候,大家或许会觉得奇怪,到底应创建多少个线程,以便为完成端口提供服务呢?这实际正是完成端口模型显得颇为“复杂”的一个方面,因为服务I/O请求所需的数量取决于应用程序的总体设计情况。在此要记住的一个重点在于,在我们调用CreateIoCompletionPort时指定的并发线程数量,与打算创建的工作者线程数量相比,它们代表的并非同一件事情。早些时候,我们曾建议大家用CreateIoCompletionPort函数为每个处理器
都指定一个线程(处理器的数量有多少,便指定多少线程)以避免由于频繁的线程“场景”交换活动,从而影响系统的整体性能。CreateIoCompletionPort函数的NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一次只允许n个工作者线程运行。假如在完成端口上创建的工作者线程数量超出n个,那么在同一时刻,最多只允许n个线程运行。但实际上,在一段较短的时间内,系统有可能超过这个值,但很快便会把它减少至事先在CreateIoCompletionPort函数中设定的值。那么,为何实际创建的工作者线程数量有时要比CreateIoCompletionPort函数设定的多一些呢?这样做有必要吗?如先前所述,这主要取决于
应用程序的总体设计情况。假定我们的某个工作者线程调用了一个函数,比如Sleep或WaitForSingleObject,但却进入了暂停(锁定或挂起)状态,那么允许另一个线程代替它的位置。换言之,我们希望随时都能执行尽可能多的线程;当然,最大的线程数量是事先在CreateIoCompletionPort调用里设定好的。这样一来,假如事先预计到自己的线程有可能暂时处于停顿状态,那么最好能够创建比CreateIoCompletionPort的NumberOfConcurrentThreads参数的值多的线程,以便到时候充分发挥系统的潜力。一旦在完成端口上拥有足够多的工作者线程来为I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这要求我们在一个现有的完成端口上,调用CreateIoCompletionPort函数,同时为前三个参数——FileHandle,ExistingCompletionPort和CompletionKey——提供套接字的信息。其中,
FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。ExistingCompletionPort参数指定的是一个现有的完成端口。CompletionKey(完成键)参数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”;在这个参数中,应用程序可保存与一个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对
应着与那个套接字句柄关联在一起的数据。可将其作为指向一个数据结构的指针,来保存套接字句柄;在那个结构中,同时包含了套接字的句柄,以及与那个套接字有关的其他信息。
根据我们到目前为止学到的东西,首先来构建一个基本的应用程序框架。下面阐述了如何使用完成端口模型,来开发一个ECHO服务器应用。在这个程序中,我们基本上按下述步骤行事:
1) 创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作者线程。
2) 判断系统内到底安装了多少个处理器。
3) 创建工作者线程,根据步骤2)得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务。
4) 准备好一个监听套接字,在端口5150上监听进入的连接请求。
5) 使用accept函数,接受进入的连接请求。
6) 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。
7) 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort。
8) 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,一个工作者线程会为I/O请求提供服务,同时继续处理未来的I/O请求,稍后便会在步骤3
)指定的工作者例程中,体验到这一点。
9) 重复步骤5 ) ~8 ),直至服务器中止。
二、完成端口和重叠I/O
将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础,投递发送与接收请求,开始对I/O请求的处理。接下来,可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Win32重叠I/O机制。在这种机制中,象WSASend和WSARecv这样的Winsock
API调用会立即返回。此时,需要由我们的应用程序负责在以后的某个时间,通过一个OVERLAPPED结构,来接收调用的结果。在完成端口模型中,要想做到这一点,需要使用GetQueuedCompletionStatus(获取排队完成状态)函数,让一个或多个工作者线程在完成端口上等待。该函数的定义如下:
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds );
其中,CompletionPort参数对应于要在上面等待的完成端口。lpNumberOfBytes参数负责在完成了一次I/O操作后(如WSASend或WSARecv),接收实际传输的字节数。lpCompletionKey参数为原先传递进入CreateIoCompletionPort函数的套接字返回“单句柄数据”。如我们早先所述,大家最好将套接字句柄保存在这个“键”(Key)中。lpOverlapped参数用于接收完成的I/O操作的重叠结果。这实际是一个相当重要的参数,因为可用它获取每个I/O操作的数据。而最后一个参数,dwMilliseconds,用于指定调用者希望等待一个完成数据包在完成端口上出现的时间。假如将其设为INFINITE,调用会无休止地等待下去。
三、单句柄数据和单I/O操作数据
一个工作者线程从GetQueuedCompletionStatus这个API调用接收到I/O完成通知后,在lpCompletionKey和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上的I/O处理。通过这些参数,可获得两方面重要的套接字数据:单句柄数据,以及单I/O操作数据。其中,lpCompletionKey参数包含了“单句柄数据”,因为在一个套接字首次与完成端口关联到一起的时候,那些数据便与一个特定的套接字句柄对应起来了。这些数据正是我们在进行CreateIoCompletionPort
API调用的时候,通过CompletionKey参数传递的。如早先所述,应用程序可通过该参数传递任意类型的数据。通常情况下,应用程序会将与I/O请求有关的套接字句柄保存在这里。lpOverlapped参数则包含了一个OVERLAPPED结构,在它后面跟随“单I/O操作数据”。我们的工作者线程处理一个完成数据包时(将数据原封不动打转回去,接受连接,投递另一个线程,等等),这些信息是它必须要知道的。单I/O操作数据可以是追加到一个OVERLAPPED结构末尾的、任意数量的字节。假如一个函数要求用到一个OVERLAPPED结构,我们便必须将这样的一个结构传递进去,以满足它的要求。要想做到这一点,一个简单的方法是定义一个结构,然后将OVERLAPPED结构作为新结构的第一个元素使用。举个例子来说,可定义下述数据结构,实现对单I/O操作数据的管理:
typedef struct { OVERLAPPED Overlapped; WSABUF DataBuf; CHAR Buffer[DATA_BUFSIZE]; BOOL OperationType; }PER_IO_OPERATION_DATA
该结构演示了通常要与I/O操作关联在一起的某些重要数据元素,比如刚才完成的那个I/O操作的类型(发送或接收请求)。在这个结构中,我们认为用于已完成I/O操作的数据缓冲区是非常有用的。要想调用一个WinsockAPI函数,同时为其分配一个OVERLAPPED结构,既可将自己的结构“造型”为一个OVERLAPPED指针,亦可简单地撤消对结构中的OVERLAPPED元素的引用。如下例所示:
PER_IO_OPERATION_DATA PerIoData; // 可像下面这样调用一个函数 WSARecv(socket, ..., (OVERLAPPED *)&PerIoData); // 或像这样 WSARecv(socket, ..., &(PerIoData.Overlapped));
在工作线程的后面部分,等GetQueuedCompletionStatus函数返回了一个重叠结构(和完成键)后,便可通过撤消对OperationType成员的引用,调查到底是哪个操作投递到了这个句柄之上(只需将返回的重叠结构造型为自己的PER_IO_OPERATION_DATA结构)。对单I/O操作数据来说,它最大的一个优点便是允许我们在同一个句柄上,同时管理多个I/O操作(读/写,多个读,多个写,等等)。大家此时或许会产生这样的疑问:在同一个套接字上,真的有必要同时投递多个I/O操作吗?答案在于系统的“伸缩性”,或者说“扩展能力”。例如,假定我们的机器安装了多个*处理器,每个处理器都在运行一个工作者线程,那么在同一个时
候,完全可能有几个不同的处理器在同一个套接字上,进行数据的收发操作。
PostQueuedCompletionStatus函数的定义: BOOL PostQueuedCompletionStatus( HANDLE CompletionPort, DWORD dwNumberOfBytesTransferred, ULONG_PTR dwCompletionKey, LPOVERLAPPED lpOverlapped );
其中,CompletionPort参数指定想向其发送一个完成数据包的完成端口对象。而就dwNumberOfBytesTransferred、dwCompletionKey和lpOverlapped这三个参数来说,每一个都允许我们指定一个值,直接传递给GetQueuedCompletionStatus函数中对应的参数。这样一来,一个工作者线程收到传递过来的三个GetQueuedCompletionStatus函数参数后,便可根据由这三个参数的某一个设置的特殊值,决定何时应该退出。例如,可用dwCompletionPort参数传递0值,而一个工作者线程会将其解释成中止指令。一旦所有工作者线程都已关闭,便可使用CloseHandle函数,关闭完成端口,最终安全退出程序。