原文http://msdn.microsoft.com/msdnmag/issues/1000/Winsock/
Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports
SUMMARY 写一个网络应用程序不难,但是写一个稳定的程序就是一个挑战。
Overlapped I/O using completion ports提供了一个真实的稳定性在Window NT
和Windows 2000.Completion ports 和Windows Socket 2.0能被用来设计应用程序,
这样的程序能稳定的接受上千条连接
文章从服务器的稳定性实现开始讨论,讨论处理低资源,高要求环境,和寻找与
稳定性相关最通用的问题
学习写网络应用程序已经被认为是一件不容易的事。事实上,只有一些原则需要掌握--
---创建,连接一个socket,接受一个连接,和发送和接受数据。真正的困难的是写一个能稳定的
接受从一个到上千个连接的程序。这篇文章将要讨论基于Windows NT 和Windows 2000的稳定程序,
用的是Winsock2。主要的焦点是服务端;然而很多话题讨论应用于两者。
因为写稳定的Winsock程序的想法让我实现了一个服务端程序,接下来的讨论是有关于运行在Window
Nt 4.0和Windows 2000上的程序。不包括Windows NT 3.x因为解决方法依赖于Winsock2.0
APIs and Scalability
Overlapped I/O 机制允许一个程序开始操作然后当这个操作完成时接受他完成的通知。这个特点对需要
很长时间的操作而言是很有效的。启动overlapped 操作的线程能够被释放以便他去做别的事情,当在
overlapped请求完成这段时间内。在Windows NT 4.0 Windows 2000上唯一提供真正的稳定性的操作的是
使用completion ports来通知的Overlapped I/O。像WSAAsyncSelect和select函数的机制被提供是为了从
Windwos 3.1和Unix之间的移植更加容易而设计的,但是不是为了稳定性。为了操作系统的内部的工作,
completion port机制被优化。
Completion Ports
一个completion port 是一个队列,操作系统将 overlapped I/O请求被完成的通知消息放入这个队列中
。一旦操作完成,一个通知消息被发送到一个能处理这个结果的工作线程。一个socket可能和一个completion
port相关联,在socket被创建以后。
一般程序将会创建一堆工作线程来处理这些通知消息。工作线程的数量根据程序的需要来指定。理想的
数目是一个处理器一个工作线程,这样的话就没有一个线程去执行一个诸塞操作了,像同步read/write或等
待一个事件。每个线程被分配一定量的cpu时间,叫做quantum。它能为他的任务而执行在其他线程被允许取
得一个时间片。如果一个线程执行了阻塞操作,操作系统将会仍出它的没有使用的时间片,然后让别的线程
代替刚才阻塞的线程来执行。因此,第一个线程没有完全用完他的quantum,然后就应该有别的线程来使用那
些留下的时间片。
两步就能使用completion port。第一步,像下面的代码来创建completion port
/*********************************************/
HANDLE hIocp;
hIocp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if (hIocp == NULL) {
// Error
}
....
/*********************************************/
一旦completion port 被创建,每一个想要使用completion port的socket必须和这个completion port 相关.
这个工作由CreateIoCompletionPort再此完成,这次设置第一个参数为想要关联的socket,设置
ExistingCompletionPort为你刚才创建的completion port 的句柄.
下面的代码创建了一个socket 然后将它和刚才创建的completion port 相关
/*********************************************/
SOCKET s;
s = socket(AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
// Error
if (CreateIoCompletionPort((HANDLE)s,
hIocp,
(ULONG_PTR)0,
0) == NULL)
{
// Error
}
...
}
/*********************************************/
这个意义上说,任何执行在这个socket的overlapped操作都会使用completion port来通知
(例如WSARead(socket,...,Overlaped,...)这样的操作叫做在socket的overlapped操作,如果他成功,
或者WSA_IO_PEDING,系统会将WSARead完成的事件放入comletion port这个队列中,但真正的操作还没做,
接下来,工作线程就被调用了,来做真正的处理)!
注意到CreateIoCompletionPort的第三个参数是一个completion key 用来指定和他相关联的socket handle
这个能用来传递与这个socket相关的上下文信息.每次一个completion notification (什么完成)到达,这个
上下文信息就能得到了.
一旦completion port 被创建并和sockets相关联,一两个线程需要用来处理completion notification.
每个线程中会有一个循环,叫做GetQueuedCompletionStatus每次穿过然后返回completion notification.
在实例真正的工作线程是什么样子的,我们需要寻找程序跟踪他的overlapped操作的方式.当一个
overlapped 调用开始时,指向overlapped 结构的指针被当作参数传递.GetQueuedCompletionStatus 将会返
回同样的指针当操作完成时.单单有这个结构,应用程序不能区别那个操作刚刚完成.为了跟踪已经完成的操作
,定义你自己的OVERLAPPED 结构,其中包含关于每个进入completion port的操作的任何额外的信息
/*********************************************/
Figure 1 Overlapped Structure
typedef struct _OVERLAPPEDPLUS {
OVERLAPPED ol;
SOCKET s, sclient;
int OpCode;
WSABUF wbuf;
DWORD dwBytes, dwFlags;
// other useful information
} OVERLAPPEDPLUS;
#define OP_READ 0
#define OP_WRITE 1
#define OP_ACCEPT 2
--------------------------------------------------------------------------------
Figure 2 Worker Thread
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
ULONG_PTR *PerHandleKey;
OVERLAPPED *Overlap;
OVERLAPPEDPLUS *OverlapPlus,
*newolp;
DWORD dwBytesXfered;
while (1)
{
ret = GetQueuedCompletionStatus(
hIocp,
&dwBytesXfered,
(PULONG_PTR)&PerHandleKey,
&Overlap,
INFINITE);
if (ret == 0)
{
// Operation failed
continue;
}
OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
switch (OverlapPlus->OpCode)
{
case OP_ACCEPT:
// Client socket is contained in OverlapPlus.sclient
// Add client to completion port
CreateIoCompletionPort(
(HANDLE)OverlapPlus->sclient,
hIocp,
(ULONG_PTR)0,
0);
// Need a new OVERLAPPEDPLUS structure
// for the newly accepted socket. Perhaps
// keep a look aside list of free structures.
newolp = AllocateOverlappedPlus();
if (!newolp)
{
// Error
}
newolp->s = OverlapPlus->sclient;
newolp->OpCode = OP_READ;
// This function prepares the data to be sent
PrepareSendBuffer(&newolp->wbuf);
ret = WSASend(
newolp->s,
&newolp->wbuf,
1,
&newolp->dwBytes,
0,
&newolp.ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
// Put structure in look aside list for later use
FreeOverlappedPlus(OverlapPlus);
// Signal accept thread to issue another AcceptEx
SetEvent(hAcceptThread);
break;
case OP_READ:
// Process the data read
// ...
// Repost the read if necessary, reusing the same
// receive buffer as before
memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
ret = WSARecv(
OverlapPlus->s,
&OverlapPlus->wbuf,
1,
&OverlapPlus->dwBytes,
&OverlapPlus->dwFlags,
&OverlapPlus->ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
break;
case OP_WRITE:
// Process the data sent, etc.
break;
} // switch
} // while
} // WorkerThread
/*********************************************/
无论什么时候,一个overlapped操作被执行(调用Read使用了Overlapped参数),一个OVERLAPPEDPLUS结构被作
为lpOverlapped参数传递.这个允许你设置每个overlapped调用的操作状态信息.当操作完
成,GetQueuedCompletionStatus返回的OVERLPADDED指针指向你的扩展结构.填充在OVERLAPPED扩展结构中的
OVERLPADD结构字段不需要再这个扩展结构中的第一位.OVERLPADDED结构指针返回后,CONTAINING_RECORD宏被
用来寻找扩展结构的指针!(并不是直接返回扩展结构)看看Figure2的工作线程的例子.PerHandleKey变量返回
当使用CreateIocCompletionPort来关联socket和completion时传递的CompletionKey,包含一个给出的socket
句柄,Overlap参数返回指向OVERLAPPEDPLUS结构,他来之于跟踪overlapped 操作.要记住的是如果overlapped
操作(也就是Read函数的返回值)马上失败(也就是说返回SOCKET_ERROR和不是WSA_IO_PENING),那么没有
completion notification 被放入这个队列中.相反如果overlapped调用成功或因为WSA_IO_PENING失败,完成
事件总是被放入completion port(为什么叫completion port,原来是用来放使用了Overlapped参数的
Read/Write成功的信息).
更多的信息关于使用completion ports 和winsock ,看看可SDK,那里包含Winsock completion port的例
子(Winsockde/iocp目录).更多例子信息在http://msdn.microsoft.com/library/en-
us/dnpic/html/msdn_servrapp.asp找到.另外, Network Programming for Microsoft Windows by Anthony
Jones and Jim Ohlund (Microsoft Press, 1999), 也包含了很多completion ports的例子