6.6.2 常见的并发网络服务程序设计方案(3)
方案4 这是对方案2 的优化,[UNP] 详细分析了它的几种变化。方案3 和方案4 这两个方案都是Apache httpd 长期使用的方案。
以上几种方案都是阻塞式网络编程(www.cppentry.com),程序流程(thread of control)通常阻塞在read() 上,等待数据到达。但是TCP 是个全双工协议,同时支持read() 和write()操作,当一个线程/进程阻塞在read() 上,但程序又想给这个TCP 连接发数据,那该怎么办?比如说echo client,既要从stdin 读,又要从网络读,当程序正在阻塞地读网络的时候,如何处理键盘输入?
又比如proxy,既要把连接a 收到的数据发给连接b,又要把从b 收到的数据发给a,那么到底读哪个?(proxy 是附录A 讲的三大TCP 网络编程(www.cppentry.com)案例之一。)
一种方法是用两个线程/进程,一个负责读,一个负责写。[UNP] 也在实现echoclient 时介绍了这种方案。§7.13 举了一个Python 双线程TCP relay 的例子,另外见Python Pinhole 的代码:http://code.activestate.com/recipes/114642/。
另一种方法是使用IO multiplexing,也就是select/poll/epoll/kqueue 这一系列的“多路选择器”,让一个thread of control 能处理多个连接。“IO 复用”其实复用的不是IO 连接,而是复用线程。使用select/poll 几乎肯定要配合non-blockingIO,而使用non-blocking IO 肯定要使用应用层buffer,原因见§7.4 。这就不是一件轻松的事儿了,如果每个程序都去搞一套自己的IO multiplexing 机制(本质是event-driven 事件驱动),这是一种很大的浪费。感谢Doug Schmidt 为我们总结出了Reactor 模式,让event-driven 网络编程(www.cppentry.com)有章可循。继而出现了一些通用的Reactor框架/库,比如libevent、muduo、Netty、twisted、POE 等等。有了这些库,我想基本不用去编写阻塞式的网络程序了(特殊情况除外,比如proxy 流量限制)。
这里先用一小段Python 代码简要地回顾“以IO multiplexing 方式实现并发echo server”的基本做法29。为了简单起见,以下代码并没有开启non-blocking,也没有考虑数据发送不完整(L28)等情况。首先定义一个从文件描述符到socket 对象的映射(L14),程序的主体是一个事件循环(L15~L32),每当有IO 事件发生时,就针对不同的文件描述符(fileno)执行不同的操作(L16, L17)。对于listening fd,接受(accept)新连接,并注册到IO 事件关注列表(watch list),然后把连接添加到connections 字典中(L18~L23)。对于客户连接,则读取并回显数据,并处理连接的关闭(L24~L32)。对于echo 服务而言,真正的业务逻辑只有L28:将收到的数据原样发回客户端。
- recipes/python/echo-poll.py
- 6 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 7 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- 8 server_socket.bind(('', 2007))
- 9 server_socket.listen(5)
- 10 # server_socket.setblocking(0)
- 11 poll = select.poll() # epoll() should work the same
- 12 poll.register(server_socket.fileno(), select.POLLIN)
- 13
- 14 connections = {}
- 15 while True:
- 16 events = poll.poll(10000) # 10 seconds
- 17 for fileno, event in events:
- 18 if fileno == server_socket.fileno():
- 19 (client_socket, client_address) = server_socket.accept()
- 20 print "got connection from", client_address
- 21 # client_socket.setblocking(0)
- 22 poll.register(client_socket.fileno(), select.POLLIN)
- 23 connections[client_socket.fileno()] = client_socket
- 24 elif event & select.POLLIN:
- 25 client_socket = connections[fileno]
- 26 data = client_socket.recv(4096)
- 27 if data:
- 28 client_socket.send(data) # sendall() partial
- 29 else:
- 30 poll.unregister(fileno)
- 31 client_socket.close()
- 32 del connections[fileno]
- recipes/python/echo-poll.py
注意以上代码不是功能完善的IO multiplexing 范本,它没有考虑错误处理,也没有实现定时功能,而且只适合侦听(listen)一个端口的网络服务程序。如果需要侦听多个端口,或者要同时扮演客户端,那么代码的结构需要推倒重来。
这个代码骨架可用于实现多种TCP 服务器。例如写一个聊天服务只需改动3 行代码,如下所示。业务逻辑是L28~L30:将本连接收到的数据转发给其他客户连接。
- $ diff echo-poll.py chat-poll.py -U4
- --- echo-poll.py 2012-08-20 08:50:49.000000000 +0800
- +++ chat-poll.py 2012-08-20 08:50:49.000000000 +0800
- 23 elif event & select.POLLIN:
- 24 clientsocket = connections[fileno]
- 25 data = clientsocket.recv(4096)
- 26 if data:
- 27 - clientsocket.send(data) # sendall() partial
- 28 + for (fd, othersocket) in connections.iteritems():
- 29 + if othersocket != clientsocket:
- 30 + othersocket.send(data) # sendall() partial
- 31 else:
- 32 poll.unregister(fileno)
- 33 clientsocket.close()
- 34 del connections[fileno]
但是这种把业务逻辑隐藏在一个大循环中的做法其实不利于将来功能的扩展,我们能不能设法把业务逻辑抽取出来,与网络基础代码分离呢?