6.6.2 常见的并发网络服务程序设计方案(2)
L6~L13 是echo 服务的“业务逻辑循环”,从L21~L24 可以看出它一次只能服务一个客户连接。后面列举的方案都是在保持这个循环的功能不变的情况下,设法能高效地同时服务多个客户端。L9 代码值得商榷,或许应该用sendall() 函数,以确保完整地发回数据。
方案1 这是传统的Unix 并发网络编程(www.cppentry.com)方案,[UNP] 称之为child-per-client 或fork()-per-client,另外也俗称process-per-connection。这种方案适合并发连接数不大的情况。至今仍有一些网络服务程序用这种方式实现,比如PostgreSQL 和Perforce的服务端。这种方案适合“计算响应的工作量远大于fork() 的开销”这种情况,比如数据库服务器。这种方案适合长连接,但不太适合短连接,因为fork() 开销大于求解Sudoku 的用时。
Python 示例如下,注意其中L9~L16 正是前面的业务逻辑循环,self.request 代替了前面的client_socket。ForkingTCPServer 会对每个客户连接新建一个子进程,在子进程中调用EchoHandler.handle(),从而同时服务多个客户端。在这种编程(www.cppentry.com)方式中,业务逻辑已经初步从网络框架分离出来,但是仍然和IO 紧密结合。
- recipes/python/echo-fork.py
- 1 #!/usr/bin/python
- 2
- 3 from SocketServer import BaseRequestHandler, TCPServer
- 4 from SocketServer import ForkingTCPServer, ThreadingTCPServer
- 5
- 6 class EchoHandler(BaseRequestHandler):
- 7 def handle(self):
- 8 print "got connection from", self.client_address
- 9 while True:
- 10 data = self.request.recv(4096)
- 11 if data:
- 12 sent = self.request.send(data) # sendall
- 13 else:
- 14 print "disconnect", self.client_address
- 15 self.request.close()
- 16 break
- 17
- 18 if __name__ == "__main__":
- 19 listen_address = ("0.0.0.0", 2007)
- 20 server = ForkingTCPServer(listen_address, EchoHandler)
- 21 server.serve_forever()
- recipes/python/echo-fork.py
方案2 这是传统的Java 网络编程(www.cppentry.com)方案thread-per-connection,在Java 1.4 引入NIO 之前,Java 网络服务多采用这种方案。它的初始化开销比方案1 要小很多,但与求解Sudoku 的用时差不多,仍然不适合短连接服务。这种方案的伸缩性受到线程数的限制,一两百个还行,几千个的话对操作系统的scheduler 恐怕是个不小的负担。
Python 示例如下,只改动了一行代码。ThreadingTCPServer 会对每个客户连接新建一个线程,在该线程中调用EchoHandler.handle()。
- $ diff -U2 echo-fork.py echo-thread.py
- if __name__ == "__main__":
- listen_address = ("0.0.0.0", 2007)
- - server = ForkingTCPServer(listen_address, EchoHandler)
- + server = ThreadingTCPServer(listen_address, EchoHandler)
- server.serve_forever()
这里再次体现了将“并发策略”与业务逻辑(EchoHandler.handle())分离的思路。用同样的思路重写方案0 的代码,可得到:- $ diff -U2 echo-fork.py echo-single.py
- if __name__ == "__main__":
- listen_address = ("0.0.0.0", 2007)
- - server = ForkingTCPServer(listen_address, EchoHandler)
- + server = TCPServer(listen_address, EchoHandler)
- server.serve_forever()
方案3 这是针对方案1 的优化,[UNP] 详细分析了几种变化,包括对accept(2)“惊群”问题(thundering herd)的考虑。