再读 python 标准库 SocketServer.py 的收获

去年,一个小项目中用到了 SocketServer 库,为了更好的使用它,阅读学习了它的代码实现
当时的我正经使用 python 开发程序只有三个月左右,还是一个 python 新手,在那之前,我是一个 linux c 程序员。
接触到动态语言,一切都是新鲜的,开发速度很快,但是也有疑惑。当时学习了 SocketServer 的源码实现后,对动态语言的概念
有了更进一步的理解。虽然加上注释它的源码只有 700 行左右,但是对于当时的我来说,理解所有内容还是有点吃力,记得是花了几个晚上断断续续的看完的,
对于它的设计思想与实现方式还是感觉不够清晰,一年过去了,今晚突然想起来这个事情,于是又看了一遍,做了些实验,也把它的实现清楚的看明白了。时隔一年,
原来需要看几个晚上才能看的模模糊糊的东西,现在一会儿就看完了,还写了测试代码,对自己的想法进行了验证。

本文主要是记录一下实现一个 tcpserver 微框架的基本元素的构造方法。

实现一个 tcpserver,主要分为两部分:

  1. server 自身,处理实现一个 tcpserver 的一切细节;
  2. 业务处理模块,能够根据接收到的数据,分发给具体的业务逻辑代码进行处理;

从直观上来说,当 tcpserver 启动并处于 accept() 状体时,就可以接收客户端发送的数据,当收到了客户端发送的数据后,调用相应的业务处理逻辑代码
对收到的数据进行处理,处理完毕后,发送相应的处理结果给客户端。

这个过程是在整体逻辑上是没有疑问的,如果我们不用 SocketServer.py 这个库,改用 socket 包自己来实现服务器,我们的代码可能是类似于下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 仅作演示,实现一个最最简单的 tcpserver
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))
s.listen(backlog)
while True:
req, client_address = s.accept()
data = req.recv(1020) # 仅作演示,未考虑数据没有读取完毕的情况
do_ret = do_something_with_data(data) # 业务逻辑处理
req.send(do_ret)
req.close()
```
上面这段代码,建立了一个最简单除了展示但没什么用的 tcpserver,它接收客户端发送的数据,处理客户端的数据,将处理结果发送回客户端,然后关闭这个连接,等待下一个连接的到来。
这里也实现了一个 tcpserver,虽然没有异常处理,一次只能服务一个客户端并且是阻塞的,但是似乎也能用?并且展示了上面的 tcpserver 实现的两部分(服务器本身、业务逻辑处理)。
既然如此,为什么要用 SocketServer.py 提供的接口,而不是用上面这段代码呢?从设计和细节上,它们的差异在哪里?
差异点主要在以下几个方面:
1. 多路 io 复用,SocketServer.py 采用了 select.select() 这个最基本的多路 IO 复用方法,可以部分提高服务器的性能;
2. 实现了多线程与多进程服务器,可以根据需要采用多线程或多进程模式;
3. 业务逻辑与服务器逻辑实现的分离,通过将业务类传递给服务器来实现了业务逻辑与服务器逻辑的对接;
4. 更多的细节处理;
本次阅读的收获是更清楚的看明白了业务逻辑与服务器逻辑的结合与运行机制。
服务器的建立过程后,当有客户端连接并就绪后,accpet() 后的 对象,客户端代码,服务器本身 这三部分内容传递给业务类,
业务类是如何来沟通客户自己编写的代码的呢?
业务类(`BaseRequestHandler`)通过预定义接口,并且在类实例化时,就调用了它定义的所有方法,从而达到了将整个服务器逻辑串联起来的目的。
通过两部分代码:
```python
# BaseServer 中的定义
def finish_request(self, request, client_address):
"""Finish one request by instantiating RequestHandlerClass."""
self.RequestHandlerClass(request, client_address, self)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# BaseRequestHandler 的具体实现,这里通过 __init__() 方法,就调用了它定义的所有接口
class BaseRequestHandler:
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
pass
def handle(self):
pass
def finish(self):
pass
```
上面两部分代码展示了服务器实现与业务逻辑实现是如何结合起来的。这里在实例化 BaseRequestHandler() 类时,就会调用 `__init__`方法,
然后就调用了几个方法。而使用 SocketServer.py 时,也是要求将业务逻辑封装在继承 `BaseRequestHandler()` 类的类中,并定义自己的
`handle()` 方法来处理数据。由此,整个从框架到客户代码就构建结合起来了。
来源验证与前处理
在有些业务逻辑中,会对请求的客户端进行验证,验证通过后,才会有后续的业务逻辑代码执行,而在业务逻辑代码执行前,有可能还会有更进一步的
其他验证与处理,这些都在 SocketServer.py 中被考虑到了,通过 `verify_request` 方法,在 server 中做一些客户端验证,
而到了业务逻辑中,还有 `self.setup()` 这个方法进一步做一些处理后,再到 `self.handle()` 中来。这里的思想与 `tornado` 的 `RequestHandler` 中
设计的 `prepare()` 接口思想是类似的。
整体而言,如果要重写 server 中实现的方法,就继承对应的 server,重载对应的方法;
下面是一个客户端演示代码:
```python
#!/usr/bin/env python
#-*-coding:utf-8-*-
from SocketServer import TCPServer, BaseRequestHandler
class Test(BaseRequestHandler):
def handle(self):
print 'handle'
def setup(self):
print 'setup'
class TestServer(TCPServer):
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
TCPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)
def verify_request(self, request, client_address):
print 'verify_request'
print request
print client_address
return True
if __name__ == "__main__":
#t = TCPServer(('localhost', 9888), Test)
t = TestServer(('localhost', 9888), Test)
t.serve_forever()

SocketServer.py 是实现 tcpserver 较好的选择么?不是! tornado 才是,在有条件的情况下(比如 linux 环境),采用了更高级的 I/O 复用机制。

完成同样一件事情,我上面最简单的 tcpserver 可以干,SocketServer.py 库可以干, tornado 也可以干,一个比一个代码复杂。但是似乎,它们都干了同一件事情。
很多不懂程序开发或者假装不懂程序开发的 IT 从业者,往往爱把需求往简单里想,但是又要把验收结果往复杂里弄。对比过来就是,他们想象中的任务复杂度就是我上面
的 tcpserver 展示代码,他们期望的验收结果却是 tornado 实现的那些功能。这真是一个日了狗的世界。

完。