针对服务器处理网络连接的几种方式,unix网络编程里给出了9种方案,并且对服务器进程/线程的开销做了一个量化的比较。从个人经验出发,觉得以下几种方式是比较实用的:
1.最简单的是堵塞Accept,收到连接后fork进程(unix)或创建Thread.原进程/线程继续堵塞Accept,创出来的进程线程只处理新连接上的客户请求。如果忽略创建进程/线程的开销,以及每个连接必须对应一个进程/线程的话,做成这样已经可以满足绝大部分简单的应用服务器需要了。qmail的tcpserver用的也是这种模式。要注意的是,如果子进程不监听连接,最好关掉继承自父进程的原来listen的socket.由于fork的开销比较大,如果对服务器的响应非常苛求,这种方式还是不实用的。另外,fork一个单线程的进程,会比fork一个多线程的进程要快的多。
2.引入进程/线程池概念,也就是先创建一堆进程/线程,让这些进程/线程来处理连接事件和客户端业务。在多线程支持的系统里,实现线程池还是很容易的,只让一个线程在accept上堵塞,收到连接后,唤醒一个本来堵塞在某个互斥量上的线程来处理之或将其放入队列等待处理。这样处理客户端连接的线程可以无限多(系统容量范围内)。但在进程模式里,因为除非使用fork让子进程继承连接句柄,否则跨进程处理accept返回的连接。(某些unix可以实现进程间传递句柄,win32也有进程复制句柄的做法,但这跟操作系统结合太紧,而且不是所有操作系统都支持,忽略之)
vcbear.mblogger.cn
3.解决上述进程问题就是让所有进程都能监听同一个端口并accept连接,可以通过一个进程创建了listen socket,并fork同样进行listen/accept的子进程来实现。这里要考虑惊群效应,也就是说有连接到来,所有堵塞在监听socket上的进程可能都会被系统唤醒,但只有一个进程能够从tcp协议栈里获得连接。惊群效应引入了进程调度开销,解决办法是在accept上加锁。同时只有一个进程在调用accept.在获得连接后,该进程不再调用Accept,而是处理客户端业务,直到客户端退出,才重新回归调用accept。Apache1.3.x使用的是类似的模式,可以从其http_main.c里看到。
典型代码如下
while(true)
{
mutex_lock
newcliet=accept(listener)
mutex_unlock
while(true)
{
/*处理客户端任务直到客户端断开*/
}
}
4.以上都只考虑了一个进程/线程对应一个连接的情况,当有大量的连接时,就可能会产生大量的线程。使用select可以让一个线程/进程处理多个连接。如下代码
if( select( .... ) ) > 0 )/*>0表示select集合里有事件发生*/
{
/*依次检查各有效的socket*/
for()
{
if(FD_ISSET( sock... )/**/
{
if(sock == ServerSocket){ /*是监听socket,调用accept,得到新socket,并加到本进程的select集合里*/ }
else{/*其他socket*/}
}
}
}
结合3的模式,比如创建10个都能监听的进程,每个进程最多处理10个客户端连接,那么进程/连接数比就降低了10倍。但这种情况下就不能对监听socket加锁,无法避免惊群问题。可以看到,在select后的for,同样可能占用不少cpu,比起系统的进程调度可能是有过之而无不及。在必须用单进程处理多个连接的case里,是可以考虑这样实现的。PS:unix网络编程里提到,如果有多个进程需要堵塞在同一个socket上,那么堵塞在accept上比堵塞在select上要好。
5:说到select,在合适的地方使用select还是不错的,尤其是读socket,使用select可以有效的实现可超时的堵塞方式,而不是永久性堵塞。在网络编程里冒把进程/线程堵塞致死的风险是很不应该的。所以最好把socket设置成非堵塞的,这样读函数可以立刻返回,读到数据或产生错误,错误码EAGAIN/EINTR/EINVAL表示连接应该没有断开,可以继续使用。
6:在windows上可以采用IOCP的做法(参见N年前翻译的文档:http://blog.csdn.net/vcbear/archive/2001/08/29/5987.aspx)。用单进程处理多个连接,让操作系统去操心网络上的事件,并挑出来是哪个连接上产生了IO。这样把任务调度的细节放到了操作系统内核,避免了在应用层上的进程/线程开销。据说LINUX/UNIX上有类似的EPOLL,怀疑Apache2.0采用了这个技术,还没有来得及研究。但如果通用代码方案已经可以满足要求,我觉得应该尽量避免使用和操作系统极度相关的代码,比如WIN32的IOCP。
7:考虑服务器上的进程调度,进程限制,信息共享是比较精细的事情。一般的实现就是做个共享内存公告板,进程竞争的读写公告板上的信息,报告自己的状态或获取其他信息。apache在这一块是做的很漂亮的,它可以根据连接的忙闲程度,由一个主进程来创建更多处理进程或控制空闲进程退出。过多的进程/线程存在还是会影响系统效率的(量化计算参考unix网络编程卷一的第27章)
8:一点注意:服务器编程一定要设置linger,否则客户端主动socket关闭的时候,服务器会持续2*TIME_WAIT的时间才真正断开,造成大量的废连接负担。
rLinger.l_onoff = 1; // 打开linegr开关
rLinger.l_linger = 0; // 设置延迟时间为 0 秒, 注意 TCPIP立即关闭,但是有可能出现化身
setsockopt(nSockfd, SOL_SOCKET, SO_LINGER, (char *)&rLinger, sizeof(rLinger)))
另外一个选项SO_REUSEADDR也是server socket必须设置的。
无论采用什么模式来编写服务器,需要关注的问题除了网络事件响应,进程/线程调度之外,还有服务器本身的容量问题。最短的木板其实并不在于处理网络连接的模型本身,而是服务器上的数据处理能力或网络带宽,如果一台服务器及其网络带宽受理100个客户端的业务就已经很吃力了(比如ftp,mud,p2p,其他应用服务...),那么其socket服务器就算能轻松处理10000个连接也没有意义了。