有好多年没有build server from scrash,一般都用现成的lib或者直接用nginx+php。学习网络服务器开发,首推两本书

最近突然有兴趣,研究了一番
写socket server程序, 老3步:create/bind/listen,然后就用accept等待客户端连接,代码如下

import sys
import socket
import select
import os

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#方便调试:让端口释放后立即就可以被再次使用,否则要等2分钟
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

try:
    s.bind(('localhost',9000) )
    s.listen(1)
except Exception, e:
    raise e

while 1:
    client,address = s.accept()
    print "%s get a client[%s] from %s" % (os.getpid(),str(client),address)
    client.close()

这段代码一次只能处理一个连接,要提高服务器的并发处理能力,有一个模式叫做:pre-fork,代码如下

import sys
import socket
import select
import os

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(('localhost',9000) )
    s.listen(1)
except Exception, e:
    raise e

for i in range(1,10):
    pid = os.fork()
    if pid <0:
        print 'fork error'
        sys.exit(-1)
    elif pid >0:
        print 'fork process %d'  % pid
    else:
        pass

while 1:
    client,address = s.accept()
    print "%s get a client[%s] from %s" % (os.getpid(),str(client),address)
    client.close()

一次启动10个子进程,监听同一个端口,所以prefork模式就是

create/bind/listen -> fork ->accept

这段代码非常可疑,多个进程accept了同一个socket,一般人都会认为可能会出错,但是linux从操作系统层面支持了这种做法。

Linux是这样实现accept调用的:

把当前进程插入这个fd的等待队列然后阻塞
当新连接进来的时候,操作系统会唤醒这个fd的等待队列的第一个进程,只唤醒一个进程
这是Linux kernel 2.4引入的功能. 相关论文:Accept scalability on Linux

prefork模式优点很多:

  • 没有锁,os来负责调度,效率很高
  • 单一进程,资源独占,对于这个用来accept的fd,随后可以随便操作,效果很好,因为子进程copy父进程全部的句柄,且copy且write(cow:copy on write)

 多线程能使用pre-fork么?

是的,当然可以!因为linux的常见的pthread 是通过进程实现的,一个thread对应一个内核轻量级进程。 N个thread accept同一个fd
一次也会只唤醒一个thread,不用自己写各种同步代码

惊群

惊群Thundering herd problem是指上述情况下,一个新连接唤醒了所有被accept阻塞的进程。

由于目前linux确保每次只唤醒一个进程,如果你还要看惊群效果,可以如下操作

s.setblocking(0)

_r = [s]
_w = []

while 1:
    reads,writes,errs = select.select(_r,_w,[])
    for sock in reads:
        if sock == s:
            try:
                client,address = sock.accept()
                print "%s get a client[%s] from %s" % (os.getpid(),str(client),address)
                client.send("goodluck!")
                client.close()
            except Exception,e:
                print '[%d]:%s' %(os.getpid(),str(e))

把socket设置为非阻塞模式,然后丢进select来等待可读信号到达,当新connection产生的时候,所以进程都会被唤醒。

但是随后调用用accept会出现异常,因为事实上产生了一个新连接,第一个进程accept可以成功返回,其他进程accept都会失败

pre-fork是一个重大的改善,极大的简化了网络server的编程,Linux可能会走得更远,Linux Kernel 3.9会引入一个新的socket option,只要设置socket的SO_REUSEPORT属性,那么不同的进程和线程都可以同时bind这个ip和port

本文地址: http://lutaf.com/216.htm 鲁塔弗原创文章,欢迎转载,请附带原文链接