Tornado源码分析
0、预备知识 我读过的对epoll最好的讲解 epoll的工作原理
epoll与select/poll性能,CPU/内存开销对比stackoverflow上的一个精彩问答
一 、 TORNAD介绍 源码组织
为什么要阅读Tornado的源码? Tornado 由前 google 员工开发,代码非常精练,实现也很轻巧,加上清晰的注释和丰富的 demo,我们可以很容易的阅读分析 tornado. 通过阅读 Tornado 的源码,你将学到: 理解 Tornado 的内部实现,使用 tornado 进行 web 开发将更加得心应手。 如何实现一个高性能,非阻塞的 http 服务器。 如何实现一个 web 框架。 各种网络编程的知识,比如 epoll python 编程的绝佳实践 在tornado的子目录中,每个模块都应该有一个.py文件,你可以通过检查他们来判断你是否从已经从代码仓库中完整的迁出了项目。在每个源代码的文件中,你都可以发现至少一个大段落的用来解释该模块的doc string,doc string中给出了一到两个关于如何使用该模块的例子。 下面首先介绍 Tornado 的模块按功能分类。 Tornado模块分类 1. Core web framework tornado.web — 包含web框架的大部分主要功能,包含RequestHandler和Application两个重要的类 tornado.httpserver — 一个无阻塞HTTP服务器的实现 tornado.template — 模版系统 tornado.escape — HTML,JSON,URLs等的编码解码和一些字符串操作 tornado.locale — 国际化支持 2. Asynchronous networking 底层模块 tornado.ioloop — 核心的I/O循环 tornado.iostream — 对非阻塞式的 socket 的简单封装,以方便常用读写操作 tornado.httpclient — 一个无阻塞的HTTP服务器实现 tornado.netutil — 一些网络应用的实现,主要实现TCPServer类 3. Integration with other services tornado.auth — 使用OpenId和OAuth进行第三方登录 tornado.database — 简单的MySQL服务端封装 tornado.platform.twisted — 在Tornado上运行为Twisted实现的代码 tornado.websocket — 实现和浏览器的双向通信 tornado.wsgi — 与其他python网络框架/服务器的相互操作 4. Utilities tornado.autoreload — 生产环境中自动检查代码更新 tornado.gen — 一个基于生成器的接口,使用该模块保证代码异步运行 tornado.httputil — 分析HTTP请求内容 tornado.options — 解析终端参数 tornado.process — 多进程实现的封装 tornado.stack_context — 用于异步环境中对回调函数的上下文保存、异常处理 tornado.testing — 单元测试
Tornado 是由 Facebook 开源的一个服务器“套装”,适合于做 python 的 web 或者使用其本身提供的可扩展的功能,完成了不完整的 wsgi 协议,可用于做快速的 web 开发,封装了 epoll 性能较好。文章主要以分析 tornado 的网络部分即异步事件处理与上层的 IOstream 类提供的异步IO,其他的模块如 web 的 tornado.web 以后慢慢留作分析。
下面开始我们的 Tornado 之旅,看源代码之前必定需要有一份源码了,大家可以去官网下载一份。这里分析的是 3.1.1。
Tornado 的源码组织如下:
tornado网络部分最核心的两个模块就是ioloop.py与iostream.py,我们主要分析的就是这两个部分。
- ioloop.py 主要的是将底层的epoll或者说是其他的IO多路复用封装作异步事件来处理。
- iostream.py主要是对于下层的异步事件的进一步封装,为其封装了更上一层的buffer(IO)事件。
我们先来看看 ioloop(文档地址:http://www.tornadoweb.org/en/stable/ioloop.html):
We use epoll (Linux) or kqueue (BSD and Mac OS X) if they are available, or else we fall back on select(). If you are implementing a system that needs to handle thousands of simultaneous connections, you should use a system that supports either epoll or kqueue.
Example usage for a simple TCP server: import errno import functools import ioloop import socket def connection_ready(sock, fd, events): while True: try: connection, address = sock.accept() except socket.error, e: if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): raise return connection.setblocking(0) handle_connection(connection, address) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setblocking(0) sock.bind(("", port)) sock.listen(128) # 创建一个ioloop 实例 io_loop = ioloop.IOLoop.instance() # connection_ready 的第一个参数为 sock,即 socket 的返回值 callback = functools.partial(connection_ready, sock) # 注册函数,第一个参数是将 sock 转换为标准的描述符,第二个为回调函数,第三个是事件类型 io_loop.add_handler(sock.fileno(), callback, io_loop.READ) io_loop.start()
可以看到在注释前都是使用了传统的创建服务器的方式,不用多介绍,注意就是把套接口设置为非阻塞方式。
创建ioloop实例,这里是使用了ioloop.IOLoop中的 instance()静态方法,以 @classmethod 方式包装。
在后面的add_handler中,程序为我们的监听套接口注册了一个回调函数和一个事件类型。工作方式是这样,在注册了相应的事件类型和回调函数以后,程序开始启动,如果在相应的套接口上有事件发生(注册的事件类型)那么调用相应的回调函数。
当监听套接口有可读事件发生,意味着来了一个新连接,在回调函数中就可以对这个套接口accept,并调用相应的处理函数,其实应该是处理函数也设置为异步的,将相应的连接套接口也加入到事件循环并注册相应的回调函数,只是这里没有展示出来。
在使用非阻塞方式的accept时候常常返回EAGAIN,EWOULDBLOCK 错误,这里采取的方式是放弃这个连接。
二 、TORNADO设计模式 大概了解Tornado的构成
在深入到模块进行分析之前,首先来看看Tornado的设计模型。
从上面的图可以看出,Tornado 不仅仅是一个WEB框架,它还完整地实现了HTTP服务器和客户端,在此基础上提供WEB服务。它可以分为四层:
- 最底层的EVENT层处理IO事件;
- TCP层实现了TCP服务器,负责数据传输;
- HTTP/HTTPS层基于HTTP协议实现了HTTP服务器和客户端;
- 最上层为WEB框架,包含了处理器、模板、数据库连接、认证、本地化等等WEB框架需要具备的功能。
三 、tornado 核心文件介绍 了解Tornado的大致工作机制
前面我们看了一些关于 Tornado 的总体框架设计图,还有一些模块设计。比如 Tornado的源码 里面的文件组织,真的不少,那么我们应该具体去读哪几个文件呢?
为了方便,约定$root指带tornado的根目录。总的来说,要用 Tornado 完成一个网站的构建,其实主要需要以下几个文件:
- $root/tornado/web.py
- $root/tornado/httpserver.py
- $root/tornado/tcpserver.py
- $root/tornado/ioloop.py
- $root/tornado/iostream.py
- $root/tornado/platfrom/epoll.py
- $root/app.py
另外可能还需要一些功能库的支持而需要引入的文件就不列举了,比如util和httputil之类的。来看看每个文件的作用。
- app.py 是自己写的,内容就如 tornado 的 readme 文档里给的示例一样,定义路由规则和 handler,然后创建 application,发起 server 监听,服务器就算跑起来了。
- 紧接着就是 web.py。其中定义了 Application 和 RequestHandler 类,在 app.py 里直接就用到了。Application 是个单例,总揽全局路由,创建服务器负责监听,并把服务器传回来的请求进行转发(__call__)。RequestHandler 是个功能很丰富的类,基本上 web 开发需要的它都具备了,比如redirect,flush,close,header,cookie,render(模板),xsrf,etag等等
- 从 web 跟踪到 httpserver.py 和 tcpserver.py。这两个文件主要是实现 http 协议,解析 header 和 body, 生成request,回调给 appliaction,一个经典意义上的 http 服务器(written in python)。众所周知,这是个很考究性能的一块(IO),所以它和其它很多块都连接到了一起,比如 IOLoop,IOStream,HTTPConnection 等等。这里 HTTPConnection 是实现了 http 协议的部分,它关注 Connection 嘛,这是 http 才有的。至于监听端口,IO事件,读写缓冲区,建立连接之类都是在它的下层--tcp里需要考虑的,所以,tcpserver 才是和它们打交道的地方,到时候分析起来估计很麻烦
- 先说这个IOStream。顾名思义,就是负责IO的。说到IO,就得提缓冲区和IO事件。缓冲区的处理都在它自个儿类里,IO事件的异步处理就要靠 IOLoop 了。
- 然后是 IOLoop。如果你用过 select/poll/epoll/libevent 的话,对它的处理模型应该相当熟悉。简言之,就是一个大大的循环,循环里等待事件,然后处理事件。这是开发高性能服务器的常见模型,tornado 的异步能力就是在这个类里得到保证的
- 最后是 epoll.py。其实这个文件也没干啥,就是声明了一下服务器使用 epoll。选择 select/poll/epoll/kqueue 其中的一种作为事件分发模型,是在 tornado 里自动根据操作系统的类型而做的选择,所以这几种接口是一样的(当然效率不一样),出于简化,直接就epoll吧^_^
- PS。如果你是一个细节控,可能会注意到 tornado 里的回调 callback 函数都不是直接使用的,而是使用 stack_context.wrap 进行了封装。但据我观察,封装前后没多大差别(指逻辑流程),函数的参数也不变。但根据它代码里的注释,这个封装还是相当有用的:
Use this whenever saving a callback to be executed later in a different execution context (either in a different thread or asynchronously in the same thread).
所以,我猜,是使用了独有的context来保证在不同环境也能很好的执行。猜测而已,我也没细想,以后有时间再看好,最有用一个简单的流程来做结。
四 、Tornado HTTP服务器的基本流程 Tornado底层I/O的内部实现
Tornado HTTP服务器的基本流程,分别分析httpserver, ioloop, iostream模块的代码来剖析Tornado底层I/O的内部实现。
httpserver.py中给出了一个简单的http服务器的demo,代码如下所示:
from tornado import httpserver from tornado import ioloop def handle_request(request): message = "You requested %s\n" % request.uri request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % ( len(message), message)) request.finish() http_server = httpserver.HTTPServer(handle_request) http_server.bind(8888) http_server.start() ioloop.IOLoop.instance().start()
该http服务器主要使用到IOLoop, IOStream, HTTPServer, HTTPConnection几大模块,分别在代码ioloop.py, iostream.py, httpserver.py中实现。工作的流程如下图所示:
服务器的工作流程:首先按照socket->bind->listen顺序创建listen socket监听客户端,并将每个listen socket的fd注册到IOLoop的单例实例中;当listen socket可读时回调_handle_events处理客户端请求;在与客户端通信的过程中使用IOStream封装了读、写缓冲区,实现与客户端的异步读写。
HTTPServer分析
HTTPServer在httpserver.py中实现,继承自TCPServer(netutil.py中实现),是一个无阻塞、单线程HTTP服务器。支持HTTP/1.1协议keep-alive连接,但不支持chunked encoding。服务器支持'X-Real-IP'和'X-Scheme'头以及SSL传输,支持多进程为prefork模式实现。在源代码的注释中对以上描述比较详细的说明,这里就不再细说。
HTTPServer和TCPServer的类结构:
class HTTPServer(TCPServer): def __init__(self, request_callback, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None, **kwargs): def handle_stream(self, stream, address): class TCPServer(object): def __init__(self, io_loop=None, ssl_options=None): def listen(self, port, address=""): def add_sockets(self, sockets): def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128): def start(self, num_processes=1): def stop(self): def handle_stream(self, stream, address): def _handle_connection(self, connection, address):
文章开始部分创建HTTPServer的过程:首先需要定义处理request的回调函数,在tornado中通常使用tornado.web.Application封装。然后构造HTTPServer实例,注册回调函数。接下来监听端口,启动服务器。最后启动IOLoop。
def listen(self, port, address=""): sockets = bind_sockets(port, address=address) self.add_sockets(sockets) def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128): # 省略sockets创建,address,flags处理部分代码 for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags)): af, socktype, proto, canonname, sockaddr = res # 创建socket sock = socket.socket(af, socktype, proto) # 设置socket属性,代码省略 sock.bind(sockaddr) sock.listen(backlog) sockets.append(sock) return sockets def add_sockets(self, sockets): if self.io_loop is None: self.io_loop = IOLoop.instance() for sock in sockets: self._sockets[sock.fileno()] = sock add_accept_handler(sock, self._handle_connection, io_loop=self.io_loop) def add_accept_handler(sock, callback, io_loop=None): if io_loop is None: io_loop = IOLoop.instance() def accept_handler(fd, events): while True: try: connection, address = sock.accept() except socket.error, e: if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): return raise # 当有连接被accepted时callback会被调用 callback(connection, address) io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) def _handle_connection(self, connection, address): # SSL部分省略 try: stream = IOStream(connection, io_loop=self.io_loop) self.handle_stream(stream, address) except Exception: logging.error("Error in connection callback", exc_info=True)
这里分析HTTPServer通过listen函数启动监听,这种方法是单进程模式。另外可以通过先后调用bind和start(num_processes=1)函数启动监听同时创建多进程服务器实例,后文有关于此的详细描述。
bind_sockets在启动监听端口过程中调用,getaddrinfo返回服务器的所有网卡信息, 每块网卡上都要创建监听客户端的请求并返回创建的sockets。创建socket过程中绑定地址和端口,同时设置了fcntl.FD_CLOEXEC(创建子进程时关闭打开的socket)和socket.SO_REUSEADDR(保证某一socket关闭后立即释放端口,实现端口复用)标志位。sock.listen(backlog=128)默认设定等待被处理的连接最大个数为128。
返回的每一个socket都加入到IOLoop中同时添加回调函数_handle_connection,IOLoop添加对相应socket的IOLoop.READ事件监听。_handle_connection在接受客户端的连接处理结束之后会被调用,调用时传入连接和ioloop对象初始化IOStream对象,用于对客户端的异步读写;然后调用handle_stream,传入创建的IOStream对象初始化一个HTTPConnection对象,HTTPConnection封装了IOStream的一些操作,用于处理HTTPRequest并返回。至此HTTP Server的创建、启动、注册回调函数的过程分析结束。
HTTPConnection分析
该类用于处理http请求。在HTTPConnection初始化时对self.request_callback赋值为一个可调用的对象(该对象用于对http请求的具体处理和应答)。该类首先读取http请求中header的结束符b("\r\n\r\n"),然后回调self._on_headers函数。request_callback的相关实现在以后的系列中有详细介绍。
def __init__(self, stream, address, request_callback, no_keep_alive=False, xheaders=False): self.request_callback = request_callback # some configuration code self._header_callback = stack_context.wrap(self._on_headers) self.stream.read_until(b("\r\n\r\n"), self._header_callback) def _on_headers(self, data): # some codes self.request_callback(self._request)
多进程HTTPServer
Tornado的HTTPServer是单进程单线程模式,同时提供了创建多进程服务器的接口,具体实现是在主进程启动HTTPServer时通过process.fork_processes(num_processes)产生新的服务器子进程,所有进程之间共享端口。fork_process的方法在process.py中实现,十分简洁
五、Tornado RequestHandler和Application类 了解web.py文件
web.py 这个文件,这个文件最关键的地方是定义了 Application 和 RequestHandler 类。我们再看看 Tornado 的 Hello World,我们再精简一下,下面是最简单的实例化并启动 Application 的方式:
1 |
import ioloop |
2 |
import web |
3 |
4 |
application = web.Application([ |
5 |
(r '/' , MainHandler), |
6 |
]) |
7 |
application.listen( 8888 ) |
8 |
ioloop.IOLoop.instance().start() |
从代码里可以看到的是:应用里定义了 URI 路由和对应的处理类,并以此构建了application对象,然后让这个对象监听在8888端口,最后由 ioloop 单例进入循环,不断分发事件。
这里的URI路由就是r"/",对应处理类就是 MainHandler,它们被放在同一个 tuple 里形成了关联。可以看到,application 是接受一个列表的,因此可以定义多个全局路由对应不同处理,往列表里 append 就是了。
如果只是在 tornado 的框架基础上进行开发,那就只需要不断定义不同的处理类,并把对应路由与其关联即可。
tornado.web 里的 RequestHandler 和 Application 类
Tornado 使用 web 模块的 Application 做URI转发,然后通过 RequestHandler处理请求。 Application 提供了一个 listen 方法作为 HTTPServer 中的 listen 的封装。
初始化 Application 时,一般将处理器直接传入,它会调用 add_handlers 添加这些处理器,初始化还包括 transforms (分块、压缩等)、UI模块、静态文件处理器的初始化。 add_handlers 方法负责添加URI和处理器的映射。
Application 实现 URI 转发时使用了一个技巧,它实现了 __call__ 方法,并将 Application 的实例传递给 HTTPServer ,当监听到请求时,它通过调用 Application 实例触发 __call__ 。 __call__ 方法中完成具体的URI转发工作,并调用已注册的处理器的 _execute 方法,处理请求。
01 def __call__(self, request): 02 transforms = [t(request) for t in self.transforms] 03 handler = None 04 args = [] 05 kwargs = {} 06 handlers = self._get_host_handlers(request) # 取得请求的host的一组处理器 07 if not handlers: 08 handler = RedirectHandler( 09 self, request, url="http://" + self.default_host + "/") 10 else: 11 for spec in handlers: 12 match = spec.regex.match(request.path) # 匹配请求的URI 13 if match: 14 handler = spec.handler_class(self, request,**spec.kwargs) # 实例化 15 if spec.regex.groups: # 取得参数 16 ... 17 if spec.regex.groupindex: 18 kwargs = dict( 19 (str(k), unquote(v)) 20 for (k, v) in match.groupdict().iteritems()) 21 else: 22 args = [unquote(s) for s in match.groups()] 23 break 24 if not handler: # 无匹配 25 handler = ErrorHandler(self, request, status_code=404) 26 ... 27 handler._execute(transforms, *args, **kwargs) # 处理请求 28 return handler
RequestHandler 完成具体的请求,开发者需要继承它,并根据需要,覆盖 head 、 get 、 post 、 delete 、 patch 、 put 、 options 等方法,定义处理对应请求的业务逻辑。
RequestHandler 提供了很多钩子,包括 initialize 、 prepare 、 on_finish 、 on_connection_close 、 set_default_headers 等等。
下面是 _execute 的处理流程:
RequestHandler 中涉及到很多 HTTP 相关的技术,包括 Header、Status、Cookie、Etag、Content-Type、链接参数、重定向、长连接等等,还有和用户身份相关的XSRF和CSRF等等。这方面的知识可以参考《HTTP权威指南》。
Tornado默认实现了几个常用的处理器:
- ErrorHandler :生成指定状态码的错误响应。
- RedirectHandler :重定向请求。
- StaticFileHandler :处理静态文件请求。
- FallbackHandler :使可以在Tornado中混合使用其他HTTP服务器。
上面提到了 transform ,Tornado 使用这种机制来对输出做分块和压缩的转换,默认给出了 GZipContentEncoding 和 ChunkedTransferEncoding 。也可以实现自定义的转换,只要实现 transform_first_chunk 和 transform_chunk 接口即可,它们由 RequestHandler 中的 flush 调用。
六、Application对象的接口与起到的作用 Application对象工作机制
前面小节谈到了Tornado的RequestHandler和Application类,这两块内容还很多,分开来再补充一下,这篇先谈谈Application类。
总的来说,Application对象提供如下几个接口:
- __init__ 接受路由-处理器列表,制定路由转发规则
- listen 建立服务器并监听端口,是对httpserver的封装调用
- add_handlers 添加路由转发规则(包括主机名匹配)
- add_transform 添加输出过滤器。例如gzip,chunk
- __call__ 服务器连接的网关处理接口,一般是被服务器调用
最简单的应该算是 add_transform 了。将一个类添加到列表里就结束了。它会在输出时被调用,比较简单,略过不提。
然后是 listen。它接受端口,地址,其它参数。也很简单,用自身和参数构造 http 服务器,并让服务器监听在端口-地址上。其中涉及到底层 socket 的使用和 ioloop 事件绑定,放在以后再说。总之,可以认为产生了如下效果:在特定端口-地址上创建并监听 socket,并注册了该 socket 的可读事件到自身的__call__方法(亦即,每逢一个新连接到来时,__call__就会被调用)
接下来看 __call__ 方法。这是 python 的一个语法特性,这个函数使得 Application 可以直接被当成函数来使用。
这里有一个问题,为什么不直接定义一个函数例如 call 并在 listen 方法里把 self.call 传给服务器做回调函数,而是使用 self 呢?它们不都是函数吗?有什么区别呢?
区别还是有的。首先,如果使用self.call方法,那么它就是一个纯粹的函数,那么 application 的内部成员就不能用了(比如路由规则表)。而使用 self(也不是self.__call__)传递给服务器做回调,当这个对象被当作函数调用时,__call__会被自动调用,此时对象上下文就被保留下来了。python 里好像经常这么搞。。。
好,来看看__call__的参数:request,HttpRequest对象,在 httputil 里被定义,在这里被用到的是它的 host 和 path 成员,用在路由匹配。忽略错误情况,这个方法的执行流程如下:_get_host_handler(request) 得到该 host 对应的路径路由列表,默认情况下,任何 host 都会被匹配(原因详见__init__),返回的列表直接就是传递给构造 application 时的那个 tuple 列表,当然,对象变了,蛋内容是一样的。然后,对于路径路由列表中的每一个对象,用 request.path 来匹配,如果匹配上了,就生成 RequestHandler 对象,并根据正则表达式解析路径里的参数,可以用数字做键值也可以用字符串做键值。具体见 python 的 re.match.groups()。然后跳出列表,执行_execute() 方法,这个方法在 RequestHandler 里被定义,下次再说,简言之,它的作用是 根据 http 方法转到对应方法,路径正则表达式里解析到的参数也被原样保留传进去。
一直有个疑问,路由规则是什么时候建立的呢?为此,我们先看 add_handlers 方法,它接受两个参数,主机名正则,路径路由列表。同时,我们还要注意到,self.handlers 也是一个列表,是主机名路由列表,它的每个元素是一个 tuple,包含主机名和对应的路径路由列表。如图:
所以,add_handlers 的流程就很简单了:将路径路由列表和主机名合成一个 tuple 添加到 self.handlers 里,这样该主机名就会在_get_host_handler 里被检索,就可以根据主机名找到对应的路径路由规则了。这里需要注意的一个问题是:由于.*的特殊性(它会匹配任意字符),因此总是需要保证它被放置在列表的最后,所以就有了这段代码:
1 |
if self .handlers and self .handlers[ - 1 ][ 0 ].pattern = = '.*$' : |
2 |
self .handlers.insert( - 1 , (re. compile (host_pattern), handlers)) |
之前还说到,默认情况下,所有的主机名都会被匹配,那是因为在__init__方法里,它调用了 add_handlers(".*",handlers)。由于.*匹配所有主机名,所以构造 application 对象时传入的路径路由规则列表就是最终默认路由列表了。
最后看一下__init__方法的流程。它一般接受一个参数,handlers,亦即最终匹配主机的路径路由列表。先设定 transform 列表,再设定静态文件的路由,然后添加主机(.*)的路由列表。
好,回顾一下。Application 的__init__方法设定了.*主机的路径路由规则,listen 方法里开启了服务器并把自身作为回调。__call__方法在服务器 accept 到一个新连接时被调用,主要是根据路由规则转发请求到不同的处理器类,并在处理器里被分派到对应的具体方法中,到此完成请求的处理。
下一节是RequestHandler。
七、RequestHandler的分析 handler是如何工作的
前面一小节谈到了Application 类,这里再来看看 RequestHandler 类。
从上一节的流程可以看出,RequestHandler 类把 _execute 方法暴露给了 application 对象,在这个方法里完成了请求的具体分发和处理。因此,我主要看这一方法(当然还包括__init__),其它方法在开发应用时自然会用到,还是比较实用的,比如header,cookie,get/post参数的getter/setter方法,都是必须的。
首先是__init__。负责对象的初始化,在对象被构造时一定会被调用的。
那对象什么时候被调用呢?从上一节可以看到,在.*主机的路径路由列表里,当路径正则匹配了当前请求的路由时,application 就会新建一个 RequestHandler 对象(实际上是子类对象),然后调用 _execute 方法。__init__ 方法接受3个参数 : application, request, **kwargs,分别是application单例,当前请求request 和 kwargs (暂时没被用上。不过可以覆盖initialize方法,里面就有它)。这个kwargs 是静态存储在路由列表里的,它最初是在给 application 设置路由列表时除了路径正则,处理器类之外的第三个对象,是一个字典,一般情况是空的。__init__ 方法也没做什么,就是建立了这个对象和 application, request 的关联就完了。构造器就应该是这样,对吧?
接下来会调用 _execute 方法。
该方法接受三个参数 transforms(相当于 application 的中间件吧,对流程没有影响),*args(用数字做索引时的正则 group),**kwargs(用字符串做键值时的正则 group,与__init__的类似但却是动态的)。该方法先设定好 transform(因为不管错误与否都算输出,因此中间件都必须到位)。然后,检查 http 方法是否被支持,然后整合路径里的参数,检查 XSRF 攻击。然后 prepare()。这总是在业务逻辑前被执行,在业务逻辑后还有个 finish()。业务逻辑代码被定义在子类的 get/post/put/delete 方法里,具体视详细情况而定。
还有一个 finish 方法,它在业务逻辑代码后被执行。缓冲区里的内容要被输出,连接总得被关闭,资源总得被释放,所以这些善后事宜就交给了 finish。与缓冲区相关的还有一个函数 flush,顾名思义它会调用 transforms 对输出做预处理,然后拼接缓冲区一次性输出 self.request.write(headers + chunk, callback=callback)。
以上,就是 handler 的分析。handler 的读与写,实际上都是依靠 request 对象来完成的,而 request 到底如何呢?且看下回分解。
八、Tornado的核心web框架tornado.web小结 RequestHandler和Application
Tornado的web框架(tornado.web)在web.py中实现,主要包括RequestHandler类(本质为对http请求处理的封装)和Application类(是一些列请求处理的集合,构成的一个web-application,源代码注释不翻译更容易理解:A collection of request handlers that make up a web application)。
RequestHandler分析
RequestHandler提供了一个针对http请求处理的基类封装,方法比较多,主要有以下功能:
- 提供了GET/HEAD/POST/DELETE/PATCH/PUT/OPTIONS等方法的功能接口,具体开发时RequestHandler的子类重写这些方法以支持不同需求的请求处理。
- 提供对http请求的处理方法,包括对headers,页面元素,cookie的处理。
- 提供对请求响应的一些列功能,包括redirect,write(将数据写入输出缓冲区),渲染模板(render, reander_string)等。
- 其他的一些辅助功能,如结束请求/响应,刷新输出缓冲区,对用户授权相关处理等。
Application分析
源代码中的注释写的非常好:
A collection of request handlers that make up a web application. Instances of this class are callable and can be passed directly to HTTPServer to serve the application.
该类初始化的第一个参数接受一个(regexp, request_class)形式的列表,指定了针对不同URL请求所采取的处理方法,包括对静态文件请求的处理(web.StaticFileHandler)。Application类中实现 __call__ 函数,这样该类就成为可调用的对象,由HTTPServer来进行调用。比如下边是httpserver.py中HTTPConection类的代码,该处request_callback即为Application对象。
1 def _on_headers(self, data): 2 # some codes... 3 self.request_callback(self._request)
__call__函数会遍历Application的handlers列表,匹配到相应的URL后通过handler._execute进行相应处理;如果没有匹配的URL,则会调用ErrorHandler。
在Application初始时有一个debug参数,当debug=True时,运行程序时当有代码、模块发生修改,程序会自动重新加载,即实现了auto-reload功能。该功能在autoreload.py文件中实现,是否需要reload的检查在每次接收到http请求时进行,基本原理是检查每一个sys.modules以及_watched_files所包含的模块在程序中所保存的最近修改时间和文件系统中的最近修改时间是否一致,如果不一致,则整个程序重新加载。
1 def _reload_on_update(modify_times): 2 for module in sys.modules.values(): 3 # module test and some path handles 4 _check_file(modify_times, path) 5 for path in _watched_files: 6 _check_file(modify_times, path)
Tornado的autoreload模块提供了一个对外的main接口,可以通过下边的方法实现运行test.py程序运行的auto-reload。但是测试了一下,功能有限,相比于django的autorelaod模块(具有较好的封装和较完善的功能)还是有一定的差距。最主要的原因是Tornado中的实现耦合了一些ioloop的功能,因而autoreload不是一个可独立的模块。
1 # tornado 2 python -m tornado.autoreload test.py [args...] 3 4 # django 5 from django.utils import autoreload 6 autoreload.main(your-main-func)
asynchronous方法
该方法通常被用为请求处理函数的decorator,以实现异步操作,被@asynchronous修饰后的请求处理为长连接,在调用self.finish之前会一直处于连接等待状态。
总结
在前面小节 Tornado HTTP服务器的基本流程 中,给出了一张tornado httpserver的工作流程图,调用Application发生在HTTPConnection大方框的handle_request椭圆中。那篇文章里使用的是一个简单的请求处理函数handle_request,无论是handle_request还是application,其本质是一个函数(可调用的对象),当服务器接收连接并读取http请求header之后进行调用,进行请求处理和应答。
1 http_server = httpserver.HTTPServer(handle_request) 2 http_server = httpserver.HTTPServer(application)
九、 HTTP层:HTTPRequest,HTTPServer与HTTPConnection HTTP层的实现
前面小节在分析 handler 时提到,handler 的读写实际是依靠 httprequest 来完成的。今天就分析 tornado 在 HTTP 这一层上的实现,类包括 HTTPRequest, HTTPServer 和 HTTPConnection.
首先,HTTP 协议是建立在面向连接的可靠连接协议 TCP 协议之上,是应用层协议,亦即它的协议内容会涉及网络业务逻辑,而与网络连接处理等底层细节关系不大,因此今天只会提到少量 socket 内容,具体的 TCP 细节留待后面说。http 协议详情查看RFC2616,简言之,http协议约定服务器接收到的内容应该包括三部分,首行,请求头,请求体。首行声明了请求方法,请求 uri,协议版本。请求头是一系列键值对,每行代表一个键值对,建与值间用': '分割。请求体就比较随意了,默认情况是用&连接键值对,也有可能是 RFC1867 里规定的multipart/form-data。先从 HTTPServer 说起吧。
1. HTTPServer
HTTPServer 继承于 TCPServer。它的__init__ 记录了连接到来时的回调函数(http 层次的回调),亦即 application 对象(它的__call__方法会被触发),然后就是父类的初始化了。TCP 服务器细节后面再看,简言之,它可以:它可以监听在特定地址-端口上,并每当有客户端发起连接到服务器时接收该连接,并调用方法 handle_stream(TCP 层次的回调,这个方法总是被子类覆盖,因为只有在这里才可以实现不同应用层协议的业务逻辑)。
HTTPServer 覆盖了父类的 handler_stream 方法,并在该方法里生成 HTTPConnection 对象就结束了。由此可知,HTTPConnection对象被构建就立即开始了 http 协议的处理,这样是合理的,因为 handle_stream 被调用的时候肯定是新连接到来,这时缓冲区里一般有数据可读,当然可以直接读取并处理。
2. HTTPConnection
HTTPConnection 是 HTTP 协议在服务端的真正实现者,我的意思是说,对于请求的读取和解析,基本是由它(依靠HTTPHeaders)完成的。但是响应方面的处理(包括响应头,响应主体的中间处理等)则是在 RequestHandler 里的 flush 方法和 finish 方法里完成的。我把它跳过了,有兴趣的可以自己自己看吧。
HTTPConnection 里的方法,大部分都可以顾名思义,有几个方法需要注意:__init__, _on_headers, _on_request_body. 其它读写之类的方法则直接对应到 IOStream 里,以后再说了。首先是__init__, 它也没干什么,初始化协议参数和回调函数的默认值(一般是None)。然后设定了 header_callback,并开始读取,如下:
1 # Save stack context here, outside of any request. This keeps 2 # contexts from one request from leaking into the next. 3 self._header_callback = stack_context.wrap(self._on_headers) 4 self.stream.read_until(b"\r\n\r\n", self._header_callback)
然后这里涉及到两个:stack_context.wrap和stream.read_until。
先是这个wrap函数,它就是一个装饰器,就是封装了一下,对多线程执行环境的上下文做了一些维护。
还有一个就是read_until了,顾名思义,就是一直读取知道\r\n\r\n(这一般意味这请求头的结束),然后调用回调函数_on_headers(这是 IOStream 层次的回调)。具体怎么做的以后再说了,先确认是这么个功能。然后 _on_headers 函数在请求头结束时被调用,它的参数只有一个 data,亦即读取到的请求头字符串。首先是找到起始行:
1 eol = data.find("\r\n") 2 start_line = data[:eol]
然后是用空格分解首行来找到方法,uri和协议版本:
1 method, uri, version = start_line.split(" ")
接着依靠HTTPHeaders解析剩余的请求头,返回一个字典:
1 headers = httputil.HTTPHeaders.parse(data[eol:])
然后设定 remote_ip(好像没什么用?)。接着创建 request 对象(这就是 RequestHandler 接收的那个 request),然后用 Content-Length 检查是否有请求体,如果没有则直接调用 HTTP 层次的回调(亦即 application 的__call__方法),如果有则读取指定长度的内容并跳到回调 _on_request_body, 当然最终还是会调用 application 对象。在 _on_request_body 方法里是调用 parse_body_arguments方法来完成解析主体,请求头和请求体的解析稍候再说。至此,执行流程就和 Application对象的接口与起到的作用 接上了。至于何时调用handle_stream,后面会说到。
在看解析请求前,简单提一下 HTTPRequest。它是客户端请求的代表,它携带了所有和客户端请求的信息,因为 application 的回调__call__方法只接收 request 参数,当然是把所有信息包在其中。另外,由于服务器只把 request 对象暴露给 application 的回调,因此request 对象还需要提供 write,finish 方法来提供服务,其实就是对 HTTPConnection 对象的封装调用。其它也没什么了。
接下来是关于请求的解析,这和 HTTP 协议的内容密切相关。先看 httpheaders(在httputil.py里)。HTTPHeaders 继承于 dict。它的parse 是一个静态方法,接收字符串参数,内容很简单如下:
1 h = cls() 2 for line in headers.splitlines(): 3 if line: 4 h.parse_line(line) 5 return h
首先创建一个自身对象,然后把字符串分解成行,对每行调用parse_line,最后返回自身对象。以下是parse_line:
1 if line[0].isspace(): 2 # continuation of a multi-line header 3 new_part = ' ' + line.lstrip() 4 self._as_list[self._last_key][-1] += new_part 5 dict.__setitem__(self, self._last_key, 6 self[self._last_key] + new_part) 7 else: 8 name, value = line.split(":", 1) 9 self.add(name, value.strip())
parse_line 方法接收一行字符串,先检查首字符是否为空格。如果不是,则用:分割该行得到键和值,保存即可。如果是空格,说明这一行的内容是从属于上一行,简单的把这一行内容附加到上次键值对的内容里。接下来是关于httputil.parse_body_arguments方法:
01 def parse_body_arguments(content_type, body, arguments, files): 02 """Parses a form request body. 03 04 Supports ``application/x-www-form-urlencoded`` and 05 ``multipart/form-data``. The ``content_type`` parameter should be 06 a string and ``body`` should be a byte string. The ``arguments`` 07 and ``files`` parameters are dictionaries that will be updated 08 with the parsed contents. 09 """ 10 if content_type.startswith("application/x-www-form-urlencoded"): 11 uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) 12 for name, values in uri_arguments.items(): 13 if values: 14 arguments.setdefault(name, []).extend(values) 15 elif content_type.startswith("multipart/form-data"): 16 fields = content_type.split(";") 17 for field in fields: 18 k, sep, v = field.strip().partition("=") 19 if k == "boundary" and v: 20 parse_multipart_form_data(utf8(v), body, arguments, files) 21 break 22 else: 23 gen_log.warning("Invalid multipart/form-data")
首先检查conten_type是否以application/x-www-form-urlencoded开头,如果是,说明是用&连接的简单字符串,调用parse_qs_bytes(与urllib.parse.parse_qs等效)来进行解析。而如果是以multipart/form-data开头,则用;分解content_type,目的是找到boundary,当找到了boundary时(这时一般content_type也到尽头了),就调用parse_multipart_data对请求体进行解析。接下来看这个函数:
01 def parse_multipart_form_data(boundary, data, arguments, files): 02 """Parses a ``multipart/form-data`` body. 03 04 The ``boundary`` and ``data`` parameters are both byte strings. 05 The dictionaries given in the arguments and files parameters 06 will be updated with the contents of the body. 07 """ 08 # The standard allows for the boundary to be quoted in the header, 09 # although it's rare (it happens at least for google app engine 10 # xmpp). I think we're also supposed to handle backslash-escapes 11 # here but I'll save that until we see a client that uses them 12 # in the wild. 13 if boundary.startswith(b'"') and boundary.endswith(b'"'): 14 boundary = boundary[1:-1] 15 final_boundary_index = data.rfind(b"--" + boundary + b"--") 16 if final_boundary_index == -1: 17 gen_log.warning("Invalid multipart/form-data: no final boundary") 18 return 19 parts = data[:final_boundary_index].split(b"--" + boundary +b"\r\n") 20 for part in parts: 21 if not part: 22 continue 23 eoh = part.find(b"\r\n\r\n") 24 if eoh == -1: 25 gen_log.warning("multipart/form-data missing headers") 26 continue 27 headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) 28 disp_header = headers.get("Content-Disposition", "") 29 disposition, disp_params = _parse_header(disp_header) 30 if disposition != "form-data" or not part.endswith(b"\r\n"): 31 gen_log.warning("Invalid multipart/form-data") 32 continue 33 value = part[eoh + 4:-2] 34 if not disp_params.get("name"): 35 gen_log.warning("multipart/form-data value missing name") 36 continue 37 name = disp_params["name"] 38 if disp_params.get("filename"): 39 ctype = headers.get("Content-Type", "application/unknown") 40 files.setdefault(name, []).append(HTTPFile( 41 filename=disp_params["filename"], body=value, 42 content_type=ctype)) 43 else: 44 arguments.setdefault(name, []).append(value)
首先,保证边界 boundary 没被“”包裹。然后用b"--" + boundary + b"--"找到请求体的结束位置。然后在开头与结束位置间用分隔符b"--" + boundary + b"\r\n"把请求体分割成多个类似部分。然后对于每一部分循环进行如下处理:用b"\r\n\r\n"分隔元描述和实际内容,在元描述里可以找到 Content-Disposition, Content-type, name, filename 等信息,name 一般成为标识该部分内容的键,在字典里存储该内容。详细细节请参看RFC1867。
至此,HTTP 层内容终于看完了。继续看TCP层。
十、Tornado在TCP层里的工作机制 TCP层的实现
上一节是关于应用层的协议 HTTP,它依赖于传输层协议 TCP,例如服务器是如何绑定端口的?HTTP 服务器的 handle_stream 是在什么时候被调用的呢?本节聚焦在 TCP 层次的实现,以便和上节的程序流程衔接起来。
首先是关于 TCP 协议。这是一个面向连接的可靠交付的协议。由于是面向连接,所以在服务器端需要分配内存来记忆客户端连接,同样客户端也需要记录服务器。由于保证可靠交付,所以引入了很多保证可靠性的机制,比如定时重传机制,SYN/ACK 机制等,相当复杂。所以,在每个系统里的 TCP 协议软件都是相当复杂的,本文不打算深入谈这些(我也谈不了多少,呵呵)。但我们还是得对 TCP 有个了解。先上一张图(UNIX 网络编程)-- 状态转换图。
除外,来一段TCP服务器端编程经典三段式代码(C实现):
1 // 创建监听socket 2 int sfd = socket(AF_INET, SOCK_STREAM, 0); 3 // 绑定socket到地址-端口, 并在该socket上开始监听。listen的第二个参数叫backlog,和连接队列有关 4 bind(sfd,(struct sockaddr *)(&s_addr), sizeof(struct sockaddr)) && listen(sfd, 10); 5 while(1) cfd = accept(sfd, (struct sockaddr *)(&cli_addr), &addr_size);
以上,忽略所有错误处理和变量声明,顾名思义吧…… 更多详细,可以搜 Linux TCP 服务器编程。所以,对于 TCP 编程的总结就是:创建一个监听 socket,然后把它绑定到端口和地址上并开始监听,然后不停 accept。这也是 tornado 的 TCPServer 要做的工作。
TCPServer 类的定义在 tcpserver.py。它有两种用法:bind+start 或者 listen。
第一种用法可用于多线程,但在 TCP 方面两者是一样的。就以 listen 为例吧。TCPServer 的__init__没什么注意的,就是记住了 ioloop 这个单例,这个下节再分析(它是tornado异步性能的关键)。listen 方法接收两个参数端口和地址,代码如下:
01 def listen(self, port, address=""): 02 """Starts accepting connections on the given port. 03 04 This method may be called more than once to listen on multiple ports. 05 `listen` takes effect immediately; it is not necessary to call 06 `TCPServer.start` afterwards. It is, however, necessary to start 07 the `.IOLoop`. 08 """ 09 sockets = bind_sockets(port, address=address) 10 self.add_sockets(sockets)
以上。首先 bind_sockets 方法接收地址和端口创建 sockets 列表并绑定地址端口并监听(完成了TCP三部曲的前两部),add_sockets 在这些 sockets 上注册 read/timeout 事件。有关高性能并发服务器编程可以参照UNIX网络编程里给的几种编程模型,tornado 可以看作是单线程事件驱动模式的服务器,TCP 三部曲中的第三部就被分隔到了事件回调里,因此肯定要在所有的文件 fd(包括sockets)上监听事件。在做完这些事情后就可以安心的调用 ioloop 单例的 start 方法开始循环监听事件了。具体细节可以参照现代高性能 web 服务器(nginx/lightttpd等)的事件模型,后面也会涉及一点。
简言之,基于事件驱动的服务器(tornado)要干的事就是:创建 socket,绑定到端口并 listen,然后注册事件和对应的回调,在回调里accept 新请求。
bind_sockets 方法在 netutil 里被定义,没什么难的,创建监听 socket 后为了异步,设置 socket 为非阻塞(这样由它 accept 派生的socket 也是非阻塞的),然后绑定并监听之。add_sockets 方法接收 socket 列表,对于列表中的 socket,用 fd 作键记录下来,并调用add_accept_handler 方法。它也是在 netutil 里定义的,代码如下:
01 def add_accept_handler(sock, callback, io_loop=None): 02 """Adds an `.IOLoop` event handler to accept new connections on ``sock``. 03 04 When a connection is accepted, ``callback(connection, address)`` will 05 be run (``connection`` is a socket object, and ``address`` is the 06 address of the other end of the connection). Note that this signature 07 is different from the ``callback(fd, events)`` signature used for 08 `.IOLoop` handlers. 09 """ 10 if io_loop is None: 11 io_loop = IOLoop.current() 12 13 def accept_handler(fd, events): 14 while True: 15 try: 16 connection, address = sock.accept() 17 except socket.error as e: 18 if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): 19 return 20 raise 21 callback(connection, address) 22 io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
需要注意的一个参数是 callback,现在指向的是 TCPServer 的 _handle_connection 方法。add_accept_handler 方法的流程:首先是确保ioloop对象。然后调用 add_handler 向 loloop 对象注册在fd上的read事件和回调函数accept_handler。该回调函数是现成定义的,属于IOLoop层次的回调,每当事件发生时就会调用。回调内容也就是accept得到新socket和客户端地址,然后调用callback向上层传递事件。从上面的分析可知,当read事件发生时,accept_handler被调用,进而callback=_handle_connection被调用。
_handle_connection就比较简单了,跳过那些ssl的处理,简化为两句stream = IOStream(connection, io_loop=self.io_loop)和self.handle_stream()。这里IOStream代表了IO层,以后再说,反正读写是不愁了。接着是调用handle_stream。我们可以看到,不论应用层是什么协议(或者自定义协议),当有新连接到来时走的流程是差不多的,都要经历一番上诉的回调,不同之处就在于这个handle_stream方法。这个方法是由子类自定义覆盖的,它的HTTP实现已经在上一节看过了。
到此,和上节的代码流程接上轨了。当事件发生时是如何回调的呢?app.py里的IOLoop.instance().start()又是怎样的流程呢?明天继续,看tornado异步高性能的根本所在
十一、 Tornado TCPServer类的设计解读 一个通用的server框架
前文已经说过,HTTPServer是派生自TCPServer,从协议层次上讲,这再自然不过。
从TCPServer的实现上看,它是一个通用的server框架,基本是按照BSD socket的思想设计的。create-bind-listen三段式一个都不少。
从helloworld.py往下追,可以看到:
- helloworld.py中的main函数创建了HTTPServer.
- HTTPServer继承自TCPServer,在HTTPServer的构造函数中直接调用了TCPServer的构造函数。
接下来我们就去看看TCPServer这个类的实现,它的代码放在tornado/tcpserver.py中。tcpserver.py只有两百多行,不算多。所有代码都是在实现TCPServer这个类。
TCPServer
在TCPServer类的注释中,首先强调了它是一个non-blocking, single-threaded TCP Server。
怎么理解呢?
non-blocking,就是说,这个服务器没有使用阻塞式API。
什么是阻塞式设计?举个例子,在BSD Socket里,recv函数默认是阻塞式的。使用recv读取客户端数据时,如果对方并未发送数据,则这个API就会一直阻塞那里不返回。这样服务器的设计不得不使用多线程或者多进程方式,避免因为一个API的阻塞导致服务器没法做其它事。阻塞式API是很常见的,我们可以简单认为,阻塞式设计就是“不管有没有数据,服务器都派API去读,读不到,API就不会回来交差”。
而非阻塞,对recv来说,区别在于没有数据可读时,它不会在那死等,它直接就返回了。你可能会认为这办法比阻塞式还要矬,因为服务器无法预知有没有数据可读,不得不反复派recv函数去读。这不是浪费大量的CPU资源么?
当然不会这么傻。tornado这里说的非阻塞要高级得多,基本上是另一种思路:服务器并不主动读取数据,它和操作系统合作,实现了一种“监视器”,TCP连接就是它的监视对象。当某个连接上有数据到来时,操作系统会按事先的约定通知服务器:某某号连接上有数据到来,你去处理一下。服务器这时候才派API去取数据。服务器不用创建大量线程来阻塞式的处理每个连接,也不用不停派API去检查连接上有没有数据,它只需要坐那里等操作系统的通知,这保证了recv API出手就不会落空。
tornado另一个被强调的特征是single-threaded,这是因为我们的“监视器”非常高效,可以在一个线程里监视成千上万个连接的状态,基本上不需要再动用线程来分流。实测表明,它比阻塞式多线程或者多进程设计更加高效——当然,这依赖于操作系统的大力配合,现在主流操作系统都提供了非常高端大气上档次的“监视器”机制,比如epoll、kqueue。
作者提到这个类一般不直接被实例化,而是由它派生出子类,再用子类实例化。
为了强化这个设计思想,作者定义了一个未直接实现的接口,叫handle_stream()。
1 def handle_stream(self, stream, address): 2 """Override to handle a new `.IOStream` from an incoming connection.""" 3 raise NotImplementedError()
这倒是个不错的技巧,强制让子类覆盖本方法,不然就报错给你看!
TCPServer是支持SSL的。由于Python的强大,支持SSL一点都不费事。要启动一个支持SSL的TCPServer,只需要告诉它你的certifile和keyfile就行。
1 TCPServer(ssl_options={"certfile": os.path.join(data_dir,"mydomain.crt"), 2 "keyfile": os.path.join(data_dir, "mydomain.key"),})
关于这两个文件的来龙去脉,可以去Google“数字证书原理”这篇文章。
TCPServer的三种形式
TCPServer的初始化有三种形式。
1. 单进程形式
1 server = TCPServer() 2 server.listen(8888) 3 IOLoop.instance().start()
我们在helloworld.py中看到的就是这种用法,不再赘述。
2. 多进程形式。
1 server = TCPServer() 2 server.bind(8888) 3 server.start(0) # Forks multiple sub-processes 4 IOLoop.instance().start()
区别主要在server.start(0)这里。后面分析listen()与start()两个成员函数时,就会看到它们是怎么跟进程结合的。
注意:这种模式启动时,不能把IOLoop对象传递给TCPServer的构造函数,这样会导致TCPServer直接按单进程启动。
3. 高级多进程形式。
1 sockets = bind_sockets(8888) 2 tornado.process.fork_processes(0) 3 server = TCPServer() 4 server.add_sockets(sockets) 5 IOLoop.instance().start()
高级意味着复杂。从上面代码看,虽然只多了一两行,实际里面的流程有比较大的差别。
这种方式的主要优点就是 tornado.process.fork_processes(0)这句,它为进程的创建提供了更多的灵活性。当然现在说了也是糊涂,后面钻进这些代码后,我们再来验证这里的说法。
以上内容都是TCPServer类的doc string中提到的。后面小节开始看code。
十二、 从代码分析TCPServer类的机制 create-bind-listen
接上面一小节,开始看 TCPServer 的 code。
TCPServer的__init__函数很简单,仅保存了参数而已。
唯一要注意的是,它可以接受一个io_loop为参数。实际上io_loop对TCPServer来说并不是可有可无,它是必须的。不过TCPServer提供了多种渠道来与一个io_loop绑定,初始化参数只是其中一种绑定方式而已。
listen
接下来我们看一下listen函数,在helloworld.py中,httpserver实例创建之后,它被第一个调用。
TCPServer类的listen函数是开始接受指定端口上的连接。注意,这个listen与BSD Socket中的listen并不等价,它做的事比BSD socket()+bind()+listen()还要多。
注意在函数注释中提到的一句话:你可以在一个server的实例中多次调用listen,以实现一个server侦听多个端口。
怎么理解?在BSD Socket架构里,我们不可能在一个socket上同时侦听多个端口。反推之,不难想到,TCPServer的listen函数内部一定是执行了全套的BSD Socket三段式(create socket->bind->listen),使得每调用一次listen实际上是创建了一个新的socket。
代码很好的符合了我们的猜想:
def listen(self, port, address=""): sockets = bind_sockets(port, address=address) self.add_sockets(sockets)
两步走,先创建了一个socket,然后把它加到自己的侦听队列里。
bind_socket
bind_socket函数并不是TCPServer的成员,它定义在netutil.py中,原型:
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None):
它也有大段的注释。
bind_socket完成的工作包括:创建socket,绑定socket到指定的地址和端口,开启侦听。
解释一下参数:
- port不用说,端口号嘛。
- address可以是IP地址,如“192.168.1.100”,也可以是hostname,比如“localhost”。如果是hostname,则可以监听该hostname对应的所有IP。如果address是空字符串(“”)或者None,则会监听主机上的所有接口。
- family是指网络层协议类型。可以选AF_INET和AF_INET6,默认情况下则两者都会被启用。这个参数就是在BSD Socket创建时的那个sockaddr_in.sin_family参数哈。
- backlog就是指侦听队列的长度,即BSD listen(n)中的那个n。
- flags参数是一些位标志,它是用来传递给socket.getaddrinfo()函数的。比如socket.AI_PASSIVE等。
另外要注意,在IPV6和IPV4混用的情况下,这个函数的返回值可以是一个socket列表,因为这时候一个address参数可能对应一个IPv4地址和一个IPv6地址,它们的socket是不通用的,会各自独立创建。
现在来一行一行看下bind_socket的代码:
sockets = [] if address == "": address = None if not socket.has_ipv6 and family == socket.AF_UNSPEC: # Python can be compiled with --disable-ipv6, which causes # operations on AF_INET6 sockets to fail, but does not # automatically exclude those results from getaddrinfo # results. # http://bugs.python.org/issue16208 family = socket.AF_INET if flags is None: flags = socket.AI_PASSIVE
这一段平淡无奇,基本上都是前面讲到的参数赋值。
接下来就是一个大的循环:
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,0, flags)):
闹半天,前面解释的参数全都被socket.getaddrinfo()这个函数吃下去了。
socket.getaddrinfo()是python标准库中的函数,它的作用是将所接收的参数重组为一个结构res,res的类型将可以直接作为socket.socket()的参数。跟BSD Socket中的getaddrinfo差不多嘛。
之所以用了一个循环,正如前面讲到的,因为IPv6和IPv4混用的情况下,getaddrinfo会返回多个地址的信息。参见python文档中的说明和示例:
The function returns a list of 5-tuples with the following structure: (family, type, proto, canonname, sockaddr)
>>> socket.getaddrinfo("www.python.org", 80, proto=socket.SOL_TCP) [(2, 1, 6, '', ('82.94.164.162', 80)), (10, 1, 6, '', ('2001:888:2000:d::a2', 80, 0, 0))]
接下来的代码在循环体中,是针对单个地址的。循环体内一开始就如我们猜想,直接拿getaddrinfo的返回值来创建socket。
af, socktype, proto, canonname, sockaddr = res try: sock = socket.socket(af, socktype, proto) except socket.error as e: if e.args[0] == errno.EAFNOSUPPORT: continue raise
先从tuple中拆出5个参数,然后拣需要的来创建socket。
set_close_exec(sock.fileno())
这行是设置进程退出时对sock的操作。lose_on_exec 是一个进程所有文件描述符(文件句柄)的位图标志,每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄(参见include/fcntl.h)。当一个程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数加载执行另一个新程序。此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。若一个文件描述符在close_on_exec中的对应比特位被设置,那么在执行execve()时该描述符将被关闭,否则该描述符将始终处于打开状态。
当打开一个文件时,默认情况下文件句柄在子进程中也处于打开状态。因此sys_open()中要复位对应比特位。
if os.name != 'nt': sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
对非NT的内核,需要额外设置一个SO_REUSEADDR参数。有些系统的设计里,服务器进程结束后端口也会被内核保持一段时间,若我们迅速的重启服务器,可能会遇到“端口已经被占用”的情况。这个标志就是通知内核不要保持了,进程一关,立马放手,便于后来者重用。
if af == socket.AF_INET6: # On linux, ipv6 sockets accept ipv4 too by default, # but this makes it impossible to bind to both # 0.0.0.0 in ipv4 and :: in ipv6. On other systems, # separate sockets *must* be used to listen for both ipv4 # and ipv6. For consistency, always disable ipv4 on our # ipv6 sockets and use a separate ipv4 socket when needed. # # Python 2.x on windows doesn't have IPPROTO_IPV6. if hasattr(socket, "IPPROTO_IPV6"): sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
这段代码的说明已经很清楚了。
sock.setblocking(0) sock.bind(sockaddr) sock.listen(backlog) sockets.append(sock)
前面经常提BSD Socket的这几个家伙,现在它们终于出现了。“非阻塞”性质也是在这里决定的。
每创建一个socket都将它加入到前面定义的列表里,最后函数结束时,将列表返回。其实这个函数蛮简单的。为什么它不是TCPServer的成员函数?
十三、 Tornado高性能的秘密:ioloop对象分析 IOLoop是个事件循环
网上都说nginx和lighthttpd是高性能web服务器,而tornado也是著名的高抗负载应用,它们间有什么相似处呢?上节提到的ioloop对象是如何循环的呢?往下看。
首先关于TCP服务器的开发上节已经提过,很明显那个三段式的示例是个效率很低的(因为只有一个连接被端开新连接才能被接受)。要想开发高性能的服务器,就得在这accept上下功夫。
首先,新连接的到来一般是经典的三次握手,只有当服务器收到一个SYN时才说明有一个新连接(还没建立),这时监听fd是可读的可以调用accept,此前服务器可以干点别的,这就是SELECT/POLL/EPOLL的思路。而只有三次握手成功后,accept才会返回,此时监听fd是读完成状态,似乎服务器在此之前可以转身去干别的,等到读完成再调用accept就不会有延迟了,这就是AIO的思路,不过在*nix平台上好像支持不是很广。。。再有,accept得到的新fd,不一定是可读的(客户端请求还没到达),所以可以等新fd可读时在read()(可能会有一点延迟),也可以用AIO等读完后再read就不会延迟了。同样类似,对于write,close也有类似的事件。
总的思路就是,在我们关心的fd上注册关心的多个事件,事件发生了就启动回调,没发生就看点别的。这是单线程的,多线程的复杂一点,但差不多。nginx和lightttpd以及tornado都是类似的方式,只不过是多进程和多线程或单线程的区别而已。为简便,我们只分析tornado单线程的情况。
关于ioloop.py的代码,主要有两个要点。一个是configurable机制,一个就是epoll循环。先看epoll循环吧。IOLoop 类的start是循环所在,但它必须被子类覆盖实现,因此它的start在PollIOLoop里。略过循环外部的多线程上下文环境的保存与恢复,单看循环:
01 while True: 02 poll_timeout = 3600.0 03 04 # Prevent IO event starvation by delaying new callbacks 05 # to the next iteration of the event loop. 06 with self._callback_lock: 07 callbacks = self._callbacks 08 self._callbacks = [] 09 for callback in callbacks: 10 self._run_callback(callback) 11 12 if self._timeouts: 13 now = self.time() 14 while self._timeouts: 15 if self._timeouts[0].callback is None: 16 # the timeout was cancelled 17 heapq.heappop(self._timeouts) 18 elif self._timeouts[0].deadline <= now: 19 timeout = heapq.heappop(self._timeouts) 20 self._run_callback(timeout.callback) 21 else: 22 seconds = self._timeouts[0].deadline - now 23 poll_timeout = min(seconds, poll_timeout) 24 break 25 26 if self._callbacks: 27 # If any callbacks or timeouts called add_callback, 28 # we don't want to wait in poll() before we run them. 29 poll_timeout = 0.0 30 31 if not self._running: 32 break 33 34 if self._blocking_signal_threshold is not None: 35 # clear alarm so it doesn't fire while poll is waiting for 36 # events. 37 signal.setitimer(signal.ITIMER_REAL, 0, 0) 38 39 try: 40 event_pairs = self._impl.poll(poll_timeout) 41 except Exception as e: 42 # Depending on python version and IOLoop implementation, 43 # different exception types may be thrown and there are 44 # two ways EINTR might be signaled: 45 # * e.errno == errno.EINTR 46 # * e.args is like (errno.EINTR, 'Interrupted system call') 47 if (getattr(e, 'errno', None) == errno.EINTR or 48 (isinstance(getattr(e, 'args', None), tuple) and 49 len(e.args) == 2 and e.args[0] == errno.EINTR)): 50 continue 51 else: 52 raise 53 54 if self._blocking_signal_threshold is not None: 55 signal.setitimer(signal.ITIMER_REAL, 56 self._blocking_signal_threshold, 0) 57 58 # Pop one fd at a time from the set of pending fds and run 59 # its handler. Since that handler may perform actions on 60 # other file descriptors, there may be reentrant calls to 61 # this IOLoop that update self._events 62 self._events.update(event_pairs) 63 while self._events: 64 fd, events = self._events.popitem() 65 try: 66 self._handlers[fd](fd, events) 67 except (OSError, IOError) as e: 68 if e.args[0] == errno.EPIPE: 69 # Happens when the client closes the connection 70 pass 71 else: 72 app_log.error("Exception in I/O handler for fd %s", 73 fd, exc_info=True) 74 except Exception: 75 app_log.error("Exception in I/O handler for fd %s", 76 fd, exc_info=True)
首先是设定超时时间。然后在互斥锁下取出上次循环遗留下的回调列表(在add_callback添加对象),把这次列表置空,然后依次执行列表里的回调。这里的_run_callback就没什么好分析的了。紧接着是检查上次循环遗留的超时列表,如果列表里的项目有回调而且过了截止时间,那肯定超时了,就执行对应的超时回调。然后检查是否又有了事件回调(因为很多回调函数里可能会再添加回调),如果是,则不在poll循环里等待,如注释所述。接下来最关键的一句是event_pairs = self._impl.poll(poll_timeout),这句里的_impl是epoll,在platform/epoll.py里定义,总之就是一个等待函数,当有事件(超时也算)发生就返回。然后把事件集保存下来,对于每个事件,self._handlers[fd](fd, events)根据fd找到回调,并把fd和事件做参数回传。如果fd是监听的fd,那么这个回调handler就是accept_handler函数,详见上节代码。如果是新fd可读,一般就是_on_headers 或者 _on_requet_body了,详见前几节。我好像没看到可写时的回调?以上,就是循环的流程了。可能还是看的糊里糊涂的,因为很多对象怎么来的都不清楚,configurable也还没有看。看完下面的分析,应该就可以了。
Configurable类在util.py里被定义。类里有一段注释,已经很明确的说明了它的设计意图和用法。它是可配置接口的父类,可配置接口对外提供一致的接口标识,但它的子类实现可以在运行时进行configure。一般在跨平台时由于子类实现有多种选择,这时候就可以使用可配置接口,例如select和epoll。首先注意 Configurable 的两个函数: configurable_base 和 configurable_default, 两函数都需要被子类(即可配置接口类)覆盖重写。其中,base函数一般返回接口类自身,default返回接口的默认子类实现,除非接口指定了__impl_class。IOLoop及其子类实现都没有初始化函数也没有构造函数,其构造函数继承于Configurable,如下:
01 def __new__(cls, **kwargs): 02 base = cls.configurable_base() 03 args = {} 04 if cls is base: 05 impl = cls.configured_class() 06 if base.__impl_kwargs: 07 args.update(base.__impl_kwargs) 08 else: 09 impl = cls 10 args.update(kwargs) 11 instance = super(Configurable, cls).__new__(impl) 12 # initialize vs __init__ chosen for compatiblity with AsyncHTTPClient 13 # singleton magic. If we get rid of that we can switch to __init__ 14 # here too. 15 instance.initialize(**args) 16 return instance
当子类对象被构造时,子类__new__被调用,因此参数里的cls指的是Configurabel的子类(可配置接口类,如IOLoop)。先是得到base,查看IOLoop的代码发现它返回的是自身类。由于base和cls是一样的,所以调用configured_class()得到接口的子类实现,其实就是调用base(现在是IOLoop)的configurable_default,总之就是返回了一个子类实现(epoll/kqueue/select之一),顺便把__impl_kwargs合并到args里。接着把kwargs并到args里。然后调用Configurable的父类(Object)的__new__方法,生成了一个impl的对象,紧接着把args当参数调用该对象的initialize(继承自PollIOloop,其initialize下段进行分析),返回该对象。
所以,当构造IOLoop对象时,实际得到的是EPollIOLoop或其它相似子类。另外,Configurable 还提供configure方法来给接口指定实现子类和参数。可以看的出来,Configurable类主要提供构造方法,相当于对象工厂根据配置来生产对象,同时开放configure接口以供配置。而子类按照约定调整配置即可得到不同对象,代码得到了复用。
解决了构造,来看看IOLoop的instance方法。先检查类是否有成员_instance,一开始肯定没有,于是就构造了一个IOLoop对象(即EPollIOLoop对象)。以后如果再调用instance,得到的则是已有的对象,这样就确保了ioloop在全局是单例。再看epoll循环时注意到self._impl,Configurable 和 IOLoop 里都没有, 这是在哪儿定义的呢? 为什么IOLoop的start跑到PollIOLoop里,应该是EPollIOLoop才对啊。 对,应该看出来了,EPollIOLoop 就是PollIOLoop的子类,所以方法被继承了是很常见的哈。
从上一段的构造流程里可以看到,EPollIOLoop对象的initialize方法被调用了,看其代码发现它调用了其父类(PollIOLoop)的它方法, 并指定了impl=select.epoll(), 然后在父类的方法里就把它保存了下来,所以self._impl.poll就等效于select.epoll().poll().PollIOLoop里还有一些注册,修改,删除监听事件的方法,其实就是对self._impl的封装调用。就如上节的 add_accept_handler 就是调用ioloop的add_handler方法把监听fd和accept_handler方法进行关联。
IOLoop基本是个事件循环,因此它总是被其它模块所调用。而且为了足够通用,基本上对回调没多大限制,一个可执行对象即可。事件分发就到此结束了,和IO事件密切相关的另一个部分是IOStream,看看它是如何读写的。
十四、Tornado IOLoop instance()方法的讲解 理解IOLoop
Tornado 的源码写得有点难懂,需要你理解好 socket、epoll 这样的东西才能充分理解。需要深入到 Tornado 的源码,ioloop.py 这个文件很关键。
接下来,我们继续读 ioloop.py 这个文件。
IOLoop 是基于 epoll 实现的底层网络I/O的核心调度模块,用于处理 socket 相关的连接、响应、异步读写等网络事件。每个 Tornado 进程都会初始化一个全局唯一的 IOLoop 实例,在 IOLoop 中通过静态方法 instance() 进行封装,获取 IOLoop 实例直接调用此方法即可。
01 @staticmethod 02 def instance(): 03 """Returns a global `IOLoop` instance. 04 05 Most applications have a single, global `IOLoop` running on the 06 main thread. Use this method to get this instance from 07 another thread. To get the current thread's `IOLoop`, use `current()`. 08 """ 09 if not hasattr(IOLoop, "_instance"): 10 with IOLoop._instance_lock: 11 if not hasattr(IOLoop, "_instance"): 12 # New instance after double check 13 IOLoop._instance = IOLoop() 14 return IOLoop._instance
Tornado 服务器启动时会创建监听 socket,并将 socket 的 file descriptor 注册到 IOLoop 实例中,IOLoop 添加对 socket 的IOLoop.READ 事件监听并传入回调处理函数。当某个 socket 通过 accept 接受连接请求后调用注册的回调函数进行读写。接下来主要分析IOLoop 对 epoll 的封装和 I/O 调度具体实现。
epoll是Linux内核中实现的一种可扩展的I/O事件通知机制,是对POISX系统中 select 和 poll 的替代,具有更高的性能和扩展性,FreeBSD中类似的实现是kqueue。Tornado中基于Python C扩展实现的的epoll模块(或kqueue)对epoll(kqueue)的使用进行了封装,使得IOLoop对象可以通过相应的事件处理机制对I/O进行调度。具体可以参考前面小节的 预备知识:我读过的对epoll最好的讲解 。
IOLoop模块对网络事件类型的封装与epoll一致,分为READ / WRITE / ERROR三类,具体在源码里呈现为:
1 # Our events map exactly to the epoll events 2 NONE = 0 3 READ = _EPOLLIN 4 WRITE = _EPOLLOUT 5 ERROR = _EPOLLERR | _EPOLLHUP
回到前面的里面的示例,
1 http_server = tornado.httpserver.HTTPServer(application) 2 http_server.listen(options.port) 3 tornado.ioloop.IOLoop.instance().start()
前两句是启动服务器,启动服务器之后,还需要启动 IOLoop 的实例,这样可以启动事件循环机制,配合非阻塞的 HTTP Server 工作。
这就是 IOLoop 的 instance() 方法的一些细节,接下来我们再看看 start() 的细节。
十五、 Tornado IOLoop start()里的核心调度 理解IOLoop
IOLoop的初始化
初始化过程中选择 epoll 的实现方式,Linux 平台为 epoll,BSD 平台为 kqueue,其他平台如果安装有C模块扩展的 epoll 则使用 tornado对 epoll 的封装,否则退化为 select。
01 def __init__(self, impl=None): 02 self._impl = impl or _poll() 03 #省略部分代码 04 self._waker = Waker() 05 self.add_handler(self._waker.fileno(), 06 lambda fd, events: self._waker.consume(), 07 self.READ) 08 09 def add_handler(self, fd, handler, events): 10 """Registers the given handler to receive the given events for fd.""" 11 self._handlers[fd] = stack_context.wrap(handler) 12 self._impl.register(fd, events | self.ERROR)
在 IOLoop 初始化的过程中创建了一个 Waker 对象,将 Waker 对象 fd 的读端注册到事件循环中并设定相应的回调函数(这样做的好处是当事件循环阻塞而没有响应描述符出现,需要在最大 timeout 时间之前返回,就可以向这个管道发送一个字符)。
Waker 的使用:一种是在其他线程向 IOLoop 添加 callback 时使用,唤醒 IOLoop 同时会将控制权转移给 IOLoop 线程并完成特定请求。唤醒的方法向管道中写入一个字符'x'。另外,在 IOLoop的stop 函数中会调用self._waker.wake(),通过向管道写入'x'停止事件循环。
add_handler 函数使用了stack_context 提供的 wrap 方法。wrap 返回了一个可以直接调用的对象并且保存了传入之前的堆栈信息,在执行时可以恢复,这样就保证了函数的异步调用时具有正确的运行环境。
IOLoop的start方法
IOLoop 的核心调度集中在 start() 方法中,IOLoop 实例对象调用 start 后开始 epoll 事件循环机制,该方法会一直运行直到 IOLoop 对象调用 stop 函数、当前所有事件循环完成。start 方法中主要分三个部分:一个部分是对超时的相关处理;一部分是 epoll 事件通知阻塞、接收;一部分是对 epoll 返回I/O事件的处理。
- 为防止 IO event starvation,将回调函数延迟到下一轮事件循环中执行。
- 超时的处理 heapq 维护一个最小堆,记录每个回调函数的超时时间(deadline)。每次取出 deadline 最早的回调函数,如果callback标志位为 True 并且已经超时,通过 _run_callback 调用函数;如果没有超时需要重新设定 poll_timeout 的值。
- 通过 self._impl.poll(poll_timeout) 进行事件阻塞,当有事件通知或超时时 poll 返回特定的 event_pairs。
- epoll 返回通知事件后将新事件加入待处理队列,将就绪事件逐个弹出,通过stack_context.wrap(handler)保存的可执行对象调用事件处理。
01 while True: 02 poll_timeout = 3600.0 03 04 with self._callback_lock: 05 callbacks = self._callbacks 06 self._callbacks = [] 07 for callback in callbacks: 08 self._run_callback(callback) 09 10 # 超时处理 11 if self._timeouts: 12 now = time.time() 13 while self._timeouts: 14 if self._timeouts[0].callback is None: 15 # the timeout was cancelled 16 heapq.heappop(self._timeouts) 17 elif self._timeouts[0].deadline <= now: 18 timeout = heapq.heappop(self._timeouts) 19 self._run_callback(timeout.callback) 20 else: 21 seconds = self._timeouts[0].deadline - now 22 poll_timeout = min(seconds, poll_timeout) 23 break 24 25 if self._callbacks: 26 # If any callbacks or timeouts called add_callback, 27 # we don't want to wait in poll() before we run them. 28 poll_timeout = 0.0 29 30 if not self._running: 31 break 32 33 if self._blocking_signal_threshold is not None: 34 # clear alarm so it doesn't fire while poll is waiting for events. 35 signal.setitimer(signal.ITIMER_REAL, 0, 0) 36 37 # epoll阻塞,当有事件通知或超时返回event_pairs 38 try: 39 event_pairs = self._impl.poll(poll_timeout) 40 except Exception, e: 41 # 异常处理,省略 42 43 # 对epoll返回event_pairs事件的处理 44 self._events.update(event_pairs) 45 while self._events: 46 fd, events = self._events.popitem() 47 try: 48 self._handlers[fd](fd, events) 49 except Exception e: 50 # 异常处理,省略
3.0后的一些改动
Tornado3.0以后 IOLoop 模块的一些改动。
IOLoop 成为 util.Configurable 的子类,IOLoop 中绝大多数成员方法都作为抽象接口,具体实现由派生类 PollIOLoop 完成。IOLoop 实现了 Configurable 中的 configurable_base 和 configurable_default 这两个抽象接口,用于初始化过程中获取类类型和类的实现方法(即 IOLoop 中 poller 的实现方式)。
在 Tornado3.0+ 中针对不同平台,单独出 poller 相应的实现,EPollIOLoop、KQueueIOLoop、SelectIOLoop 均继承于 PollIOLoop。下边的代码是 configurable_default 方法根据平台选择相应的 epoll 实现。初始化 IOLoop 的过程中会自动根据平台选择合适的 poller 的实现方法。
01 @classmethod 02 def configurable_default(cls): 03 if hasattr(select, "epoll"): 04 from tornado.platform.epoll import EPollIOLoop 05 return EPollIOLoop 06 if hasattr(select, "kqueue"): 07 # Python 2.6+ on BSD or Mac 08 from tornado.platform.kqueue import KQueueIOLoop 09 return KQueueIOLoop 10 from tornado.platform.select import SelectIOLoop 11 return SelectIOLoop
其他有很多细节上的改动,详细可参见官方文档What’s new in Tornado 3.0
十六、Tornado IOLoop与Configurable类 理解IOLoop的机制
IOLoop 是 tornado 的核心。程序中主函数通常调用 tornado.ioloop.IOLoop.instance().start() 来启动IOLoop,但是看了一下 IOLoop 的实现,start 方法是这样的:
1 def start(self): 2 """Starts the I/O loop. 3 4 The loop will run until one of the callbacks calls `stop()`, which 5 will make the loop stop after the current event iteration completes. 6 """ 7 raise NotImplementedError()
也就是说 IOLoop 是个抽象的基类,具体工作是由它的子类负责的。由于是 Linux 平台,所以应该用 Epoll,对应的类是 PollIOLoop。PollIOLoop 的 start 方法开始了事件循环。
问题来了,tornado.ioloop.IOLoop.instance() 是怎么返回 PollIOLoop 实例的呢?刚开始有点想不明白,后来看了一下 IOLoop 的代码就豁然开朗了。
IOLoop 继承自 Configurable,后者位于 tornado/util.py。
A configurable interface is an (abstract) class whose constructor acts as a factory function for one of its implementation subclasses. The implementation subclass as well as optional keyword arguments to its initializer can be set globally at runtime with configure.
Configurable 类实现了一个工厂方法,也就是设计模式中的“工厂模式”,看一下__new__函数的实现:
01 def __new__(cls, **kwargs): 02 base = cls.configurable_base() 03 args = {} 04 if cls is base: 05 impl = cls.configured_class() 06 if base.__impl_kwargs: 07 args.update(base.__impl_kwargs) 08 else: 09 impl = cls 10 args.update(kwargs) 11 instance = super(Configurable, cls).__new__(impl) 12 # initialize vs __init__ chosen for compatiblity with AsyncHTTPClient 13 # singleton magic. If we get rid of that we can switch to __init__ 14 # here too. 15 instance.initialize(**args) 16 return instance
当创建一个Configurable类的实例的时候,其实创建的是configurable_class()返回的类的实例。
01 def __new__(cls, **kwargs): 02 base = cls.configurable_base() 03 args = {} 04 if cls is base: 05 impl = cls.configured_class() 06 if base.__impl_kwargs: 07 args.update(base.__impl_kwargs) 08 else: 09 impl = cls 10 args.update(kwargs) 11 instance = super(Configurable, cls).__new__(impl) 12 # initialize vs __init__ chosen for compatiblity with AsyncHTTPClient 13 # singleton magic. If we get rid of that we can switch to __init__ 14 # here too. 15 instance.initialize(**args) 16 return instance
最后,就是返回的configurable_default()。此函数在IOLoop中的实现如下:
01 @classmethod 02 def configurable_default(cls): 03 if hasattr(select, "epoll"): 04 from tornado.platform.epoll import EPollIOLoop 05 return EPollIOLoop 06 if hasattr(select, "kqueue"): 07 # Python 2.6+ on BSD or Mac 08 from tornado.platform.kqueue import KQueueIOLoop 09 return KQueueIOLoop 10 from tornado.platform.select import SelectIOLoop 11 return SelectIOLoop
EPollIOLoop 是 PollIOLoop 的子类。至此,这个流程就理清楚了。
十七、 弄清楚HTTPServer与Request处理流程 tornaode web研究
TCPServer.bind_sockets()会返回一个socket对象的列表,列表中的socket都是用来监听客户端连接的。
列表由TCPServer.add_sockets()处理。在这个函数里我们就会看到IOLoop相关的东西。
def add_sockets(self, sockets): if self.io_loop is None: self.io_loop = IOLoop.current() for sock in sockets: self._sockets[sock.fileno()] = sock add_accept_handler(sock, self._handle_connection, io_loop=self.io_loop)
首先,io_loop是TCPServer的一个成员变量,这说明每个TCPServer都绑定了一个io_loop。注意,跟传统的做法不同,ioloop不是跟socket一一对应,而是跟TCPServer一一对应。也就是说,一个Server上即使有多个listening socket,他们也是由同一个ioloop在处理。
前面提到过,HTTPServer的初始化可以带一个ioloop参数,最终它会被赋值给TCPServer的成员。如果没有带ioloop参数(如helloworld.py所展示的),TCPServer则会自己倒腾一个,即IOLoop.current()。
add_accept_handler()定义在netutil.py中(bind_sockets在这里)。它的代码并复杂,如下:
def add_accept_handler(sock, callback, io_loop=None): if io_loop is None: io_loop = IOLoop.current() def accept_handler(fd, events): while True: try: connection, address = sock.accept() except socket.error as e: # EWOULDBLOCK and EAGAIN indicate we have accepted every # connection that is available. if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): return # ECONNABORTED indicates that there was a connection # but it was closed while still in the accept queue. # (observed on FreeBSD). if e.args[0] == errno.ECONNABORTED: continue raise callback(connection, address) io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
文档中说,IOLoop.current()返回当前线程的IOLoop对象。可能有点不好理解。
实际上IOLoop的实例就相当于一个线程,有start, run, stop这些函数。IOLoop实例都包含一个叫_current的成员,指向创建它的线程。每个线程在创建IOLoop时,都被绑定到创建它的线程上。在IOLoop的类定义中,有这么一行:
_current = threading.local()
创建好IOLoop后,下面又定义了一个accept_handler。这是一个函数内定义的函数。
accept_handler相当于连接监视器的,所以我们把它绑定到listening socket上。
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
它正式对socket句柄调用 accept。当接收到一个connection后,调用callback()。不难想到,callback就是客户端连上来后对应的响应函数。
回到add_sockets函数里看:
add_accept_handler(sock, self._handle_connection, io_loop=self.io_loop)
callback就是我们传进去的TCPServer._handle_connection()函数。TCPServer._handle_connection()本身不复杂。前面大段都是在处理SSL事务,把这些东西都滤掉的话,实际上代码很少。
def _handle_connection(self, connection, address): try: stream = IOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size) self.handle_stream(stream, address) except Exception: app_log.error("Error in connection callback", exc_info=True)
思路是很清晰的,客户端连接在这里被转化成一个IOStream。然后由handle_stream函数处理。
这个handle_stream就是我们前面提到过的未直接实现的接口,它是由HTTPServer类实现的。
def handle_stream(self, stream, address): HTTPConnection(stream, address, self.request_callback, self.no_keep_alive, self.xheaders, self.protocol)
最后,处理流程又回到了HTTPServer类中。可以预见,在HTTConnection这个类中,stream将和我们注册的RequestHandler协作,一边读客户端请求,一边调用相应的handler处理。
总结一下:
listening socket创建起来以后,我们给它绑定一个响应函数叫accept_handler。当用户连接上来时,会触发listening socket上的事件,然后accept_handler被调用。accept_handler在listening socket上获得一个针对该用户的新socket。这个socket专门用来跟用户做数据沟通。TCPServer把这个socket封闭成一个IOStream,最后交到HTTPServer的handle_stream里。
经过一翻周围,用户连接后的HTTP通讯终于被我们导入到了HTTPConnection。在HTTPConnection里我们将看到熟悉的HTTP通信协议的处理。
HTTPConnection类也定义在httpserver.py中,它的构造函数如下:
def __init__(self, stream, address, request_callback, no_keep_alive=False, xheaders=False, protocol=None): self.stream = stream self.address = address # Save the socket's address family now so we know how to # interpret self.address even after the stream is closed # and its socket attribute replaced with None. self.address_family = stream.socket.family self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders self.protocol = protocol self._clear_request_state() # Save stack context here, outside of any request. This keeps # contexts from one request from leaking into the next. self._header_callback = stack_context.wrap(self._on_headers) self.stream.set_close_callback(self._on_connection_close) self.stream.read_until(b"\r\n\r\n", self._header_callback)
常规的参数保存动作,还有一些初始化、清理动作,最后一句开始办正事:
self.stream.read_until(b”\r\n\r\n”, self._header_callback)
从socket中读数据,直到读到”\r\n\r\n”为止。这是HTTP头部结束标志,读到的数据就会由self._header_callback处理。
经过stack_context.wrap()的传递,HTTP头会交给HTTPConnection._on_headers()。
HTTPConnection._on_headers()完成了HTTP头的分析,具体过程这里就不必详述了(如果有写HTTPServer需求,倒是可以借鉴一下)。
经过一轮校验与分析,HTTP头(注意,只是HTTP头部哦)被组装成一个HTTPRequest对象。(HTTP Body如果数据量不是太大,就直接放进了一个buffer里,就叫self._on_request_body。)
self._request = HTTPRequest( connection=self, method=method, uri=uri, version=version, headers=headers, remote_ip=remote_ip, protocol=self.protocol)
HTTPRequest对象被交给了(其实HTTP Body最后也是交给他的……)
self.request_callback(self._request)
这个request_callback是什么来头呢?它是在HTTPConnection构造时传进来的参数。我们回到HTTPServer.handle_stream()
def handle_stream(self, stream, address): HTTPConnection(stream, address, self.request_callback, self.no_keep_alive, self.xheaders, self.protocol)
它是一个HTTPServer类的成员,继续往回追来历:
def __init__(self, request_callback, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None, protocol=None, **kwargs): self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders self.protocol = protocol TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, **kwargs)
Bingo!这就是HTTPServer初始化时传进来的那个RequestHandler。
在helloworld.py里,我们看到的是:
application = tornado.web.Application([(r"/", MainHandler), ]) http_server = tornado.httpserver.HTTPServer(application)
在另一个例子里,我们看到的是:
def handle_request(request): message = "You requested %s\n" % request.uri request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % ( len(message), message)) request.finish() http_server = tornado.httpserver.HTTPServer(handle_request)
可见这个request_handler通吃很多种类型的参数,可以是一个Application类的对象,也可是一个简单的函数。
如果是handler是简单函数,如上面的handle_request,这个很好理解,由一个函数处理HTTPRequest对象嘛。
如果是一个Application对象,就有点奇怪了。我们能把一个对象作另一个对象的参数来呼叫吗?
Python中有一个有趣的语法,只要定义类型的时候,实现__call__函数,这个类型就成为可调用的。换句话说,我们可以把这个类的对象当作函数来使用,相当于重载了括号运算符。
十八、 对socket封装的IOStream机制概 IOStream是什么?
IOStream对socket读写进行了封装,分别提供读、写缓冲区实现对socket的异步读写。当socket被accept之后HTTPServer的_handle_connection会被回调并初始化IOStream对象,进一步通过IOStream提供的功能接口完成socket的读写。文章接下来将关注IOStream实现读写的细节。
IOStream的初始化
IOStream初始化过程中主要完成以下操作:
- 绑定对应的socket
- 绑定ioloop
- 创建读缓冲区_read_buffer,一个python deque容器
- 创建写缓冲区_write_buffer,同样也是一个python deque容器
IOStream提供的主要功能接口
主要的读写接口包括以下四个:
1 class IOStream(object): 2 def read_until(self, delimiter, callback): 3 def read_bytes(self, num_bytes, callback, streaming_callback=None): 4 def read_until_regex(self, regex, callback): 5 def read_until_close(self, callback, streaming_callback=None): 6 def write(self, data, callback=None):
- read_until和read_bytes是最常用的读接口,它们工作的过程都是先注册读事件结束时调用的回调函数,然后调用_try_inline_read方法。_try_inline_read首先尝试_read_from_buffer,即从上一次的读缓冲区中取数据,如果有数据直接调用 self._run_callback(callback, self._consume(data_length)) 执行回调函数,_consume消耗掉了_read_buffer中的数据;否则即_read_buffer之前没有未读数据,先通过_read_to_buffer将数据从socket读入_read_buffer,然后再执行_read_from_buffer操作。read_until和read_bytes的区别在于_read_from_buffer过程中截取数据的方法不同,read_until读取到delimiter终止,而read_bytes则读取num_bytes个字节终止。执行过程如下图所示:
- read_until_regex相当于delimiter为某一正则表达式的read_until。
- read_until_close主要用于IOStream流关闭前后的读取:如果调用read_until_close时stream已经关闭,那么将会_consume掉_read_buffer中的所有数据;否则_read_until_close标志位设为True,注册_streaming_callback回调函数,调用_add_io_state添加io_loop.READ状态。
- write首先将data按照数据块大小WRITE_BUFFER_CHUNK_SIZE分块写入write_buffer,然后调用handle_write向socket发送数据。
其他内部功能接口
- def _handle_events(self, fd, events): 通常为IOLoop对象add_handler方法传入的回调函数,由IOLoop的事件机制来进行调度。
- def _add_io_state(self, state): 为IOLoop对象的handler注册IOLoop.READ或IOLoop.WRITE状态,handler为IOStream对象的_handle_events方法。
- def _consume(self, loc): 合并读缓冲区loc个字节,从读缓冲区删除并返回这些数据。
十九、 IOStream实现读写的一些细节 IOStream分析
对于 IOStream,整体的认识就是,它负责IO读写,顺便回调。
先认识一个工具函数:_merge_prefix。它的作用是将双端队列(deque)的首项调整为指定大小,如果明白双端队列的popleft和appendleft方法,这个函数还是很容易看懂的,我略过对它的分析。接下来是非阻塞读写基类BaseIOStream。首先是__init__方法,记录了ioloop(毕竟要实现异步非阻塞,ioloop是必须的),然后初始化了两个缓冲区双端队列和缓冲区大小:
1 self.max_buffer_size = max_buffer_size 2 self.read_chunk_size = read_chunk_size 3 self.error = None 4 self._read_buffer = collections.deque() 5 self._write_buffer = collections.deque()
以及将其它标志置为默认none。很明显,io的最底层就是缓冲区了,先来看关于读缓冲区的两个方法: _read_to_buffer 和 _read_from_buffer 外加一个_consume方法。先看第一个函数:
01 def _read_to_buffer(self): 02 """Reads from the socket and appends the result to the read buffer. 03 04 Returns the number of bytes read. Returns 0 if there is nothing 05 to read (i.e. the read returns EWOULDBLOCK or equivalent). On 06 error closes the socket and raises an exception. 07 """ 08 try: 09 chunk = self.read_from_fd() 10 except (socket.error, IOError, OSError) as e: 11 # ssl.SSLError is a subclass of socket.error 12 if e.args[0] == errno.ECONNRESET: 13 # Treat ECONNRESET as a connection close rather than 14 # an error to minimize log spam (the exception will 15 # be available on self.error for apps that care). 16 self.close(exc_info=True) 17 return 18 self.close(exc_info=True) 19 raise 20 if chunk is None: 21 return 0 22 self._read_buffer.append(chunk) 23 self._read_buffer_size += len(chunk) 24 if self._read_buffer_size >= self.max_buffer_size: 25 gen_log.error("Reached maximum read buffer size") 26 self.close() 27 raise IOError("Reached maximum read buffer size") 28 return len(chunk)
首先是调用read_from_fd函数(由子类覆盖重写,简单的认为就是fd .read())得到chunk。一般fd可读时操作系统缓冲区里都会有一定长度的chunk,所以一般总是能得到某个chunk(但不一定是符合预期的chunk,比如我希望将所有的内容读完直到结束,但系统缓冲区里不一定就放的下。。。)。得到chunk后,把它放到自己的缓冲区里(这样操作系统的缓冲区就可以复用为新内容服务)并增加buffesize,检查是否超过了缓冲区最大允许容量,最后返回chunk的大小。
接下来是 _consume,它用作从自身缓冲区中取出指定长度的内容。代码就不贴了,流程很简单,先_merge_prefix使缓冲区首项符合指定大小,再popleft弹出首项并调整buffersize即可。然后是read_from_buffer,这个函数比较重要了,因为iostream需要支持很多种读的方式,例如rea_until,read_bytes,read_regex等,这些模式和对应的callback都是在这个函数里被实现和调用的:
01 def _read_from_buffer(self): 02 """Attempts to complete the currently-pending read from the buffer. 03 04 Returns True if the read was completed. 05 """ 06 if self._streaming_callback is not None and self._read_buffer_size: 07 bytes_to_consume = self._read_buffer_size 08 if self._read_bytes is not None: 09 bytes_to_consume = min(self._read_bytes, bytes_to_consume) 10 self._read_bytes -= bytes_to_consume 11 self._run_callback(self._streaming_callback, 12 self._consume(bytes_to_consume)) 13 if self._read_bytes is not None and self._read_buffer_size >=self._read_bytes: 14 num_bytes = self._read_bytes 15 callback = self._read_callback 16 self._read_callback = None 17 self._streaming_callback = None 18 self._read_bytes = None 19 self._run_callback(callback, self._consume(num_bytes)) 20 return True 21 elif self._read_delimiter is not None: 22 # Multi-byte delimiters (e.g. '\r\n') may straddle two 23 # chunks in the read buffer, so we can't easily find them 24 # without collapsing the buffer. However, since protocols 25 # using delimited reads (as opposed to reads of a known 26 # length) tend to be "line" oriented, the delimiter is likely 27 # to be in the first few chunks. Merge the buffer gradually 28 # since large merges are relatively expensive and get undone in 29 # consume(). 30 if self._read_buffer: 31 while True: 32 loc = self._read_buffer[0].find(self._read_delimiter) 33 if loc != -1: 34 callback = self._read_callback 35 delimiter_len = len(self._read_delimiter) 36 self._read_callback = None 37 self._streaming_callback = None 38 self._read_delimiter = None 39 self._run_callback(callback, 40 self._consume(loc +delimiter_len)) 41 return True 42 if len(self._read_buffer) == 1: 43 break 44 _double_prefix(self._read_buffer) 45 elif self._read_regex is not None: 46 if self._read_buffer: 47 while True: 48 m = self._read_regex.search(self._read_buffer[0]) 49 if m is not None: 50 callback = self._read_callback 51 self._read_callback = None 52 self._streaming_callback = None 53 self._read_regex = None 54 self._run_callback(callback, self._consume(m.end())) 55 return True 56 if len(self._read_buffer) == 1: 57 break 58 _double_prefix(self._read_buffer) 59 return False
首先是检查_streaming_callback回调(字符流回调,一般是读操作没有彻底读够而处于streaming状态,一般默认是None,如果调用read_bytes和read_until_close并指定了streaming_callback参数就会造成这个回调)和buffersize。如果read_bytes没有被设定则说明调用的是read_until_close,则直接把buffer里所有内容读出并调用回调,否则的话根据read_bytes和buffersize决定要读取的大小,其它步骤同read_until_close。这只是开胃菜,然后开始判断各个标志来决定回调。不同的标志在各自读函数里分别设置,在此就不一一赘述了。简言之就是根据不同的标志采取不同的条件判断,如果判断成功就回调。
和ioloop异步有关的是两个函数:_add_io_state 和 _handle_events。第一个函数就比较简单了,主要是更新自身的状态并告诉ioloop监听新事件。另一个函数主要是负责事件的分发,将发生的事件和和read/write进行比对并调用响应的回调,同时检查自身状态机的状态(是否在读,是否在写,是否已经关闭等)向ioloop注册新的回调(这个函数有点像memcached里的超级状态机drive_machine,不过很明显tornado比它简单多了(tornado的状态机挺弱的,我觉得)。相应的handle_read和handle_write被调用,handle_read就是调用了read_to_buffer把内容复制进读缓冲区并调用read_from_buffer在条件被满足时执行回调,handle_write就调用write_to_fd 把写缓冲区中的内容移除。
看了半天,终于可以看到iostream对外提供的接口了。以read_bytes为例,它首先设置了回调及读取内容的大小,接着调用_try_inline_read做结。而的_try_inline_read代码如下:
01 def _try_inline_read(self): 02 """Attempt to complete the current read operation from buffered data. 03 04 If the read can be completed without blocking, schedules the 05 read callback on the next IOLoop iteration; otherwise starts 06 listening for reads on the socket. 07 """ 08 # See if we've already got the data from a previous read 09 if self._read_from_buffer(): 10 return 11 self._check_closed() 12 try: 13 # See comments in _handle_read about incrementing _pending_callbacks 14 self._pending_callbacks += 1 15 while not self.closed(): 16 if self._read_to_buffer() == 0: 17 break 18 finally: 19 self._pending_callbacks -= 1 20 if self._read_from_buffer(): 21 return 22 self._maybe_add_error_listener()
首先尝试从自身缓冲区读取,如果失败则反复调用直到close或者缓冲区里没有东西可读(由于设置了fd为非阻塞模式,read不会被阻塞而是返回0),在此尝试从自身缓冲区读取,还是没达到要求的话就调用_maybe_add_error_listener,其实就是开始监听read事件。中间还有个 _pending_callbacks 信号量,作用稍候再说。综上述,当上层调用iostream的read_*方法时,它首先设置回调,然后调用_try_inline_read进行非阻塞式读取,能一次性读到满足条件最好,不行就监听read。当read事件发生时再调用handle_read会做好善后处理(在上一段)。整个过程不会被阻塞,就是回调里跳来跳去的可能会花点时间(和网络延迟比起来简直不值一提)。同理,write函数也是这样,把内容放进自身缓冲区,当write事件到来时再输出,省去了网络延迟。
IOStream相对就比较简单了,主要是实现了socket的读写,叫它SocketStream也许更合适? 最后是关于_pending_callbacks 信号量。首先它总是成对出现,有增就有减,并且总是先增后减。另外,凡是在它减一后总是会执行 _maybe_run_close_callback 或者 _maybe_add_error_listener。是的,没错,_pending_callbacks只对这两个函数起作用。根据注释的说法,信号量的作用是为了防止读缓冲区中部出现的‘’空字符串导致被误认为close,为了防止所有的回调都不会因为空字符串而被close所中断,就使用信号量告诉系统现在暂时不要close。而且由于信号量增减后总是会调用两个函数之一,因此close回调总是会被调用而不会因为信号量而没有得到正确执行)。
以上,就是IO层的代码执行。整个思路大概明晰了许多吧。
二十、Tornado的多进程管理分析 process.py代码解读
Tornado的多进程管理我们可以参看process.py这个文件。
在编写多进程的时候我们一般都用python自带的multiprocessing,使用方法和threading基本一致,只需要继承里面的Process类以后就可以编写多进程程序了,这次我们看看tornado是如何实现他的multiprocessing,可以说实现的功能不多,但是更加简单高效。
我们只看fork_process里面的代码:
01 global _task_id 02 assert _task_id is None 03 if num_processes is None or num_processes <= 0: 04 num_processes = cpu_count() 05 if ioloop.IOLoop.initialized(): 06 raise RuntimeError("Cannot run in multiple processes: IOLoop instance " 07 "has already been initialized. You cannot call " 08 "IOLoop.instance() before calling start_processes()") 09 logging.info("Starting %d processes", num_processes) 10 children = {}
这一段很简单,就是在没有传入进程数的时候使用默认的cpu个数作为将要生成的进程个数。
01 def start_child(i): 02 pid = os.fork() 03 if pid == 0: 04 # child process 05 _reseed_random() 06 global _task_id 07 _task_id = i 08 return i 09 else: 10 children[pid] = i 11 return None
这是一个内函数,作用就是生成子进程。fork是个很有意思的方法,他会同时返回两种状态,为什么呢?其实fork相当于在原有的一条路(父进程)旁边又修了一条路(子进程)。如果这条路修成功了,那么在原有的路上(父进程)你就看到旁边来了另外一条路(子进程),所以也就是返回新生成的那条路的名字(子进程的pid),但是在另外一条路上(子进程),你看到的是自己本身修建成功了,也就返回自己的状态码(返回结果是0)。
所以if pid==0表示这时候cpu已经切换到子进程了,相当于我们在新生成的这条路上面做事(返回任务id);else表示又跑到原来的路上做事了,在这里我们记录下新生成的子进程,这时候children[pid]=i里面的pid就是新生成的子进程的pid,而 i 就是刚才在子进程里面我们返回的任务id(其实就是用来代码子进程的id号)。
1 for i in range(num_processes): 2 id = start_child(i) 3 if id is not None: 4 return id
if id is not None表示如果我们在刚刚生成的那个子进程的上下文里面,那么就什么都不干,直接返回子进程的任务id就好了,啥都别想了,也别再折腾。如果还在父进程的上下文的话那么就继续生成子进程。
01 num_restarts = 0 02 while children: 03 try: 04 pid, status = os.wait() 05 except OSError, e: 06 if e.errno == errno.EINTR: 07 continue 08 raise 09 if pid not in children: 10 continue 11 id = children.pop(pid) 12 if os.WIFSIGNALED(status): 13 logging.warning("child %d (pid %d) killed by signal %d, restarting", 14 id, pid, os.WTERMSIG(status)) 15 elif os.WEXITSTATUS(status) != 0: 16 logging.warning("child %d (pid %d) exited with status %d, restarting", 17 id, pid, os.WEXITSTATUS(status)) 18 else: 19 logging.info("child %d (pid %d) exited normally", id, pid) 20 continue 21 num_restarts += 1 22 if num_restarts > max_restarts: 23 raise RuntimeError("Too many child restarts, giving up") 24 new_id = start_child(id) 25 if new_id is not None: 26 return new_id
剩下的这段代码都是在父进程里面做的事情(因为之前在子进程的上下文的时候已经返回了,当然子进程并没有结束)。
pid, status = os.wait()的意思是等待任意子进程退出或者结束,这时候我们就把它从我们的children表里面去除掉,然后通过status判断子进程退出的原因。
如果子进程是因为接收到kill信号或者抛出exception了,那么我们就重新启动一个子进程,用的当然还是刚刚退出的那个子进程的任务号。如果子进程是自己把事情做完了才退出的,那么就算了,等待别的子进程退出吧。
我们看到在重新启动子进程的时候又使用了
1 if new_id is not None: 2 return new_id
主要就是退出子进程的空间,只在父进程上面做剩下的事情,不然刚才父进程的那些代码在子进程里面也会同样的运行,就会形成无限循环了,我没试过