简介

SocketServer是对底层的Socket进行封装,简化网络服务器编写工作的Python标准库。

源码地址:Lib/SocketServer.py

文档地址:20.17. SocketServer — A framework for network servers

基本socket编程

我们假设读者对socket编程已经有了基本的了解,否则请阅读官方文档

以TCP为例,基本的socket编程,server端的流程如下:

  1. 创建一个socket
  2. bind绑定地址和端口
  3. listen端口监听
  4. accept接受连接
  5. 然后通过recv和send来接收、发送数据
  6. close关闭连接

client端流程如下:

  1. 创建socket
  2. connect与服务端建立连接
  3. 连接建立后即可通过recv和send交互数据
  4. close关闭连接

server端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Echo server program
import socket

HOST = '' # Symbolic name meaning all available interfaces
PORT = 50007 # Arbitrary non-privileged port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)
conn, addr = s.accept()
print 'Connected by', addr
while 1:
data = conn.recv(1024)
if not data: break
conn.sendall(data)
conn.close()

client端:

1
2
3
4
5
6
7
8
9
10
11
# Echo client program
import socket

HOST = 'daring.cwi.nl' # The remote host
PORT = 50007 # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('Hello, world')
data = s.recv(1024)
s.close()
print 'Received', repr(data)

从上面的例子看出,直接使用socket编程的话,需要处理比较多的细节,并且扩展性不够好。

基于SocketServer编程

同样以TCP为例,server端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import SocketServer

class MyTCPHandler(SocketServer.StreamRequestHandler):

def handle(self):
# self.rfile is a file-like object created by the handler;
# we can now use e.g. readline() instead of raw recv() calls
self.data = self.rfile.readline().strip()
print "{} wrote:".format(self.client_address[0])
print self.data
# Likewise, self.wfile is a file-like object used to write back
# to the client
self.wfile.write(self.data.upper())

if __name__ == "__main__":
HOST, PORT = "localhost", 9999

# Create the server, binding to localhost on port 9999
server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)

# Activate the server; this will keep running until you
# interrupt the program with Ctrl-C
server.serve_forever()

从上面看出,利用SocketServer,有关于socket连接的细节都由其处理,我们只需要构造一个继承自StreamRequestHandler的类,override掉handle方法,我们更加专心于业务逻辑处理。

SocketServer源码结构

SocketServer.py文件中包含ServerRequestHandler两大类的class。

RequestHandler

RequestHandler比较简单,StreamRequestHandler(TCP)和DatagramRequestHandler(UDP)都继承自BaseRequestHandler

RequestHandler只有四个方法:

  1. __init__,初始化
  2. setup,构造一些协议相关的东西,比如用于读写的file
  3. handle,用于给用户override的方法,用于编写业务逻辑的地方
  4. finish,一些扫尾工作,如用于读写文件的关闭

源码分析

RequestHandler部分的源码比较简单,我们就用处理TCP连接的StreamRequestHandler为例来讲解,UDP的Handler和TCP大同小异,并且更加简单一些。

从671行开始看起,先定义了的rbufsizewbufsize分别表示读缓冲区大小和写缓冲区大小。其中disable_nagle_algorithm是用来控制是否启用nagle算法。nagle算法是为了解决网络中小数据传输的带宽浪费问题的。在TCP/IP协议中,哪怕只传输1个字节的数据,但只要是一个数据段,就要加上几十个字节的协议头,所以在传输小数据的时候,造成了大量的浪费。nagle算法的基本思想就是将数据进行缓存,当达到一定的大小时,再统一发送,这样可以大大提高网络利用率。但是提高网络利用率的同时,网络的实时性就降低了。对于某些交互性很强的应用程序来说是不允许的。默认情况下是启用了这个算法的。

在693行,setup方法中,赋的第一个变量self.connection表示用来交互的socket对象。它的值是从self.request给出来的。我们来追踪一下这个self.request。首先追踪到644行,在BaseRequestHandler__init__方法中,self.request是从类构造的参数request传进来的。那么我们就找这个类使用的地方。

从上面的示例代码可以看出,我们的RequestHandler是作为参数传进Server里面的,所以我们追到209行,看出来赋值给了Serverself.RequestHanlerClass,再看哪儿使用了这个变量,追到334行,在finish_request里传了那个request进来,并构造了一个RequestHandler的实例。那么这个request又是从321行的process_request传进来的,再追下去,传进来的request是在295行,这个值又是290行的get_request的返回值。get_request这个方法基类BaseServer没有定义,是由子类来实现的,我们到463行可以看出,get_request的返回值就是socket.accept()的返回值,即建立连接的socket对象。

由此我们可以看出,在SocketServer中的request其本质就是socket对象。

RequestHandler接下来就没有什么好讲的了,都很简单。

Server

从下图可以看出有关于Server部分的继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+

我们以TCPServer为例讲解。从221行的对外的入口serve_forever()开始。从230行可以看出,这是个由self.__is_shutdown_request变量控制的循环。与self.__is_shutdown_request相关的还有一个threading.Event对象self.__is_shut_down

threading.Event对象是一种我理解上的信号,只有TrueFalse两个值。有三个基本方法,waitsetclearwait就是阻塞等待直到其值为Trueset方法就是将其赋值为True,而clear就是reset其值成False。在243行shutdown这另一个对外的方法,首先将self.__is_shutdown_request赋值为True之后,便wait等待结束。

self.__is_shutdown_request控制的就是230行的Server主循环。主循环里面就做了两件事情,一个是使用select来等待连接的到来,二个是到连接到来的时候,调用self._handle_request_noblock来处理这个连接。

先说这个select是对操作系统下的select系统调用做的封装。select系统调用是是通过维护一个文件描述符数组,来监视等待IO的文件描述符。是一种跨平台性较好、比较简单的IO复用技术。但缺点很明显,由于能开的文件描述符数量在系统上是有限制的,并且随着数组的增大,其扫描一遍的时间增长,效率会大幅度的降低。代码235行调用的select外面包了一层方法,这个方法不需要太多了解,只是为了处理一个系统中断的。select的最后一个参数是poll_interval,默认值为0.5,也就是0.5秒,表示阻塞等待的时间为0.5秒。

然后就是handle这个request了。处理流程就是get_request->verify_request->process_request,如果出现异常,则调用handle_errorshutdown_requestget_request我们上面也讲了,就是一个socket.accept()verify_request在这里直接返回True,并且下面的两个子类也并没有修改这个方法,也就是为了抽象考虑,并留给用户来使用的。这个process_request中重点在finish_requst上,上面已经说了,这个里面就是调用RequestHandlerhandle来进行逻辑处理。

MixIn

源码中还有两个类,ForkingMixInThreadingMixIn,两个混合类。对于混合类的解释,我显然是比不上赖勇浩大叔的,所以还是贴上他的文章吧——Mixin 扫盲班

简单的来说,混合类就是用来给原本没有某种能力的类插入某个能力的东西。这两个类就是给我们本来只能处理单request的server类提供多request处理能力的类。一个是通过多进程,一个通过多线程来实现的。

我们以ThreadingMixIn为例,其实本质上就是通过多继承的方式,把MixIn放在前面,根据Python多继承的一个方法的搜索顺序,由下至上,从左到右,把MixIn放在前面,重写process_request方法,然后通过开线程的方式来实现同时处理多request的。

其他

其他的诸如ThreadingTCPServerUnixStreamServer只不过是对上述的几个类的组合,继承,很简单,就不做过多的介绍了。