摘要:单线程网络服务器是linux系统中一个新的服务器设计模式,有良好的性能表现,本文在分析现有linux网络服务器开发模式的基础上,指出了单线程网络服务器的优点,然后给出了单线程网络服务器的设计框架,最后对其中的关键技术进行了阐述。 

一、引言 
linux以其良好的稳定性被广泛地作为服务器程序的运行平台,特别是现在众多的网络服务器,比如apache、openvpn等,都运行于linux平台之上。与windows系统相比,linux系统具有更高的稳定性和可扩展性,因此,linux已成为网络服务器开发的首选平台。 
与一般程序不同,网络服务器程序要求高的稳定性和一定的性能,而传统的多线程的开发方式存在众多缺点,总结起来主要有如下: 
首先:大用户量时,线程切换开销大,服务器性能严重下降; 
其次:线程间共享变量容易冲突,影响稳定性,如果变量锁太多,又影响服务器的处理性能; 
最后:linux原生不支持多线程,操作系统级对线程支持不好。 
因此,采用多线程的方法很难开发出高性能稳定的服务器,本文分析了除多线程外的几种网络服务器程序设计模式,并详细说明了单线程网络服务器程序的开发设计方法。 
二、linux网络服务器常见设计模式分析 

除了多线程模式外,linux网络服务器还有线程池、多进程、prefork子进程,阻塞迭代、单线程等。

一)线程池技术由服务器进程维护一个线程池,在这个池里预先创建了若干个线程,当一个用户连接接入时,就从线程池里取出一个线程进行工作,当连接关闭时,就将该线程放回到连接池中。这种方式减少了线程初始化的时间,加快了连接接入的速度,但是该方式并没有真正解决多线程程序线程切换、共享变量带来的性能问题。 
(二)多进程技术是为每个用户连接创建一个进程,进程间不像多线程那样连接紧密,其共享变量的方法一般通过共享内存等机制,该方式减少了共享变量带来的开销,但是大量用户在线时,其性能依然较低。 
(三)prefork子进程模式是由主进程预先创建一些子进程,其采用某个算法,根据用户量动态地有准备地创建多个子进程,常用的apache服务器便是采用该种方式,该方式节约了进程创建时间,但是当大量用户连接存在时,其进程切换开销也是惊人的。 
(四)阻塞迭代服务器中只存在一个进程,该进程处理完一个请求后,再处理另外一个,该方式适用于少量用户接入的情况,当处理一个用户时间比较长时,可能耽误其它用户的处理,一般网络服务器很少采用该方式。 
(五)单线程服务器与迭代服务器类似,也是由一个进程处理所有请求,该进程只有一个主线程在运行,并不创建其它的线程,nginx、lighttpd等服务器便是采用的这种方式,该方式由一个程序来模拟cpu的切换,减少了大量用户访问时的切换开销,在处理io时采用非阻塞的方式,防止了某个连接处理超时而耽误其它用户连接的处理。 
单线程服务器开销小,能够支持大量用户在线,是一种比较好的方式。然而该方式需要保存每个用户处理现场和精确的用户处理时间片控制,对程序设计实现要求较高,下面重点说明如何设计实现单线程网络服务器。 
三、单线程网络服务器的设计 
一个基本的单线程网络服务器模块如图所示,主要由初始化模块、主循环模块、定时处理模块、连接接入处理模块、连接业务处理模块、读处理模块、写处理模块、断开处理模块等构成。 

图1单线程网络服务器模块结构 
初始化模块负责程序的一些初始化工作,比如配置文件的读取、监听socket的创建、必要参数的设置等。 
主循环模块是一个死循环处理,其负责监听每一个socket事件,当有事件发生时,调用相关socket的处理函数进行处理,处理的事件包括定时事件、连接接入事件、和连接读、写、断开等事件,这些事件分别由不同的模块来处理。 
定时处理模块负责处理定时触发的事件,一般1秒执行一次,负责对所有用户连接进行检查或进行一些定期的操作,主要检查的项目包括网络超时、会话超时等,定期执行操作的包括处理状态输出、垃圾回收等。 
连接接入处理模块负责对用户接入事件进行响应,接收用户的连接,并进行相应的处理,比如为用户连接分配必要的处理资源,以及将该连接加入到监听队列里。 
连接业务处理模块负责处理用户连接socket上所发生的一切事件的处理,主要的事件包括读、写、断开,这三个事件分别由对应的三个子模块进行处理。其中需要注意的是读、写处理模块均需要为非阻塞操作,否则会影响程序为多个用户处理的时间。 
四、单线程网络服务器的关键技术 
(一)非阻塞io 
在单线程服务器中,不能由于某个连接的处理而耽误其它连接的处理,因此,当一个连接的io需要等待时,就需要去处理其它连接,而不能“死等”。非阻塞io在当前数据无法处理完毕时直接返回,而不需要调用者等待其处理完毕,节约了处理时间。 
在linux系统中,操作系统为每个socket都分配了一个读缓冲和一个写缓冲,当执行read操作时,会将内核空间读缓冲中的内容复制到用户空间,当执行write操作时,会将用户空间的内容复制到内核写缓冲中,再由操作系统发送出去。如果内核读缓冲中的内容比较少,在阻塞的情况下,read函数会一直等待操作系统从网络读取,在非阻塞的情况下,就不会等待,因此如果一个数据分组没有读取完毕,就必须将已读数据时行缓存,等到下次有数据到达时再进行读取。对于write函数来说也是一样,如果内核写缓冲中的待写数据太多,就必须对待写数据进行缓存,当缓存可写时再进行写入。

(二)epoll技术 
在非阻塞io情况下,必须采用异步事件处理的方式,在主循环模块中,常用的做法是采用select函数来监听当前所有连接事件,当有事件发生时,select便返回当前发生的事件。然而select方式最大的问题在于,当有事件发生时,必须采用轮循的方式来判断是哪个连接触发了事件,这样在大量用户连接时,将会降低处理速度,另外一点是select一般最大能够监听的连接数只有1024个,对于同时需要大量连接处理的任务来说,是无法满足的。 
从linux2.4内核开始支持了epoll技术,该技术允许当一个事件发生时,发生事件的socket将会被返回,这样,程序能够立即判断出来是哪个连接发生了事件,节约了大量的时间,而且epoll允许最大监听的连接数与操作系统本身所能够支持的最大连接数一致,因此,epool特别适用于同时处理大量大线连接的网络服务器。 
五、结论 
单线程网络服务器是一种新的服务器设计模式,相对于其它服务器开发模式,其具有良好的性能,特别是对于大量用户连接同时在线的网络服务器来说,单线程模式是非常适合的。