通常要开发网络应用程序并不是一件轻松的事情,不过,实际上只要掌握几个关键的原则也就可以了——创建和连接一个套接字,尝试进行连接,然后收发数据。真正难的是要写出一个可以接纳少则一个,多则数千个连接的网络应用程序。本文将讨论如何通过Winsock2在Windows NT? 和 Windows 2000上开发高扩展能力的Winsock应用程序。文章主要的焦点在客户机/服务器模型的服务器这一方,当然,其中的许多要点对模型的双方都适用。
API与响应规模
通过Win32的重叠I/O机制,应用程序可以提请一项I/O操作,重叠的操作请求在后台完成,而同一时间提请操作的线程去做其他的事情。等重叠操作完成后线程收到有关的通知。这种机制对那些耗时的操作而言特别有用。不过,像Windows 3.1上的WSAAsyncSelect()及Unix下的select()那样的函数虽然易于使用,但是它们不能满足响应规模的需要。而完成端口机制是针对操作系统内部进行了优化,在Windows NT 和 Windows 2000上,使用了完成端口的重叠I/O机制才能够真正扩大系统的响应规模。
完成端口
一个完成端口其实就是一个通知队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O操作一旦完成,某个可以对该操作结果进行处理的工作者线程就会收到一则通知。而套接字在被创建后,可以在任何时候与某个完成端口进行关联。
通常情况下,我们会在应用程序中创建一定数量的工作者线程来处理这些通知。线程数量取决于应用程序的特定需要。理想的情况是,线程数量等于处理器的数量,不过这也要求任何线程都不应该执行诸如同步读写、等待事件通知等阻塞型的操作,以免线程阻塞。每个线程都将分到一定的CPU时间,在此期间该线程可以运行,然后另一个线程将分到一个时间片并开始执行。如果某个线程执行了阻塞型的操作,操作系统将剥夺其未使用的剩余时间片并让其它线程开始执行。也就是说,前一个线程没有充分使用其时间片,当发生这样的情况时,应用程序应该准备其它线程来充分利用这些时间片。
完成端口的使用分为两步。首先创建完成端口,如以下代码所示:
HANDLE hIocp;
hIocp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if (hIocp == NULL) {
// Error
}
完成端口创建后,要把将使用该完成端口的套接字与之关联起来。方法是再次调用CreateIoCompletionPort ()函数,第一个参数FileHandle设为套接字的句柄,第二个参数ExistingCompletionPort 设为刚刚创建的那个完成端口的句柄。
以下代码创建了一个套接字,并把它和前面创建的完成端口关联起来:
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
}
???
}
这时就完成了套接字与完成端口的关联操作。在这个套接字上进行的任何重叠操作都将通过完成端口发出完成通知。注意,CreateIoCompletionPort()函数中的第三个参数用来设置一个与该套接字相关的“完成键(completion key)”(译者注:完成键可以是任何数据类型)。每当完成通知到来时,应用程序可以读取相应的完成键,因此,完成键可用来给套接字传递一些背景信息。
在创建了完成端口、将一个或多个套接字与之相关联之后,我们就要创建若干个线程来处理完成通知。这些线程不断循环调用GetQueuedCompletionStatus ()函数并返回完成通知。
下面,我们先来看看应用程序如何跟踪这些重叠操作。当应用程序调用一个重叠操作函数时,要把指向一个overlapped结构的指针包括在其参数中。当操作完成后,我们可以通过GetQueuedCompletionStatus()函数中拿回这个指针。不过,单是根据这个指针所指向的overlapped结构,应用程序并不能分辨究竟完成的是哪个操作。要实现对操作的跟踪,你可以自己定义一个OVERLAPPED结构,在其中加入所需的跟踪信息。
无论何时调用重叠操作函数时,总是会通过其lpOverlapped参数传递一个OVERLAPPEDPLUS结构(例如WSASend、 WSARecv等函数)。这就允许你为每一个重叠调用操作设置某些操作状态信息,当操作结束后,你可以通过GetQueuedCompletionStatus()函数获得你自定义结构的指针。注意OVERLAPPED字段不要求一定是这个扩展后的结构的第一个字段。当得到了指向OVERLAPPED结构的指针以后,可以用CONTAINING_RECORD宏取出其中指向扩展结构的指针(译者注:以上两小段一会是OVERLAPPEDPLUS结构,一会是OVERLAPPED结构,本人也看不太懂,请高手赐教)。
OVERLAPPED 结构的定义如下:
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
下面让我们来看看Figure2里工作者线程的情况。
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
其中每句柄键(PerHandleKey)变量的内容,是在把完成端口与套接字进行关联时所设置的完成键参数;Overlap参数返回的是一个指向发出重叠操作时所使用的那个OVERLAPPEDPLUS结构的指针。
要记住,如果重叠操作调用失败时(也就是说,返回值是SOCKET_ERROR,并且错误原因不是WSA_IO_PENDING),那么完成端口将不会收到任何完成通知。如果重叠操作调用成功,或者发生原因是WSA_IO_PENDING的错误时,完成端口将总是能够收到完成通知。(未完待续)
译者:刘西齐