浅谈windows下高性能服务器程序模型IOCP
2016-04-07梁金晶姚宏
梁金晶 姚宏
摘要:本文讨论在Windows 环境下,如何开发出一种基于TCP协议的高并发服务器程序。本文谈论Windows 下套接字I/O模型,同步和异步I/O,阻塞和非阻塞模式,主要谈论IOCP通讯模型的原理和开发过程。
关键词:高并发;服务器端程序开发;IOCP
中图分类号:TP393 文献标识码:A 文章编号:1009-3044(2016)03-0257-02
1 概述
当前,移动互联网已成为了人们主要的获取信息的主要渠道。在移动互联网上主要有两种方式获取信息。 一是通过各种浏览器来浏览网站内容。二是通过手机上安装的app来获得来自服务器上的数据,如QQ和微信。这两种通讯方式都需要服务器端程序的支持。而衡量服务器端程序性能的一个重要指标就是并发量,也就是同时可以接入客户端的数量,本文讨论如何在windows系统下最性能的服务器端程序模型完成端口模型。
2 具体内容
首先,我们讲到同步和异步两个概念。
同步模式是比较早期的模式,在Windows套接字创建时,默认工作在同步模式下。同步模式就是需要服务器完成收发后才可以做下一步的操作,这样会大大降低程序的运行性能。所以高性能的服务器端程序多数使用异步模式。异步模式是指服务器程序不等当前I/O是否完成,就马上执行下一个任务,并不等待程序执行结果或程序本身出现错误退出。这样就大大提高了程序性能。举个例子来说,一个公司的物流公司的经理给一个快递员下达快递任务后,等待这个快递员送完快递后再给第二个快递员下达新任务。如果需要用1分钟的时间把下单的内容通知给快递员,且第一个快递员用时完成任务1小时,第二个快递员也用时1小时完成任务,则一共用时2小时2分钟,这样的模式就是同步模式。如果经理给第一个快递员下完任务后不等待快递员执行任务的完成就直接给下一个快递员下达第二个任务,这时用时就只有2分钟,仅仅是下单内容通知给快递员的时间,这样的模式就是异步模式。很明显,采用异步模式的程序大大提高了性能。
第二,我们讲到阻塞和非阻塞两个概念
阻塞模式就是指本线程结果返回之前,线程会被挂起该线程进入非可执行状态,CPU不给线程分配时间片,即线程暂停运行,进入待机状态。函数只会有执行结果或者出错码生成以后才会做下一步的操作。而非阻塞模式指的是不等待执行结果或出错码的出现不会直接进入睡眠状态而是马上执行下一步的操作,此时线程本身还是处在激活状态。同样用上面经理-快递员的例子来说明,阻塞模式是指经理在下达指令后一直等待快递员返回执行情况。在此期间不做任何事情,直到快递员返回唤醒经理,再进行下一个命令。而非阻塞模式指的是经理下达指令后在等待返回执行情况期间可以做其他事情,比例不停打电话给快递员询问执行情况等。
第三,我们来讲到IOCP的工作原理和编写过程。
IOCP是Windows下高性能的可伸展的I/O模型,是最高性能的模型,与其性能相当的模型都没有。IOCP全称Input/Output Completion Port,译为I/O完成端口。首先我们来说为什么Windows下要使用该模型。我们知道在网络上客户端与服务器发送和接收数据(即网络I/O操作)相对于CPU而言要花费大的时间,两者的差距可能有数几千倍之多。显然如果要CPU和网络I/O操作采用同步模式的话即让CPU等待网络I/O完成收发数据之后再做下一步的操作,对于整个系统来说是一种巨大的浪费。所以为了提高效率,让系统不等待结果而是直接做下一条操作,只是让I/O操作完成之后再通过回调函数来通知系统收发执行情况。这也是为什么叫做完成端口的原因。IOCP 其实上是一个线程池。这个线程池的核心工作是去调用I/O操作完成时的回调函数。IOCP可以想象为一个包含网络通信操作即接收新客户端的加入,发送数据到客户端,接收客户端上交的数据的一个信息队列,它会把上述网络操作即接收客户端接入请求,发送数据和接收数据操作执行的结果存放在这个队列里。我们可以读出所有的网络操作的结果以及出错码。
具体实现过程:
第一步:创建一个完成端口。使用HANDLE 完成端口 = CreateIoCompletionPort(参数1,参数2,参数3,参数4);参数4代表的是程序可以同时生成线程的最大数量。人们很自然地想到在一个CPU上运行的线程数量是越多越好。但是如果线程太多的话,系统要在不同的线程上不断地切换上下文,这样不但不能加大CPU的能力,反而会降低CPU的实际使用效率。所以我在这里只是设置为零,也就是说我们只需要让一个CPU上运行一个线程就好。
第二步:创建n个I/O操作线程。业内一般控制n的数量为CPU数量的2倍加2,如果是4核CPU,就是4*2+2个即创建10个I/O操作线程。因为在网络上接收客户端数据和发送数据到客户端都是比较需要时间的事情,而在CPU上的线程操作是比较高速的,两者的差距可能是网络I/O操作用时几千ms而CPU一个线程只是用几个ms而已,所以只需要几个I/O操作线程就可以满足一台服务器上几万个客户端的accept,send,rec执行操作。
I/O操作线程用来处理上述讲到的接收客户端接入请求,发送数据和接收数据操作,也是处理我们I/O网络事件的线程。那么我们就要首先获取系统中CPU的数量以便确定I/O操作线程的具体数量。
使用SYSTEM_INFO, GetSystemInfo来取得Windows系统信息,在GetSystemInfo中的就有CPU的核心数量,然后如上所述生成的I/O操作线程为CPU核心数量的两倍加二。
然后:把用于监听的本地套接字绑定到完成端口上面并在程序设置的网络端口上开始监听。绑定方法为使用CreateIoCompletionPort()函数。注意这里和创建完成端口的WindowsAPI完全相同。
第三步:I/O操作线程不停地取出已有的发送数据和接收数据的结果,这些结果都存放在完成端口上,取出这些结果的函数是阻塞函数,也就是说不会被打断,除非出错。
第四步:程序的主线程里只做等待客户端的套接字连接的工作。这里可以使用Accpet()和AcceptEx()两个函数,AcceptEx是微软专门在Windows操作系统里面提供的扩展函数,使用重叠I/O机制。在Windows里的异步I/O操作都要使用重叠I/O机制。重叠I/O机制可以简单地理解为含有这种机制的操作方法和系统里别的操作是异步的,不需要和别的操作有任何联系。
扩展函数AcceptEx()的定义
BOOL AcceptEx (
生成套接字 参数1, //生成用于本地监听的套接字
生成套接字 参数2, //用于接受连接的socket,事先建好的,有客户端连接进来时直接把这个Socket拿给它用的那个,是AcceptEx高性能的关键所在。一般来说,根据业务的多少来确定事先准备socket的数理。
PVOID 参数3, //这个为缓冲区,包含了三个信息:一是客户端发来的第一组数据,二是服务器的地址,三是客户端的地址。
DWORD参数4, //参数3中用于存放数据的空间大小。如果为零,则Accept时将不会等待数据到来,而直接返回,如果此参数不为0,那么一定得等接收到数据了才会返回。
DWORD参数5, //服务器地址信息空间的大小。
DWORD参数6, //接入客户端地址的信息空间大小。
LPDWORD 参数7, //可不管理
LPOVERLAPPED 参数8 //重叠I/O操作所要用到的重叠结构);
第五步:这时主线程和I/O操作线程开始分工工作,主线程处理新接入的客户端工作,就是接入连接和安排到完成端口中去。使用accept()和acceptEx()函数接入新的客户端,使用CreateIoCompletionPort()安排到已有的完成端口中去,同时可以在对新接入的客户端进行读写操作。
主线程一直在等待新的客户端的接入,这里采用的是阻塞模式,I/O操作线程则一直在做网络I/O操作,I/O操作线程采用的是异步非阻塞模式,只是给操作系统发命令,让操作系统去完成数据的收发工作,I/O操作线程这时并不等待系统是否完成收发工作,而是只不停的发命令,就好像上面提到的快递员-经理模式一样,这里的I/O操作线程就是经理,而做具体工作的操作系统就是快递员。经理只发命令,不监督快递员的具体工作,只是关心工作执行的结果。
在操作系统完成收发数据后,把结果发送到完成端口上来,这样完成端口上就有了所有已有客户端的收发数据执行结果。
而I/O操作线程只要读出结果就可以了,以此循环。
简单地概括完成端口模型为:
完成端口由三个主体构成,第一主线程,第二I/O操作线程,第三操作系统。首先由主线程生成一个完成端口,初始化监听套接字和生成两倍CPU数量加2个的I/O操作线程,之后主线程的主要工作是接入新的客户端。工作线程负责命令操作系统去接收和发送数据,操作系统负责具体的收发数据工作,并把执行结果发送到完成端口上。I/O操作线程不断的读取完成端口上的收发执行结果,以确定下一步的操作,以此循环。
最后提到关闭完成端口,由于I/O操作线程是采用阻塞模式一直不停地读取操作系统放在完成端口上的执行结果,所以没有新的收发数据工作执行结果,完成端口则进入死循环中不能退出,所有我们要采用发信息的方式来使完成端口关闭。
操作系统发送完成端口退出信息的API为PostQueuedCompletionStatus,实际上就是发送一个特定的网络事件给I/O操作线程,让完成端口自行退出。
代码:
for (int i = 0; i < I/O操作线程数量; i++)
{ PostQueuedCompletionStatus(完成端口, 0, (DWORD) NULL, NULL); }
参考文献:
[1] 王艳平,张越.Windows 网络与通信程序设计[M].人民邮电出版社,2013.
[2] 张越.Visual C++ 网络程序设计实例详解[M].人民邮电出版社,2013.
[3] Jeffrey Richter.Windows 核心编程 Windows via C/C++[M].清华大学出版社,2011.
[4] 徐磊,腾婧,张莹.Windows Sockets网络编程[M].机械工业出版社出版,2012.