Skip to menu

服务器框架

写socket server程序, 老3步:create/bind/listen,然后就用accept等待客户端连接,代码如下


 
  1. import sys
  2. import socket
  3. import select
  4. import os
  5.  
  6. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  7. #方便调试:让端口释放后立即就可以被再次使用,否则要等2分钟
  8. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  9.  
  10. try:
  11.     s.bind(('localhost',9000) )
  12.     s.listen(1)
  13. except Exception, e:
  14.     raise e
  15.  
  16. while 1:
  17.     client,address = s.accept()
  18.     print "%s get a client[%s] from %s" % (os.getpid(),str(client),address)
  19.     client.close()

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


 
  1. import sys
  2. import socket
  3. import select
  4. import os
  5.  
  6. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  7. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  8. try:
  9.     s.bind(('localhost',9000) )
  10.     s.listen(1)
  11. except Exception, e:
  12.     raise e
  13.  
  14. for i in range(1,10):
  15.     pid = os.fork()
  16.     if pid <0:
  17.         print 'fork error'
  18.         sys.exit(-1)
  19.     elif pid >0:
  20.         print 'fork process %d'  % pid
  21.     else:
  22.         pass
  23.  
  24. while 1:
  25.     client,address = s.accept()
  26.     print "%s get a client[%s] from %s" % (os.getpid(),str(client),address)
  27.     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确保每次只唤醒一个进程,如果你还要看惊群效果,可以如下操作


 
  1. s.setblocking(0)
  2.  
  3. _r = [s]
  4. _w = []
  5.  
  6. while 1:
  7.     reads,writes,errs = select.select(_r,_w,[])
  8.     for sock in reads:
  9.         if sock == s:
  10.             try:
  11.                 client,address = sock.accept()
  12.                 print "%s get a client[%s] from %s" % (os.getpid(),str(client),address)
  13.                 client.send("goodluck!")
  14.                 client.close()
  15.             except Exception,e:
  16.                 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

 

向上