socket编程之登峰造极(2)---完成端口

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 ,需在进行大量不必要的内核/ 用户模式进行调用、线程执行场景的频繁切换以及参数的汇集等等,使总体性能大打折扣
 

你可能感兴趣的:(socket编程之登峰造极(2)---完成端口)