Windows socket之IO完成端口(IOCP)模型开发
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完成端口内核对象。
[cpp] view plaincop
1 HANDLE CreateIoCompletionPort(
2
3 HANDLE hFile,
4
5 HANDLE hExistingCompletionPort,
6
7 ULONG_PTR CompletionKey,
8
9 DWORD dwNumberOfConcurrentThreads);
这个函数会完成两个任务:
一是创建一个IO完成端口对象。
二是将一个设备与一个IO完成端口关联起来。
hFile就是设备句柄。在本文中就是套接字。
hExistingCompletionPort是与设备关联的IO完成端口句柄。为NULL时,系统会创建新的完成端口。
dwCompletionKey是一个对我们有意义的值,但是操作系统并不关心我们传入的值。一般用它来区分各个设备。
dwNumberOfConcurrentThreads告诉IO完成端口在同一时间最多能有多少进程处于可运行状态。如果传入0,那么将使用默认值(并发的线程数量等于cpu数量)。
每次调用CreateIoCompletionPort时,函数会判断hExistingCompletionKey是否为NULL,如果为NULL,会创建新的完成端口内核对象。并为此完成端口创建设备列表然后将设备加入到此完成端口设备列表中(先入先出)。
如果CreateIoCompletionPort调用成功则返回完成端口的句柄。否则返回NULL。
一般情况下,分两次调用这个函数,每次实现一个功能。
首先创建新的完成端口(不关联设备):此时hFile应为INVALID_HANDLE_VALUE。
第二步:将套接字与IO完成端口关联CreateIoCompletionPort(sListenSocket,hIOPort,完成键,0);
调用此函数即告诉系统:当IO操作完成时,想完成端口发送一个IO操作完成通知。这些通知按照FIFO 方式在完成队列中等待服务线程读取。
在利用IO完成端口开发套接字应用程序时,通常声明一个结构体保存与套接字相关的信息。该结构通常作为完成键传递给CreateIoCompletionPort用以区分与套接字相关的信息。我们可以给完成键传入任何对我们有用的信息,一般情况下都是传入一个结构的地址。如可以定义以下结构,:
作为完成键传递给CreateIoCompletionPort代码如下:
3:发起重叠IO操作
将套接字与IO完成端口关联后,应用程序可以调用以下函数,发起重叠IO操作:
WSASend和WSASendTo:发送数据。
WSARecv和WSARecvFrom:接收数据。
在应用程序中通常声明一个和IO操作相关的结构体,它是WSAOVERLAPPED结构的扩展。用以保存每一次IO操作的相关信息。该结构定义如下:
除了上面这一种方法:将WSOVERLAPPED结构作为IO_OPERATION_DATAA的第一个成员外,还可以将IO_OPERATION_DATA结构继承自WSAOVERLAPPED结构。效果是一样的。
下列代码演示了调用WSARecv发起异步接收数据的过程,程序清单如下:
4:等待重叠IO操作结果:
服务线程启动后,调用GetQueuedCompletionStatus函数等待重叠IO操作的完成结果。当重叠IO操作完成时,IO操作完成通知被发送到完成端口上,此时函数返回。
GetQueuedCompletionStatus函数从完成端口完成队列中取出一个完成项。完成队列为空则等待。该函数声明如下:
[cpp] view plaincopy
1 BOOL GetQueuedCompletionStatus(
2
3 HANDLE hCompletionPort,
4
5 PDWORD pdwNumberOfBytesTransferred,
6
7 ULONG_PTR pCompletionKey,
8
9 OVERLAPPED** ppOverlapped,
10
11 DWORD dwMilliSeconds);
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函数的用法:
5:取消异步操作。
当关闭套接字时,如果此时系统还有未完成的异步操作,应用程序可以调用CancelIo函数取消等待执行的异步操作。函数声明如下:
如果函数调用成功,返回TRUE,所有在此套接字上等待的异步操作都被成功的取消。
投递完成通知
当服务器退出,应用程序可以调用PostQueuedCompletionStatus函数向服务器发送一个特殊的完成通知。服务器收到通知后即退出。
该函数声明如下:
1 BOOL PostQueuedCompletionStatus(
2
3 HANDLE hCompletionPort,
4
5 DWORD dwNumBytes,
6
7 ULONG_PTR CompletionKey,
8
9 OVERLAPPED*pOverlapped);
这个函数用来将已完成的IO通知追加到IO完成端口的队列中。
该函数的四个参数与GetQueuedCompletionStatus的函数相同。
hCompletionPort表示我们要将已完成的IO项添加到哪个完成端口的队列中。
更多关于IO完成队列的介绍请参考博文《Windows核心编程系列》十异步IO之IO完成端口
当应用程序退出时,可以指定该函数的后三个中的一个或多个为某个特殊值。
下面的代码段演示利用PostQueuedCompletionStatus函数发送退出通知的过程:
利用完成端口开发应用程序可以按一下步骤进行:
1:调用CreateIoCompletionPort创建完成端口。
2:创建服务线程。
3:接受客户端请求;
4:声明完成键结构,它包含客户端套接字信息。
5:调用CreateIoCompletionPort将套接字与完成端口关联起来。并传入完成键。
6:声明IO操作结构,它包含每次重叠IO时的操作信息。如WSAOVERLAPPED结构,WSADATA结构等。
7:在服务线程中,调用GetQueuedCompletionStatus函数等待IO操作结果
完成端口模型开发服务器程序:
为了简单起见,重点突出如何利用完成端口模型开发Windows socket程序框架,服务器会将接收到的客户端的数据原封不动的返回给客户端。
IO完成端口是异步IO的一种。它是在处理与IO有关的操作时起作用。因此一个完整的服务器程序,除了采用IOCP管理IO操作外,还必须使用前面介绍的几种机制对套接字进行监听。可以使用阻塞模式或者是WSAEventSelect模型管理。具体可以参考另一片博文《使用IOCP开发驾照考试系统》。
服务器分为两个线程。主线程和服务器线程。主线程用于接收客户端的连接请求(可以采用阻塞模式,或WSAEventSelect模式),并初始化重叠IO操作。服务器线程用于为客户端提供服务。
一:定义IO操作数据结构。在该结构中包含了OVERLAPPED,WSADATA和缓冲区,除此之外还包括IO操作类型。它指明发起IO操作的类型。根据GetQueuedCompletionStatus函数返回的重叠结构指针,可以获取当前完成的IO操作类型,从而知道完成的是什么IO操作。
二:定义CClient类。
CClient类用以管理服务器接受的套接字,并执行与客户端通信的任务。CClient的构造函数参数为服务器接受的套接字和客户端地址。在析构函数中将这个套接字关闭。CClient的Recv用以接收数据,Send函数用以发送数据。在该类中定义了一个IO_OPERATION_DATA类型m_io变量,保存客户端IO操作的相关信息。可以为接收和发送操作定义不同的IO_OPERATION_DATA变量。
由于GetQueuedCompletionStatus函数会等待成功接收和发送异步IO,因此为了区分接收和发送,在WSAOVERLAPPED扩展结构,加一个IoType成员。这样在GetQueuedCompletionStatus函数返回时,就可以根据此成员知道是接收或发送异步IO完成。
由于overlapped为该结构的第一个成员,因此overlapped的地址与IO_OPERATION_DATA结构地址相同。根据这一原理,在调用WSARecv时第六个参数可以传入IO_OPERATION_DATA的地址。这样在GetQueuedCompletionStatus函数返回时就可以根据返回的OVERLAPPED的地址得到IO_OPERATION_DATA的地址。另外一种实现方式是让IO_OPERATION_DATA从OVERLAPPED结构上继承而来。
CClient类声明如下:
Recv函数实现发起异步操作接收数据功能。在该函数中首先设置当前IO操作类型为READ,然后调用WSARecv函数接收数据,最后判断函数返回值是否为ERROR_IO_PENDING。如是,说明接收数据未能立即完成,即成功发起一个异步接收数据操作:
Recv函数定义如下:
Send函数定义:
子线程用于循环调用GetQueuedCompletionStatus函数。等待接收或发送异步IO通知。看代码:
监听套接字使用WSAEventSelect模型管理。在子线程内循环调用WSAWaitForMultipleEvents函数。
看代码: