WINSOCK I\O模型有六种:
一:select模型
二:WSAAsyncSelect模型
三:WSAEventSelect模型
四:Overlapped I/O 事件通知模型
五:Overlapped I/O 完成例程模型
六:完成端口IOCP模型
且一个比一个完善,一个比一个高深。最好用的莫过于完成端口,但可惜的是只有NT、2000的系统才支持这种功能。心痛之余,我们只能寄希望于Overlapped I/O模型。
下面,我们来详细分析一下重叠模型:
一、重叠模型的优点
1)可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。
2)比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。
因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。
而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。
3)从《windows网络编程》中提供的试验结果中可以看到,在使用了P4 1.7G Xero处理器(CPU很强啊)以及768MB的回应服务器中,最大可以处理4万多个SOCKET连接,在处理1万2千个连接的时候CPU占用率才40% 左右 ―― 非常好的性能,已经直逼完成端口了^_^
二、重叠模型的基本原理
说了这么多的好处,你一定也跃跃欲试了吧,不过我们还是要先提一下重叠模型的基本原理。
概括一点说,重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。
需要注意的是,有两个方法可以用来管理重叠IO请求的完成情况(就是说接到重叠操作完成的通知):
1)事件对象通知(event object notification)
2)完成例程(completion routines) ,注意,这里并不是完成端口
而本文只是讲述如何来使用事件通知的的方法实现重叠IO模型,完成例程的方法准备放到下一篇讲 :) (内容太多了,一篇写不完啊) ,如没有特殊说明,本文的重叠模型默认就是指的基于事件通知的重叠模型。
既然是基于事件通知,就要求将Windows事件对象与WSAOVERLAPPED结构关联在一起(WSAOVERLAPPED结构中专门有对应的参数),通俗一点讲,就是。。。。对了,忘了说了,既然要使用重叠结构,我们常用的send, sendto, recv, recvfrom也都要被WSASend, WSASendto, WSARecv, WSARecvFrom替换掉了, 它们的用法我后面会讲到,这里只需要注意一点,它们的参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作“绑定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完WSARecv以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要德数据了。
三、关于重叠模型的基础知识
下面来介绍并举例说明一下编写重叠模型的程序中将会使用到的几个关键函数。
1)WSAOVERLAPPED结构
这个结构自然是重叠模型里的核心,它是这么定义的
1 |
typedef struct _WSAOVERLAPPED |
2 |
{ DWORD Internal; |
3 |
DWORD InternalHigh; |
4 |
DWORD Offset; |
5 |
DWORD OffsetHigh; |
6 |
WSAEVENT hEvent; // 唯一需要关注的参数,用来关联WSAEvent对象 |
7 |
} WSAOVERLAPPED, *LPWSAOVERLAPPED; |
我们需要把WSARecv等操作投递到一个重叠结构上,而我们又需要一个与重叠结构“绑定”在一起的事件对象来通知我们操作的完成,看到了和hEvent参数,不用我说你们也该知道如何来来把事件对象绑定到重叠结构上吧?大致如下:
1 |
WSAEVENT event; // 定义事件 |
2 |
WSAOVERLAPPED AcceptOverlapped ; // 定义重叠结构 |
3 |
event = WSACreateEvent(); // 建立一个事件对象句柄 |
4 |
ZeroMemory(&AcceptOverlapped, sizeof (WSAOVERLAPPED)); // 初始化重叠结构 |
5 |
AcceptOverlapped.hEvent = event; // Done !! |
2)WSARecv系列函数
在重叠模型中,接收数据就要靠它了,它的参数也比recv要多,因为要用刀重叠结构嘛,它是这样定义的:
01 |
int WSARecv(SOCKET s, // 当然是投递这个操作的套接字 |
02 |
LPWSABUF lpBuffers, // 接收缓冲区,与Recv函数不同 |
03 |
// 这里需要一个由WSABUF结构构成的数组 |
04 |
DWORD dwBufferCount, // 数组中WSABUF结构的数量 |
05 |
LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回函数调用 |
06 |
// 所接收到的字节数 |
07 |
LPDWORD lpFlags, // 说来话长了,我们这里设置为0 即可 |
08 |
LPWSAOVERLAPPED lpOverlapped, // “绑定”的重叠结构 |
09 |
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine |
10 |
// 完成例程中将会用到的参数,我们这里设置为 NULL |
11 |
); |
返回值:
WSA_IO_PENDING : 最常见的返回值,这是说明我们的WSARecv操作成功了,但是I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成
其他的函数我这里就不一一介绍了,因为我们毕竟还有MSDN,而且在讲后面的完成例程和完成端口的时候我还会讲到一些 ^_^
3)WSAWaitForMultipleEvents函数
熟悉WSAEventSelect模型的朋友对这个函数肯定不会陌生,不对,其实大家都不应该陌生,这个函数与线程中常用的WaitForMultipleObjects函数有些地方还是比较像的,因为都是在等待某个事件的触发嘛。
因为我们需要事件来通知我们重叠操作的完成,所以自然需要这个等待事件的函数与之配套。
1 |
DWORD WSAWaitForMultipleEvents( |
2 |
DWORD cEvents, // 等候事件的总数量 |
3 |
const WSAEVENT* lphEvents, // 事件数组的指针 |
4 |
BOOL fWaitAll, // 这个要多说两句: 如果设置为 TRUE,则事件数组中所有事件被传信的时候函数才会返回,FALSE则任何一个事件被传信函数都要返回,我们这里肯定是要设置为FALSE的 |
5 |
DWORD dwTimeout, // 超时时间,如果超时,函数会返回WSA_WAIT_TIMEOUT,如果设置为0,函数会立即返回,如果设置为 WSA_INFINITE只有在某一个事件被传信后才会返回,在这里不建议设置为WSA_INFINITE |
6 |
BOOL fAlertable // 在完成例程中会用到这个参数,这里我们先设置为FALSE |
7 |
); |
返回值:
WSA_WAIT_TIMEOUT :最常见的返回值,我们需要做的就是继续Wait
WSA_WAIT_FAILED : 出现了错误,请检查cEvents和lphEvents两个参数是否有效
如果事件数组中有某一个事件被传信了,函数会返回这个事件的索引值,但是这个索引值需要减去预定义值 WSA_WAIT_EVENT_0才是这个事件在事件数组中的位置。
具体的例子就先不在这里举了,后面还会讲到
注意:WSAWaitForMultipleEvents函数只能支持由WSA_MAXIMUM_WAIT_EVENTS对象定义的一个最大值,是 64,就是说WSAWaitForMultipleEvents只能等待64个事件,如果想同时等待多于64个事件,就要 创建额外的工作者线程,就不得不去管理一个线程池,这一点就不如下一篇要讲到的完成例程模型了。
4)WSAGetOverlappedResult函数
既然我们可以通过WSAWaitForMultipleEvents函数来得到重叠操作完成的通知,那么我们自然也需要一个函数来查询一下重叠操作的结果,定义如下
1 |
BOOL WSAGetOverlappedResult( |
2 |
SOCKET s, // SOCKET,不用说了 |
3 |
LPWSAOVERLAPPED lpOverlapped, // 这里是我们想要查询结果的那个重叠结构的指针 |
4 |
LPDWORD lpcbTransfer, // 本次重叠操作的实际接收(或发送)的字节数 |
5 |
BOOL fWait, // 设置为TRUE,除非重叠操作完成,否则函数不会返回,设置FALSE,而且操作仍处于挂起状态,那么函数就会返回FALSE,错误为WSA_IO_INCOMPLETE, 不过因为我们是等待事件传信来通知我们操作完成,所以我们这里设,置成什么都没有作用…..-_-b 别仍鸡蛋啊,我也想说得清楚一些… |
6 |
LPDWORD lpdwFlags // 指向DWORD的指针,负责接收结果标志 |
7 |
); |
这个函数没什么难的,这里我们也不需要去关注它的返回值,直接把参数填好调用就可以了,这里就先不举例了
唯一需要注意一下的就是如果WSAGetOverlappedResult完成以后,第三个参数返回是 0 ,则说明通信对方已经关闭连接,我们这边的SOCKET, Event之类的也就可以关闭了。
实现重叠模型的步骤作了这么多的准备工作,费了这么多的笔墨,我们终于可以开始着手编码了。其实慢慢的你就会明白,要想透析重叠结构的内部原理也许是要费点功夫,但是只是学会如何来使用它,却是真的不难,唯一需要理清思路的地方就是和大量的客户端交互的情况下,我们得到事件通知以后,如何得知是哪一个重叠操作完成了,继而知道究竟该对哪一个套接字进行处理,应该去哪个缓冲区中的取得数据,everything will be OK^_^。下面我们配合代码,来一步步的讲解如何亲手完成一个重叠模型。
下面是我写的一个例子,用的标准C\C++,可以直接编译。
由于WSAWaitForMultipleEvents最多只能同时等待64个消息,所以两个线程最多支持64个连接,若要更多可以在开一个线程,达到128个连接。以此类推,成线性增长。
但线程过多的话,由于CPU忙于在线程上下文之间的切换,也会影响程序的性能,所以这种模式,还是不太适合非常多的连接数,如10000多个连接就不行了,这时,我们只能用后面的完成例程或完成端口了,这也正是它的弊端所在。
源码---------------------------------------------------------------------------------------------
001 |
#pragma comment(lib,"ws2_32.lib") |
002 |
#include <winsock2.h> |
003 |
#include <stdio.h> |
004 |
#define DATA_BUFSIZE 1024 // 接收缓冲区大小 |
005 |
SOCKET ListenSocket, |
006 |
AcceptSocket[DATA_BUFSIZE] = {0}; |
007 |
WSABUF DataBuf[DATA_BUFSIZE]; |
008 |
WSAOVERLAPPED Overlapped[DATA_BUFSIZE]; // 重叠结构 |
009 |
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 用来通知重叠操作完成的事件句柄数组 |
010 |
DWORD dwRecvBytes = 0, // 接收到的字符长度 |
011 |
Flags = 0; // WSARecv的参数 |
012 |
DWORD volatile dwEventTotal = 0; // 程序中事件的总数 |
013 |
014 |
//由于EVENT数量限制,目前最多只能支持64个连接 |
015 |
DWORD WINAPI AcceptThread( LPVOID lpParameter) |
016 |
{ |
017 |
WSADATA wsaData; |
018 |
WSAStartup(MAKEWORD(2,2),&wsaData); |
019 |
ListenSocket = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,NULL,WSA_FLAG_OVERLAPPED); |
020 |
SOCKADDR_IN ServerAddr; |
021 |
ServerAddr.sin_family = AF_INET; |
022 |
ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); |
023 |
ServerAddr.sin_port = htons(1234); |
024 |
bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof (ServerAddr)); |
025 |
listen(ListenSocket,100); |
026 |
printf ( "listenning...\n" ); |
027 |
int i = 0; |
028 |
SOCKADDR_IN ClientAddr; |
029 |
int addr_length= sizeof (ClientAddr); |
030 |
while (TRUE) |
031 |
{ |
032 |
while ((AcceptSocket[i] == 0) && (AcceptSocket[i] = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET) |
033 |
{ |
034 |
printf ( "accept %d ip:%s port:%d\n" ,i+1,inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port); |
035 |
EventArray[i] = WSACreateEvent(); |
036 |
dwEventTotal++; |
037 |
memset (&Overlapped[i],0, sizeof (WSAOVERLAPPED)); |
038 |
Overlapped[i].hEvent = EventArray[i]; |
039 |
char * buffer = new char [DATA_BUFSIZE]; |
040 |
memset (buffer,0,DATA_BUFSIZE); |
041 |
DataBuf[i].buf = buffer; |
042 |
DataBuf[i].len = DATA_BUFSIZE; |
043 |
if (WSARecv(AcceptSocket[i], &DataBuf[i], dwEventTotal, &dwRecvBytes, &Flags, &Overlapped[i], NULL) == SOCKET_ERROR) |
044 |
{ |
045 |
int err = WSAGetLastError(); |
046 |
if (WSAGetLastError() != WSA_IO_PENDING) |
047 |
{ |
048 |
printf ( "disconnect: %d\n" ,i+1); |
049 |
closesocket(AcceptSocket[i]); |
050 |
AcceptSocket[i] = 0; |
051 |
//WSACloseEvent(EventArray[i]); // 关闭事件 |
052 |
DataBuf[i].buf = NULL; |
053 |
DataBuf[i].len = NULL; |
054 |
continue ; |
055 |
} |
056 |
} |
057 |
i = (i+1)%WSA_MAXIMUM_WAIT_EVENTS; |
058 |
} |
059 |
|
060 |
} |
061 |
return FALSE; |
062 |
} |
063 |
064 |
DWORD WINAPI ReceiveThread( LPVOID lpParameter) |
065 |
{ |
066 |
DWORD dwIndex = 0; |
067 |
while ( true ) |
068 |
{ |
069 |
dwIndex = WSAWaitForMultipleEvents(dwEventTotal, EventArray, FALSE, 1000, FALSE); |
070 |
if (dwIndex == WSA_WAIT_FAILED || dwIndex == WSA_WAIT_TIMEOUT) |
071 |
continue ; |
072 |
dwIndex = dwIndex - WSA_WAIT_EVENT_0; |
073 |
WSAResetEvent(EventArray[dwIndex]); |
074 |
|
075 |
DWORD dwBytesTransferred; |
076 |
WSAGetOverlappedResult( AcceptSocket[dwIndex], &Overlapped[dwIndex], &dwBytesTransferred, FALSE, &Flags); |
077 |
if (dwBytesTransferred == 0) |
078 |
{ |
079 |
printf ( "disconnect: %d\n" ,dwIndex+1); |
080 |
closesocket(AcceptSocket[dwIndex]); |
081 |
AcceptSocket[dwIndex] = 0; |
082 |
//WSACloseEvent(EventArray[dwIndex]); // 关闭事件 |
083 |
DataBuf[dwIndex].buf = NULL; |
084 |
DataBuf[dwIndex].len = NULL; |
085 |
continue ; |
086 |
} |
087 |
//使用数据 |
088 |
printf ( "%s\n" ,DataBuf[dwIndex].buf); |
089 |
memset (DataBuf[dwIndex].buf,0,DATA_BUFSIZE); |
090 |
if (WSARecv(AcceptSocket[dwIndex], &DataBuf[dwIndex], dwEventTotal, &dwRecvBytes, &Flags, &Overlapped[dwIndex], NULL) == SOCKET_ERROR) |
091 |
{ |
092 |
if (WSAGetLastError() != WSA_IO_PENDING) |
093 |
{ |
094 |
printf ( "disconnect: %d\n" ,dwIndex+1); |
095 |
closesocket(AcceptSocket[dwIndex]); |
096 |
AcceptSocket[dwIndex] = 0; |
097 |
//WSACloseEvent(EventArray[dwIndex]); // 关闭事件 |
098 |
DataBuf[dwIndex].buf = NULL; |
099 |
DataBuf[dwIndex].len = NULL; |
100 |
continue ; |
101 |
} |
102 |
} |
103 |
} |
104 |
|
105 |
return FALSE; |
106 |
} |
107 |
108 |
void main() |
109 |
{ |
110 |
HANDLE hThreads[2]; |
111 |
hThreads[0] = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL); |
112 |
hThreads[1] = CreateThread(NULL, 0, ReceiveThread, NULL, NULL, NULL); |
113 |
|
114 |
WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); |
115 |
printf ( "exit\n" ); |
116 |
CloseHandle(hThreads[0]); |
117 |
CloseHandle(hThreads[1]); |
118 |
} |
由于篇幅问题,今天只讨论了Overlapped I/O 事件通知模型,而Overlapped I/O 完成例程模型将会在下一篇中讨论,敬请期待。。。