关闭

IOCP在服务器开发中的应用

标签: 服务器工作数据结构socketapi多线程
2966人阅读 评论(1) 收藏 举报
分类:

引言


基于Socket的网络通信服务已经使用得相当普遍,然而一个服务器应用程序,假如不能够同时为多个客户端提供服务,那它就没有什么意义可言。针对一个服务器应用程序底层通信模块的设计,要使其在给定的时间内同时控制几个套接字,采用重叠的I/O机制是比较好的,但是要求服务器在任何给定时间内都会为海量I/O请求提供服务,Winsock 2.0中引入的内核级完成端口(Input/Output Completion Port,IOCP)是处理大量并发连接的最佳处理方案。相对于其他I/O模型,IOCP针对操作系统内部进行了优化,提供了较好的伸缩性和较高的数据吞吐率,满足服务器的高性能要求。文中针对基于IOCP通信模式的服务器程序的实现进行了探讨。

一、完成端口(IOCP)

    IOCP模型是微软提供的用于Windows系统上高效处理各种设备I/O的一种机制,它提供了一个高效复杂的内核对象,该对象通过指定数量的线程。对重叠的I/O操作完成进行处理。它的核心思想简单概括如下:将所有用户的请求投递到一个消息队列中,利用事先创建好的若干个工作者线程逐一从消息队列中取出消息并加以处理。它可以为任何用户的任何I/O操作服务,只需少数几个线程就可以处理大量I/O请求,避免CPU花费时间在大量的线程调度上,提高了资源的利用率。

    1.1 重叠I/O

    IOCP模型是基于重叠I/O技术的。重叠I/O(Overlapped I/O)是Win32的一项技术,它的基本原理是让应用程序使用一个重叠的数据结构(OVERLAPPED),一次投递—个或多个I/O请求。不论该请求是否已经完成,做投递动作的函数马上返回,I/O的实际工作则交由操作系统底层处理。

    1.2 工作者线程

    IOCP使用多线程机制,它创建并管理若干个工作者线程[Worker Threads]。工作者线程服务于IOCP,用于处理到达的I/O完成通知。


   1.3 信息的传递


工作者线程调用相应API函数接收到I/O完成通知。通过参数的传递,可获得与之相关的两方面重要的套接字数据;单I/O操作数据和单句柄数据。系统利用OVERLAPPED结构进行重叠I/O操作。这个结构后边跟着“单I/O操作数据”,通过构建一个新的类来组织它们。“单句柄数据”是指—个套接字句柄首次与IOCP相关联时所使用的一个数据结构,它存储与这个特定的套接字句柄相关联的上下文信息。

二、IOCP应用中存在的问题及解决办法


在实际应用IOCP模型来开发服务器/客户端的过程中会遇到各种各样棘手的问题,详述如下。

    2.1 WSAENOBUFS出错


伴随着每一次重叠发送和接收操作,其中指定的发送或接收缓冲区会被加锁。而操作系统会强行为能被锁定的内存的大小设定一个上限,当达到这个上限,重叠操作将失败,并发送WSAENOBUFS错误。假如服务器在每个连接上提供多个重叠接收,随着连接数量的增长,很快就会达到这个极限。因此,服务器可以在每个连接上循环投递使用零字节缓冲区的接收,这时的接收操作和内存无关,内存不需要被锁定。并且当带零缓冲区的接收操作完成并返回时,每一个套接字的底层缓冲区的数据被完整地保留而没有被读取到接收操作的缓冲区来。由于每个投递操作是按顺序返回的,总能保证有被锁定的内存被成功解锁,此时服务器又可以投递一个或多个带非零字节缓冲区的重叠接收操作,将存在于套接字缓冲区中的数据读出来。

    2.2 数据包的重排序


尽管使用IOCP,可以使数据按照它们被发送的顺序进行可靠的处理,但是实际工作者线程的完成顺序是不确定的,例如,存在两个工作者线程,并且接收到了“数据包A、数据包B、数据包C”,就有可能按另一个顺序处理它们,如“数据包C、数据包A、数据包B”。这也意味着,当你通过投递发送请求到IOCP来发送数据时,待发送的数据也被重新排序了。针对该问题,对内存类增加顺序号,并按照顺序号来依次处理内存,而具有不正确顺序号的内存将被保存起来备用。

    2.3 异步阻塞读和字节块包处理

    TCP协议是基于包的流协议,包头包含着一个完整包长度的信息,系统通过读取包头得到该长度数据,并将其与已读数据的长度作比较来判断是否读包完成。当系统只提供一个异步阻塞读操作(即一个重叠接收)时,该机制完美无缺;但是为了完全发挥IOCP的能力,采用多个异步读操作,在同一个时间段内,多个读操作等待的数据会同时到达。类同2.节所讨论,多个异步读的无序完成会导致返回字节块流的无序,进一步说,一个字节块流可能包含一个或多个包,或者包的一部分。


图1表明部分包(黑色块)和完整包(白色块)在字节块流中是如何异步到达的。这就意味着要想成功解读一个完整的应用层逻辑数据包,必须处理字节流数据块和部分包。在此需要编写专门的包处理函数来解决。其中,“把几个数据包合并成一个数据包”的拼包操作必然会涉及内存的复制拷贝。在此采用“环形缓冲区”思想:模块处理完一次从缓冲区里取走所有完整逻辑数据包后,把新接收的数据包直接复制到缓冲区内剩下数据的尾部,直接进行下一次逻辑解析,避免了频繁调用内存拷贝函数。

2.4 访问紊乱(Access Violation)


这不是IOCP特有的问题,却是编码设计所带来的结果。在IOCP的API函数调用中,要为它的参数传递一个指向客户端特定数据的结构体(CItentContext)的指针。倘若一个客户端连接丢失,并且释放该客户端数据结构体所占用的内存,当该客户端之前所执行的一些I/O调用返回了错误码,此时需要试图去访问或删除这个客户端结构体时,一个访问紊乱就发生了。为了避免该现象的产生,为客户端结构体增加一个阻寒I/O调用的计数,只有当计数为零,即不存在阻塞I/O调用时,才删除这个结构体。

三、基于IOCP的服务器开发应用实例

    3.1 IOCP的主要API函数介绍和数据结构的设计

    3.1.1 CreateIocompletionPort函数


该函数主要有两个作用:一是“创建”一个完成端口,二是将一个Socket句柄与已经创建的完成端口句柄“绑定”,绑定之后,基于该Socket的收、发、断开等事件都可以被完成端口感知。

    3.1.2 GetQueuedCompletionstatus函数


若干的工作者线程通过循环调用该函数从IOCP消息队列取得完成通知,并通过其参数来传递“单I/O操作数据”和“单句柄数据”等信息。

    3.1.3 PostQueuedcompletionstatus函数


该函数允许应用程序将自定义的专用I/O完成包进行排队,作用相当于一个异步I/O调用:一般情况下是通过直接调用WSASEND/WSARECV函数来进行实际的异步调用。但为了公平地分配CPU,当应用程序想要执行一个I/O调用操作时,它并不直接去做,而是将其发送到IOCP,这些操作被I/O工作者线程执行。该函数也可用于线程的终止退出:在主线程中调用它给IOCP的每个线程都发送一个特殊的I/O完成包通知线程结束,那么关联在该IOCP上的工作者线程可以获得此包,并结束自己。

    3.1.4 OVERLAPPED结构


该结构和其他与I/O操作关联在一起的某些营要数据元素封装在一个类中。要重点强调的是,这个数据要被加锁,并且不可超出物理内存页,具体定义如下:


3.1.5 ClientContext结构


监听线程中,一旦有新的客户连接发生时,将给其分配一个描述客户上下文信息的结构,一般包括客户端的套接字以及与该套接字相关的读写缓冲区等信息。具体定义如下:


3.2 IOCP的工作流程介绍


采用IOCP模型为服务器端应用程序的网络接口部分提供服务,主要有两种类型的线程:主线程和工作线程。主线程负责创建并监听套接字,创建工作线程,等待并接受到来的连接,将其关联到IOCP等。而工作线程则负责等待并处理在IOCP对象上完成的事件。对于工作者线程,它循环调用API函数从IOCP消息队列中取I/O完成包:IOCP释放一个等待中的线程来进行数据处理。如果目前没有线程正在等待,它不会产生新的线程,当执行中的线程处理完毕之前的I/O请求后才返回到完成端口进行等待。这样就保证了工作者线程总是保持一个稳定的数量。数据处理完毕后,根据需要发出异步I/O请求操作,然后继续下一次循环,处理未来的I/O请求。


因此,IOCP的总体执行过程为:主线程不断地循环接收客户端的请求,投递异步的I/O请求,操作系统完成实际的I/O处理后,把结果送到IOCP消息队列上,而等待着的工作者线程调用函数从IOCP不断地取出I/O操作结果,进行数据处理,然后根据需要再发出异步I/O请求,循环往复直至应用程序退出,具体的程序执行流程如图2所示。

图2:程序执行流程

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:343290次
    • 积分:4110
    • 等级:
    • 排名:第7771名
    • 原创:60篇
    • 转载:115篇
    • 译文:0篇
    • 评论:25条
    最新评论