2.完成端口和重叠I/O
将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础。投递发送或接
收请求。开始I/O请求的处理。接下来,可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说、完成瑞口模型利用了Win32重叠I/O机制。在这种机制中。象WSASend和WSARecv这样的Winsock API调用会立即返回。此时, 需要由我们的应用程序负责在以后的某个时间。通过一个OVERLAPPED结构,来接收调用的结果。在完成端口模型中。要想做到这一点,需要使用GetQueuedCompletionStatus(获取排队完成状态)函数。让一个或者多个工作者线程在完成端口上等待。该函数的定义如下:
BOOL
GetQueuedCompletionStatus
(
HANDLE
CompletionPort
,
LPDWORD
lpNumberOfBytesTransferred
,
LPDWORD
lpCompletionKey
,
LPOVERLAPPED *
lpOverlapped
,
DWORD
dwMilliseconds
)
其中,
CompletionPort
参数对应与要在上面等待的完成端口.
lpNumberOfBytesTransferred
参数负责在完成了—次I/O
操作后(
如WSASend
或WSARecv)
、接收实际传输的字节数。
lpCompletionKey
参数为原先传递进入CreateCompletionPort
函数的套接字返回“单句柄数据”。如我们早先所述,大家最好将套接字句柄保存在这个“键”(Key)
中。
lpOverlapped
参数用于接收完成的I/O
操作的重叠结果。这实际是一个相当重要的参数,因为要用它获取每个I/O
操作的数据。
DwMilliseconds
用于指定调用者希望等待一个完成数据包在完成端门上出现的时间。假如将其设为INFINITE
。调用会无休止地等持下去。
3
.单句柄数据和单
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
Bufferl[DATA_BUFSIZE];
BOOL
OperationType;
}PER_IO_OPERATION_DATA;
该结构演示了通常要与I/O
操作关联在—起的某些重要数据元素,
比如刚才完成的那个I/O
操作的类型(
发送或接收请求).
在这个结构中。我们认为用于已完成I/O
操作的数据缓冲区是非常有用的。要想调用—个Winsock API
函数,同时为其分配一个OVERLAPPED
结构,
既可将自己的结构“造型”为一个OVERLAPPED
指针,
亦可简单地撤消对结构中的OVBRLAPPED
元素的引用。如下例所示:
PER_IO_OPERATION_DATA PerIoData;
可以象下边这样调用一个函数
WSARecv(socket,…,(OVERLAPPED *)&PerIoData;
或者象下边这样
WSARecv(socket
,…,&( PerIoData.Overlapped));
在工作线程的后面部分。等GetQueuedCompletionStatus
函数返回了—个重叠结构(
和完成键)
后。便可通过撤消对OperationType
成员的引用。调查到底是哪个操作投递到了这个句柄之上(
只需将返回的重叠结构造型为自的PER_IO_OPERATlON_DATA
结构)
。对单I/O
操作数据来说,
它最大的—个优点便是允许我们在同一个句柄上。同时管理多个I/O
操作(
读/
写、多个读、多个写,等等)
。大家此时或许会产生这样的疑问:在同—个套接字上,
真的有必要同时投递多个I/O
操作吗?
答案在于系统的“伸缩性”,
或者说“扩展能力”。例如,
假定我们的机器安装了多个中央处理器。每个处理器都在远行一个工作者线程,那么在同一个时候、完全可能有几个不同的处理器在同一个套接字上,进行数据的收发操作。
为了完成前述的简单回应服务器示例,我们需要提供一个ServerWorkerThread(
服务器工作者线程)
函数。在程序消单8.10
中,我们展示了如何设计一个工作者线程例程,
令其使用单句柄数据以及单I/O
操作数据,
为I/O
请求提供服务。
程序代码见下节……
在程序清单8-9
和程序清单8-10
列出的简单服务器示例中(
配套光盘也有),
最后要注意的一处细节是如何正确地关闭I/O
完成端口一—特别是同时运行了一个或多个线程,在几个不同的套接字上执行I/O
操作的时候。要避免的一个重要问题是在进行重叠I/O
操作的同时,强行释放—个OVERLAPPED
结构。要想避免出现这种情况,最好的办法是针对每个套接字句柄,调用closesocket
函数。任何尚未进行的重叠I/O
操作都会完成。—旦所有套接字句柄都已关闭。便需在完成端口上,
终止所有工作者线程的运行。要想做到这一点,需要使用
PostQueuedCompletionStatus
函数,向每个工作者线程都发送—个特殊的完成数据包。该函数会指示每个线程都“立即结束并退出”.
下面是PostQueuedCompletionStatus
函数的定义:
BOOL PostQueuedCompletionStatus(
HANDLE CompletlonPort,
DW0RD dwNumberOfBytesTrlansferred,
DWORD dwCompletlonKey,
LPOVERLAPPED lpoverlapped,
);
其中,CompletionPort
参数指定想向其发送一个完成数据包的完成端口对象。而就dwNumberOfBytesTransferred,dwCompletionKey
和lpOverlapped
这三个参数来说.
每—个都允许我们指定—个值,
直接传递给GetQueuedCompletionStatus
函数中对应的参数。这样—来。—个工作者线程收到传递过来的三个GetQueuedCompletionStatus
函数参数后,便可根据由这三个参数的某一个设置的特殊值,决定何时应该退出。例如,
可用dwCompletionPort
参数传递0
值,
而—个工作者线程会将其解释成中止指令。一旦所有工作者线程都已关闭,
便可使用CloseHandle
函数,
关闭完成端口。最终安全退出程序。
4
.其他问题
另外还有几种颇有价值的技术。可用来进—步改善套接字应用程序的总体I/O
性能。值得考虑的一项技术是试验不同的套接字缓冲区大小,以改善I/O
性能和应用程序的扩展能力。例如,
假如某个程序只采用了—个比较大的缓冲区,仅能支持—个wSARecv
请求,而不是同时设置了三个较小的缓冲区。提供对三个WSARecv
请求的支持,那么该程序的扩展能力并不是很好,
特别是在转移到安装了多个处理器的机器上之后。这是由于单独一个缓冲区每次只能处理一个线程!
除此以外,
单缓冲区设计还会对性能造成一定的干扰,
假如—次仅能进行—次接收操作。网络协议驱动程序的潜力使不能得到充分发挥(
它经常都会很“闲”)
。换言之,假如在接收更多的数据前、需要等待—次WSARecv
操作的完成,
那么在WSARecv
完成和下一次接收之间,整个协议实际上处于“休息”状态。
另—个值得考虑的性能改进措施是用套接宇选项SO_SNDBUF
和SO_RCVBUF
对内部套接字缓冲区的大小进行控制。利用这些选项,
应用程序可更改—个套接字的内部数据缓冲区的大小。如将该设为0,Winsock
便会在重叠I/O
调用中直接使用应用程序的缓冲区、进行数据在
协议堆栈里的传人,传出。这样一来,在应用程序与Winsock
之间,
便避免了进行—次缓冲区复制的必要。下述代码片断阐释了如何使用SO_SNDBUF
选项,来进行setsockopt
函数的调用:
setsockopt(socket
,SOL_S0CKET,SO_SNDBUF
,
(char *)&nZero,sizeof(nZero));
要注意的是,将这些缓冲区的大小设为0
后,
只有在一段给定的时间内,存在着多个I/O
请求的前提下才会产生积极作用。等到第9
章,我们会向大家更深入地讲述套接字选项的知识。
提升性能的最后一项措施是使用AcceptEx
这个API
调用,来进行连接请求的处理,并投递少量数据。这样一来,我们的应用程序只需通过一次API
调用,便可为一次接受请求和数据的接收提供服务。从而减少了单独进行accept
和 WSARecv
调用造成的开销。这样做还有另一个好处,我们可使完成端口为AcceptEx
提供服务,因为它也提供了一个OVERLAPPED
结构。
假如事先预计到自己的服务器应用在一个连接建立好之后,只会进行少量的recv-send
(收发)操作,那么AcceptEx
便显得相当有用(
比如在设计
一个Web
服务器的时候)
。否则的话,接受一个连接后,假如程序要负责数百上千次数据
的传输操作,这样的对性能便没有多大的助益。
最后提醒大家,注意,在Winsock
中,一个Winsock
应用不应使用ReadFile
和 WriteFile
这两个Win32
函数,在一个完成端口上进行IO
处理。尽管这两个函数确实提供了一个OVERLAPPED
结构,而且可在完成端口上成功的使用,但就在Winsock 2
环境下进行IO
处理来说,WSARecv
和 WSASend
这两个函数却进行了更大程序的优化。若使用ReadFile WriteFile
,需在进行大量不必要的内核/
用户模式进行调用、线程执行场景的频繁切换以及参数的汇集等等,使总体性能大打折扣