现在网络游戏很流行,上万个玩家同时在线的情况很常见,网游服务器如何处理这么巨量的数据?!要是读过了"Winsock I/O 方法"这篇文章,可以了解到套接字I/O 模型 中的:select,WSAAsyncSelect,WSAEventSelect,Overlapped I/O 模型一次最多都只能支持6 4 个套接字!这些模型显然不能胜任。而Winsock I/O 模型中的"Completion port ",可以同时管理数百乃至上千个套接字,辅以集群技术和提升硬件配置等措施,应付网游肯定可以了。
对 IOCP 的评价
I/O 完成端口可能是Win32 提供的最复杂的内核对象。
[Advanced Windows 3rd] Jeffrey Richter
这是[IOCP] 实现高容量网络服务器的最佳方法。
[Windows Sockets2.0:Write Scalable Winsock Apps Using Completion Ports]
Microsoft Corporation
完成端口模型提供了最好的伸缩性。这个模型非常适用来处理数百乃至上千个套接字。
[Windows 网络编程 2nd] Anthony Jones & Jim Ohlund
I/O completion ports 特别显得重要,因为它们是唯一适用于高负载服务器[ 必 须同时维护许多连接线路] 的一个技术。Completion ports 利用一些线程,帮助平衡由I/O 请求所引起的负载。这样的架构特别适合用在**P 系统中产生的"scalable" 服务器。
[Win32 多线程程序设计] Jim Beveridge & Robert Wiener
IOCP 的限制
该模型只适用于Windows NT 和Windows 2000 操作系统
什么是IOCP ?
IOCP 全称I/O Completion Port ,中文译为I/O 完成端口。所谓"完成端口",实际是Win32 、Windows NT 以及Windows 2000 采用的一种I/O 构造机制,除套接字句柄之 外,实际上还可接受其他东西。IOCP 是一个异步I/O 的API ,它可以高效地将I/O 事件通知给应用程序。从本质上说,完 成端口模型要求我们创建一个Win32 完成端口对象,通过指定数量的线程,对重叠I/O 请求进行管理,以便为已经完成的重叠I/O 请求提供服务。IOCP 只不过是用来进行读写操作,和文件I/O 有些类似。
使用IOCP
Microsoft 为IOCP 提 供了相应的API 函数,主要的就两个: CreateIoCompletionPort( ... ), GetQueuedCompletionStatus( ... ) 。
1. 首先要创建一个I/O 完成端 口对象,用它面向任意数量的套接字句柄,管理多个I/O 请求。
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,//既然要处理网络事件,那也就是将客户的socket作为HANDLE传进去
HANDLE ExistingCompletionPort,//是一个现有的完成端口
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads//并发线程的数量
);
*NumberOfConcurrentThreads 告诉系统一个完成端口上 同时允许运行的线程最大数。在默认情况下,所开线程数和 CPU 数 量相同,经验公式: 线程数 = CPU 数 * 2 + 2 。若将该参数设为 0 , 表明系统内安装了多少个处理器,便允许同时运行多少个线程!
此函数有2 种用法,不同的作用:
1. 创建一个完成端口对象
hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
2. 将 一个句柄同完成端口关联到一起
CreateIoCompletionPort((HANDLE)hAccept,hCompletionPort,(DWORD)PerHandleData,0);
2. 接收有关I / O 操作完成情况的通知
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,//线程要监视哪一个完成端口
LPDWORD lpNumberOfBytes,//接收实际传输的字节数
PULONG_PTR lpCompletionKey,//为原先传递进入CreateCompletionPort函数的套接字返回"单句柄数据"
LPOVERLAPPED *lpOverlapped, //接收完成的I/O操作的重叠结果
DWORD dwMilliseconds //指定等待一个完成数据包在完成端口上出现的时间。
);
一个工作者线程从G e tQueuedCompletionStatus 这个API 调用接收到I/O 完成通知后,在lpCompletionKey 和lpOverlapped 参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上的I/O 处理。
lpCompletionKey 参数包含了"单句柄数 据",因为在一个套接字首次与完成端口关联到一起的时候,那些数据便与一个特定的套接字句柄对应起来了。通常情况下,应用程序会将与I / O 请求有关的套接字句柄保存在这里。
lpOverlapped 参 数则包含 了一个OVERLAPPED 结构,在它后面跟随"单I / O 操作数据"。单I / O 操作数据可以是追加到一个OVERLAPPED 结 构末尾的、任意数量的字节。要想做到这一点,一个简单的方法是定义一个结构,然后将OVERLAPPED 结 构作为新结构的第一个元素使用。
同 时运行了一个或多个线程,在几个不同的套接字上执行I/O 操作的时候。要避免的一个重要问题是在进行重叠I/O 操作的同时,强行释放一个OVERLAPPED 结构。要想避 免出现这种情况,最好的办法是针对每个套接字句柄,调用closesocket 函数,任何尚未进行的重叠I/O 操作都会完成。一旦所有套接字句柄都已关闭,便需在完成端口上, 终止所有工作者线程的运行。要想做到这一点,需要使用PostQueuedCompletionStatus 函 数,向每个工作者线程都发送一个特殊的完成数据包。该函数会指示每个线程都"立即结束并退出"。
大致的流程:
1. 初始化 Winsock
2. 创建一个完成端口
3. 根据服务器线程数创建一定量的线程数
4. 准备好一个socket 进 行bind 然后 listen
5. 进入循环accept 等待客户请求
6. 创建一个数据结构容纳socket 和 其他相关信息
7. 将连进来的socket 同 完成端口相关联
8. 投递一个准备接受的请求
以后就不断的重复5 至8 的 过程
示 例
1 .
HANDLE InitializeThreads()
{
HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (hCompletionPort == NULL) return NULL;
for (i = 0; i < g_dwNumThreads; i++)
{
hThread = CreateThread(NULL, 0, WorkerThreadProc, hCompletionPort, CREATE_SUSPENDED, &dwThreadId);
if (hThread == NULL){
CloseHandle(hCompletionPort);
return NULL;
}
SetThreadPriority(hThread, THREAD_PRIORITY_TIME_CRITICAL);
ResumeThread(hThread);
CloseHandle(hThread);
}
return hCompletionPort ;
};
2 .
g_hCompletionPort = InitializeThreads();
AcceptClients();
3 .
BOOL AcceptClients()
{
...
g_nNumSocket = EnumNetworkAdapter();
if (g_nNumSocket == 0) return FALSE;
for (int nIndex = 0; nIndex < g_nNumSocket ; nIndex++)
{
...
g_interface[nIndex].hSocket = socket(AF_INET, SOCK_STREAM, 0);
if (g_interface[nIndex].hSocket == INVALID_SOCKET) return FALSE;
nError = bind(g_interface[nIndex].hSocket, (LPSOCKADDR)&sin, sizeof(sin));
nError = listen(g_interface[nIndex].hSocket, 5);
...
}
while ( TRUE )
{
for (i = 0; i < g_nNumSocket; i++)
{
FD_SET(g_interface[i].hSocket, &readfds);
}
nRet = select(0, &readfds, NULL, NULL, NULL);
if ((nRet == 0) || (nRet == SOCKET_ERROR)) continue;
for (i = 0; i < g_nNumSocket; i++)
{
if (!FD_ISSET(g_interface[i].hSocket, &readfds))
continue;
hSocket = accept(g_interface[i].hSocket, NULL, NULL);
...
g_hCompletionPort = CreateIoCompletionPort(
(HANDLE)hSocket,
g_hCompletionPort,
lpClientContext->dwVerifyClientContext,
0);
if (g_hCompletionPort == NULL){
CloseClient(lpClientContext);
continue;
}
...
nError = setsockopt(hSocket, SOL_SOCKET, SO_SNDBUF, (char *)&nZero, sizeof(nZero));
if (nError == SOCKET_ERROR)
{
PostQueuedCompletionStatus(g_hCompletionPort, 0, (DWORD)lpClientContext->dwVerifyClientContext, NULL);
continue;
}
...
}
return TRUE;
}
4.
DWORD WINAPI WorkerThreadProc(LPVOID lpCmpltPort)
{
HANDLE hCompletionPort = (HANDLE)lpCmpltPort;
...
while (TRUE)
{
GetQueuedCompletionStatus(hCompletionPort,dwIoSize,&dwClientNo,&lpOverlapped,INFINITE);
...
}
}