IO完成端口與WASSocket結合使用時,發送與接收數據不能再使用Send與Recv,而需要使用WSASend與WSARecv。或者使用WriteFile與ReadFile。(PS
WriteFileEx與ReadFileEx亦不可以)
WSASend覆盖标准的send函数,并在下面两个方面有所增强:>它可以用于overlapped
socket(重叠socket)上以进行重叠发送的操作(简单地理解为就是异步send也可以了)>它可以一次发送多个缓冲区中的数据来进行集中写入。应该相当于unix上的writev,好处看来是避免Nagle算法。
注意Socket(非WSASocket函數--非WAS開頭的函數)也可以用于非阻塞,采用多线程实现。主要作用爲可與linux兼容。
IO完成端口是一种内核对象。利用完成端口,套接字应用程序能够管理数百上千个套接字。应用程序创建完成端口对象后,通过指定一定数量的服务线程,为已经完成的重叠IO操作提供服务。该模型可以达到最后的系统性能。
完成端口是一种真正意义上的异步模型。在重叠IO模型中,当Windows socket应用程序在调用WSARecv函数后立即返回,线程继续运行。另一线程在在完成端口等待操作结果,当系统接收数据完成后,会向完成端口发送通知,然后应用程序对数据进行处理。
为了将Windows打造成一个出色的服务器环境,Microsoft开发出了IO完成端口。它需要与线程池配合使用。
服务器有两种线程模型:串行和并发模型。
串行模型:单个线程等待客户端请求。当请求到来时,该线程被唤醒来处理请求。但是当多个客户端同时向服务器发出请求时,这些请求必须依次被请求。
并发模型:单个线程等待请求到来。当请求到来时,会创建新线程来处理。但是随着更多的请求到来必须创建更多的线程。这会导致系统内核进行上下文切换花费更多的时间。线程无法即时响应客户请求。伴随着不断有客户端请求、退出,系统会不断新建和销毁线程,这同样会增加系统开销。
而IO完成端口却可以很好的解决以上问题。它的目标就是实现高效服务器程序。
与重叠IO相比较
重叠IO与IO完成端口模型都是异步模型。都可以改善程序性能。但是它们也有以下区别:
1:在重叠IO使用事件通知时,WSAWaitForMultipleEvents只能等待WSA_MAXIMUM_WAIT_EVENTS(64)个事件。这限制了服务器提供服务的客户端的数量。
2:事件对象、套接字和WSAOVERLAPPED结构必须一一对应关系,如果出现一点疏漏将会导致严重的后果。
完成端口模型实现包括以下步骤:
1:创建完成端口
2:将套接字与完成端口关联。
3:调用输入输出函数,发起重叠IO操作。
4:在服务线程中,等待完成端口重叠IO操作结果。
虽然在《谈谈Windows核心编程系列》十异步IO之IO完成端口博文中,已经详细介绍了IO完成端口的方方面面.但是处于完整性的考虑,此处介绍与Windows socket IO完成端口开发有关的内容再介绍一遍。
一:创建IO完成端口IO完成端口也是一个内核对象。调用以下函数创建IO完成端口内核对象。
1HANDLECreateIoCompletionPort(
2
3HANDLEhFile,
4
5HANDLEhExistingCompletionPort,
6
7ULONG_PTRCompletionKey,
8
9DWORDdwNumberOfConcurrentThreads);
这个函数会完成两个任务:
一是创建一个IO完成端口对象。
二是将一个设备与一个IO完成端口关联起来。
hFile就是设备句柄。在本文中就是套接字。
hExistingCompletionPort是与设备关联的IO完成端口句柄。为NULL时,系统会创建新的完成端口。
dwCompletionKey是一个对我们有意义的值,但是操作系统并不关心我们传入的值。一般用它来区分各个设备。
dwNumberOfConcurrentThreads告诉IO完成端口在同一时间最多能有多少进程处于可运行状态。如果传入0,那么将使用默认值(并发的线程数量等于cpu数量)。
每次调用CreateIoCompletionPort时,函数会判断hExistingCompletionKey是否为NULL,如果为NULL,会创建新的完成端口内核对象。并为此完成端口创建设备列表然后将设备加入到此完成端口设备列表中(先入先出)。
如果CreateIoCompletionPort调用成功则返回完成端口的句柄。否则返回NULL。
一般情况下,分两次调用这个函数,每次实现一个功能。
首先创建新的完成端口(不关联设备):此时hFile应为INVALID_HANDLE_VALUE。
ExistingCompletionPort为NULL。
hIOPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
第二步:将套接字与IO完成端口关联CreateIoCompletionPort(sListenSocket,hIOPort,完成键,0);
调用此函数即告诉系统:当IO操作完成时,想完成端口发送一个IO操作完成通知。这些通知按照FIFO 方式在完成队列中等待服务线程读取。
在利用IO完成端口开发套接字应用程序时,通常声明一个结构体保存与套接字相关的信息。该结构通常作为完成键传递给CreateIoCompletionPort用以区分与套接字相关的信息。我们可以给完成键传入任何对我们有用的信息,一般情况下都是传入一个结构的地址。如可以定义以下结构,:
typedefstruct_completionKey
{
SOCKET s;
SOCKADDR_IN clientAddr;
}COMPLETIONKEY,*PCOMPLETIONKEY;
作为完成键传递给CreateIoCompletionPort代码如下:
PCOMPLETIONPKEY pCompletionKey=newCOMPLETIONKEY;
SOCKADDR_IN addr;
intlen;
sAccept=accept(sListen,(SOCKADDR*)&addr,&len);
pCompletionKey->s=sAccept;
pCompletionKey->clientAddr=addr;
HANDLEh=CreateIoCompletionPort((HANDLE)sAccept,
hIOPort,
(DWORD)pCompletionKey,
0);
3:发起重叠IO操作
将套接字与IO完成端口关联后,应用程序可以调用以下函数,发起重叠IO操作:
WSASend和WSASendTo:发送数据。
WSARecv和WSARecvFrom:接收数据。
在应用程序中通常声明一个和IO操作相关的结构体,它是WSAOVERLAPPED结构的扩展。用以保存每一次IO操作的相关信息。该结构定义如下:
typdefstruct_io_operation_data
{
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
CHARbuffer[BUFFER_SIZE];
}IO_OPERATION_DATA;
除了上面这一种方法:将WSOVERLAPPED结构作为IO_OPERATION_DATAA的第一个成员外,还可以将IO_OPERATION_DATA结构继承自WSAOVERLAPPED结构。效果是一样的。
下列代码演示了调用WSARecv发起异步接收数据的过程,程序清单如下:
IO_OPERATION_DATA *pIoData=newIO_OPERATION_DATA;
pIoData->dataBuf=pIoData->buffer;
pIoData->dataBuf.len=BUFFER_SIZE;
ZeroMemory(&pIoData->overlapped,sizeof(WSAOVERLAPPED));
if(WSARecv(sAccept,&(pIo->dataBuf),1,&recvBytes,&flags,&(pIoData->overlapped),NULL)==SOCKET_ERROR)
{
if(WSAGetLastError()!=ERROR_IO_PENDING)
{
return;
}
}
4:等待重叠IO操作结果:
服务线程启动后,调用GetQueuedCompletionStatus函数等待重叠IO操作的完成结果。当重叠IO操作完成时,IO操作完成通知被发送到完成端口上,此时函数返回。
GetQueuedCompletionStatus函数从完成端口完成队列中取出一个完成项。完成队列为空则等待。该函数声明如下:
1BOOLGetQueuedCompletionStatus(
2
3HANDLEhCompletionPort,
4
5PDWORD pdwNumberOfBytesTransferred,
6
7ULONG_PTRpCompletionKey,
8
9OVERLAPPED** ppOverlapped,
10
11DWORDdwMilliSeconds);
hCompletionPort表示线程希望对哪个完成端口进行监视,GetQueuedCompletionStatus的任务就是将调用线程切换到睡眠状态,也就是阻塞在此函数上,直到指定的IO完成端口出现一项或者超时。
pdwNumberOfBytesTransferred返回在异步IO完成时传输的字节数。
pCompletionKey返回完成键。
ppOverlapped返回异步IO开始时传入的OVERLAPPED结构地址。
dwMillisecond指定等待时间。
函数执行成功则返回true,否则返回false。
如果在完成端口上成功等待一个完成项的到来,则函数返回TRUE。此时lpNumberOfBytesTransferred,lpCompletionKey和lpOverlapped参数返回相关信息。一般从lpCompletionKey和lpOverlapped获得与本次IO相关的信息。
如果在完成端口等待失败,则返回false,此时lpOverlapped不为NULL。如果等待超时,则返回false,错误代码为WAIT_TIMEOUT。
综上,在使用完成端口开发Windows socket应用程序时,一般需要定义两种数据结构:完成键和扩展的WSAOVERLAPPED结构。完成键保存与套接字有关的信息。在GetQueuedCompletionStatus返回时可以通过该参数获取套接字的相关信息。这用于区分不同设备。
扩展的WSAOVERLAPPED结构,保存每次发起IO操作时IO操作相关的信息。当GetQueuedCompletionStatus返回时通过该参数获取套接字的IO操作相关信息。
下面展示GetQueuedCompletionStatus函数的用法:
PCOMPLETIONKEY pCompletionKey;
DWORDdwNumberOfBytesTransferrd;
LPOVERLAPPED pOverlapped;
boolret=GetQueuedCompletionStatus(hIOPort,&dwNumberOfBytesTransferred,(LPDWORD)pCompletionKey,&pOverlapped,100);
if(ret)
{
//等待成功。
}
else
{
interr=WSAGetLastError();
if(NULL!=pOverlapped)
{
//失败的IO操作。
}
elseif(ret==WAIT_TIMEOUT)
{
//超时。
}
}
5:取消异步操作。
当关闭套接字时,如果此时系统还有未完成的异步操作,应用程序可以调用CancelIo函数取消等待执行的异步操作。函数声明如下:
boolCancelIo(HANDLEhFile);
如果函数调用成功,返回TRUE,所有在此套接字上等待的异步操作都被成功的取消。
投递完成通知
当服务器退出,应用程序可以调用PostQueuedCompletionStatus函数向服务器发送一个特殊的完成通知。服务器收到通知后即退出。
该函数声明如下:
1BOOLPostQueuedCompletionStatus(
2
3HANDLEhCompletionPort,
4
5DWORDdwNumBytes,
6
7ULONG_PTRCompletionKey,
8
9OVERLAPPED*pOverlapped);
这个函数用来将已完成的IO通知追加到IO完成端口的队列中。
该函数的四个参数与GetQueuedCompletionStatus的函数相同。
hCompletionPort表示我们要将已完成的IO项添加到哪个完成端口的队列中。
当应用程序退出时,可以指定该函数的后三个中的一个或多个为某个特殊值。
下面的代码段演示利用PostQueuedCompletionStatus函数发送退出通知的过程:
PCOMPLETIONKEY pCompletionKey;
LPOVERLAPPED pOverlapped;
PostQueuedCompletionStatus(hIOPort,0,0,NULL);
boolret=GetQueuedCompletionStatus(hIOPort,
&dwNumberOfBytesTransferred,
(LPDWORD)pCompletionKey,
&pOverlapped,
100);
if(NULL==pOverlapped&&NULL==pCompletionKey)
{
//服务器退出。
}
利用完成端口开发应用程序可以按一下步骤进行:
1:调用CreateIoCompletionPort创建完成端口。
2:创建服务线程。
3:接受客户端请求;
4:声明完成键结构,它包含客户端套接字信息。
5:调用CreateIoCompletionPort将套接字与完成端口关联起来。并传入完成键。
6:声明IO操作结构,它包含每次重叠IO时的操作信息。如WSAOVERLAPPED结构,WSADATA结构等。
7:在服务线程中,调用GetQueuedCompletionStatus函数等待IO操作结果