0%

tornado.web 模块解析

引言

在前面 Tornado 的分析文章中已经详细介绍了 HttpServer 接收处理客户端 Http 连接请求,并将请求委托给请求回调(即 HttpServer.request_callback 字段,由 HttpServer 的构造参数来完成初始化)处理的流程。基于 HttpServer 的支持,tornado.web 模块为我们提供了一个简单的 Web 框架,该框架支持路由请求到对应的(自定义/默认)请求处理器(RequestHandler)并自带一个简易的 HTML 模板引擎。在分析 tornado.web 模块的设计之前,先通过一张图来回顾一下 HttpServer 的处理流程,看看 web.tornado 提供的框架是如何获得支持的。

由上图可知:

  1. httpsever.HttpServer 继承自 tcpserver.TCPServer 并覆写了 handle_stream(stream, address) 方法。

  2. httpsever.HttpServer.handle_stream(stream, address) 方法中将请求相关的 IOStream、地址和协议描述(http/https)保证成 httpsever._HTTPRequestContext 实例以构建 http1connection.HTTP1ServerConnection 实例,并调用 Http1ServerConnection.start_serving 方法启动请求处理。

  3. http1connection.HTTP1ServerConnection 是一个支持 Http/1.x 的服务端抽象连接类型,也就是说能支持 Http/1.1 的长连接。在该连接实例服务周期中(实现在其 _server_request_loop 方法的 while 循环),其内部对于每一次 Http 请求会生成一个 http1connection.HTTP1Connection 实例。

    • http1connection.Http1ServerConnection.start_serving(delegate:httputil.HTTPServerConnectionDelegate) 方法要求接收一个 httputil.HTTPServerConnectionDelegate 实例,该实例的 start_request(server_conn, request_conn:HTTP1Connection) 方法返回一个 httputil.HTTPMessageDelegate 实例,该实例将与 http1connection.HTTP1Connection 实例配合处理 Http 请求的各个阶段。

    • http1connection.HTTP1Connectionhttputil.HTTPConnection 的 Http/1.x 子类实现,是每次 Http 连接请求的抽象,提供了读取请求和数据和发送响应数据的接口。http1connection.HTTP1ServerConnection 抽象了一个服务端的物理连接,http1connection.HTTP1Connection 则抽象了每次 Http 请求的连接,这样一来前者就可以基于同一个物理连接处理多个 Http 请求,也就是支持了 Http 协议要求的长连接。

  4. httpsever.HttpServer 同时继承自 httputil.HTTPServerConnectionDelegate,所以它能作为 http1connection.HTTP1ServerConnection.start_serving() 方法的参数为其提供实际的请求处理。对请求的处理工作 httpsever.HttpServer 实际上是委托给其内部的 request_callback 字段来进行,该字段在 httpsever.HttpServer 构造时初始化。在 Tornado v4.0 之前的版本中,request_callback 是一个以 httputil.HTTPServerRequest 作为参数的回调对象,v4.0 之后引入 httputil.HTTPMessageDelegate 类型,随之 request_callback 改为支持 httputil.HTTPMessageDelegate 实例。为了向后兼容,httpsever.HttpServer.start_request 方法返回一个 httpserver._ServerRequestAdapter 类型实例。httpserver._ServerRequestAdapter 是一个对象适配器,它负责将之前的回调对象(适配者)适配到 httputil.HTTPMessageDelegate 类型。

  5. web.Application 继承自 httputil.HTTPServerConnectionDelegate,可作为请求连接处理回调对象传递给 httpsever.HttpServer, 作为其 request_callback 字段负责处理请求。

到这里,httpsever.HttpServer 的整个处理过程基本回顾了一遍,后面处理将转到 web.Application ,开始进入 Tornado Web Framework 的处理流程。

Tornado Web Framwork

tornado.web 中实现的 Web 框架总的来看主要由 3 个部分(类)组成:

  1. Application 类型继承自 httputil.HTTPServerConnectionDelegate,其实例可直接传递给 HttpServer 来处理客户端连接;

  2. RequestHandler 类型是请求处理的基类型,其子类实例负责处理具体的请求,也就是 Tornado Web 框架分发请求的目标对象;

  3. _RequestDispatcher 是一个模块内部类,继承自 httputil.HTTPMessageDelegate 。它把映射请求到具体 RequestHandler 实例的逻辑从 Application 分离出来,简化 Application 的实现。

也就是说实现上,Application 是一系列 RequestHandler 子类实例的集合,具体请求将根据主机和资源路径进行映射。映射关系则由 URLSpec 类型实例来描述,映射动作的执行(或者说请求分发)则由 _RequestDispatcher 来完成。

URLSpec

URLSpec 用于描述请求 URLs 与 handlers 的映射关系,别名是 url,即通过 web.url 也能引用该类型。URLSpec 的实现从构造方法就可窥见,与 URLs 的映射是通过正则表达式匹配来完成的。下面是其构造方法的签名:

1
2
3

def __init__(self, pattern, handler, kwargs=None, name=None):
pass

  1. pattern 是用来匹配 URL 的正则表达式字符串。通过匹配得到的分组将作为 handler 实例 get/post 等等方法的参数调用;

  2. handler 是将被实例化后用来处理器请求的 RequestHandler 子类,可以是一个 “module.ClassName” 格式的字符串;

  3. kwargshandler 实例化是传递给构造方法的参数;

  4. name 是为给 Application.reverse_url 使用给 handler 取的名字。URLSpec 提供方法 reverse 来配合 Application 工作,但不支持复杂的 pattern ,实现代码不复杂,感兴趣的可以看看。

RequestHandler

RequestHandler 是负责请求的处理(响应)的基类,是 Tornado 简单 Web 框架的重要组成部分。一般我们都会从它派生子类型来处理器具体的请求。RequestHandler 在 Tornado Web Framework 中的位置,有点类似Java Web 中的 Servlet 或者 ASP.NET 中的 HTTPHandler。

RequestHandler 中封装的功能细节比较多,基本上包含了响应的方方面面,这里我们主要关注其处理流程,具体的功能细节后续再分析。RequestHandler 请求处理流程的入口是方法 _execute,该方法会被 _RequestDispatcherexecute 方法中调用。_execute 方法代码不多,清晰表达了 RequestHandler 的处理流程。代码如下所示:

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
@gen.coroutine
def _execute(self, transforms, *args, **kwargs):
"""Executes this request with the given output transforms."""
self._transforms = transforms
try:
if self.request.method not in self.SUPPORTED_METHODS:
raise HTTPError(405)
self.path_args = [self.decode_argument(arg) for arg in args]
self.path_kwargs = dict((k, self.decode_argument(v, name=k))
for (k, v) in kwargs.items())
# If XSRF cookies are turned on, reject form submissions without
# the proper cookie
if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
self.application.settings.get("xsrf_cookies"):
self.check_xsrf_cookie()

result = self.prepare()
if is_future(result):
result = yield result
if result is not None:
raise TypeError("Expected None, got %r" % result)
if self._prepared_future is not None:
# Tell the Application we've finished with prepare()
# and are ready for the body to arrive.
self._prepared_future.set_result(None)
if self._finished:
return

if _has_stream_request_body(self.__class__):
# In streaming mode request.body is a Future that signals
# the body has been completely received. The Future has no
# result; the data has been passed to self.data_received
# instead.
try:
yield self.request.body
except iostream.StreamClosedError:
return

method = getattr(self, self.request.method.lower())
result = method(*self.path_args, **self.path_kwargs)
if is_future(result):
result = yield result
if result is not None:
raise TypeError("Expected None, got %r" % result)
# 就目前版本的实现而言,默认 `self._auto_finish=True`,只有当方法被
# `asynchronous` 装饰时才设置为 False,此时 `self.finish()` 将在
# 异步方法执行完成后回调。
if self._auto_finish and not self._finished:
self.finish()
except Exception as e:
self._handle_request_exception(e)
if (self._prepared_future is not None and
not self._prepared_future.done()):
# In case we failed before setting _prepared_future, do it
# now (to unblock the HTTP server). Note that this is not
# in a finally block to avoid GC issues prior to Python 3.4.
self._prepared_future.set_result(None)

从上面的代码可看出 RequestHandler 中通过类字段 SUPPORTED_METHODS 来定义 Handler 支持的请求方法(Http method)列表,处理方法名字就是 Http Method 名称的小写字符(method = getattr(self, self.request.method.lower())),例如POST 请求对应 post 方法,GET 请求对应 get 方法。若 Handler 支持当前 Http Method,则进入处理流程:execute prepare method -> execute http method -> execute finish method。另外需要指出的一点是该方法被 gen.coroutine 装饰,没有返回值(或者说返回值 None ),但被一个完整的异常处理块包围而不会抛出任何异常,所以在 _RequestDispatcher 中可以不去关心其返回的 Future 实例何时完成,结果是什么,是否在异步执行时发生异常。

Application

Application 是一个 Web 应用的 Request Handler 集合。它支持按照虚拟主机/子域名/泛域名对 handler 进行映射,通过 add_handlers 方法可以指定 handler 对应的主机。Application 实例化时可以指定一些默认的 handler,这些 handler 适用于所有的主机地址(匹配主机地址的正则表达式被设置为 ".*$"),构造方法内也是通过调用 add_handlers 方法完成 handler 添加。下面是 add_handlers 的代码:

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
def add_handlers(self, host_pattern, host_handlers):
"""Appends the given handlers to our handler list.

Host patterns are processed sequentially in the order they were
added. All matching patterns will be considered.
"""
if not host_pattern.endswith("$"):
host_pattern += "$"
handlers = []
# The handlers with the wildcard host_pattern are a special
# case - they're added in the constructor but should have lower
# precedence than the more-precise handlers added later.
# If a wildcard handler group exists, it should always be last
# in the list, so insert new groups just before it.
if self.handlers and self.handlers[-1][0].pattern == '.*$':
self.handlers.insert(-1, (re.compile(host_pattern), handlers))
else:
self.handlers.append((re.compile(host_pattern), handlers))

for spec in host_handlers:
if isinstance(spec, (tuple, list)):
# 需要满足 `URLSpec` 构造函数参数列表的要求:(pattern, handler, kwargs=None, name=None)
assert len(spec) in (2, 3, 4)
spec = URLSpec(*spec)
handlers.append(spec)
if spec.name:
if spec.name in self.named_handlers:
app_log.warning(
"Multiple handlers named %s; replacing previous value",
spec.name)
self.named_handlers[spec.name] = spec

对上述代码可以做一些分析:

  1. 参数 host_pattern 指定匹配主机地址的正则表达式字符串, host_handlers 是一个 list,其中的元素可以是 URLSpec 实例或者用于构造 URLSpec 实例的参数列表,若是后者需要动态创建 URLSpec 实例。

  2. self.handlers 字段保存着所有的 handler 列表,其中的每个元素是一个 tuple 实例,tuple 实例的第一个元素是匹配主机的正则表达式实例,第二个元素是 URLSpec 实例列表,其中通配所有主机的元素优先级最低,默认添加到 handler 列表的尾部。

  3. 对于命名的 handler 同时会被保存在 named_handlers 字段中,以备 reverse_url 方法使用。

Application 作为 httputil.HTTPServerConnectionDelegate 的子类,其 start_request 方法将返回一个 _RequestDispatcher 实例来处理请求。在 Tornado v4.0.2 中,start_request 的方法签名与 httputil.HTTPServerConnectionDelegate 定义的不一致,貌似没有重构完成一样,不过后面版本中已经做了修正。

1
2
3
def start_request(self, connection):
# Modern HTTPServer interface
return _RequestDispatcher(self, connection)

Application 本身还有一些诸如启动 HttpSever,配置 debug 参数,HTML 模板解析支持等功能,这里我们仅分析梳理其处理请求的流程,这些功能就不再一一介绍。

_RequestDispatcher

_RequestDispatcherhttputil.HTTPMessageDelegate 的子类,负责将请求按照主机地址和资源路径分发到具体的 handler 实例。在 Tornado v4.0 之前,这部分功能属于 Application。这样的职责分离是程序不断重构的结果。

_RequestDispatcher 的代码比较简单,结合前面的介绍很容易看懂,这里就直接将分析内容写在代码注释中,不再单独赘述。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class _RequestDispatcher(httputil.HTTPMessageDelegate):
def __init__(self, application, connection):
self.application = application
self.connection = connection
self.request = None
self.chunks = []
self.handler_class = None
self.handler_kwargs = None
self.path_args = []
self.path_kwargs = {}

def headers_received(self, start_line, headers):
self.set_request(httputil.HTTPServerRequest(
connection=self.connection, start_line=start_line, headers=headers))
# 当前请求处理器若支持 `streaming body` 模式,便直接进入请求处理器的处理阶段,否则便需要
# 等待 body 数据接收解析完成后才进入(由 finish 方法中进入)。
if self.stream_request_body:
self.request.body = Future()
# self.execute() 方法将返回 `handler._prepared_future` 字段,对于 `streaming body` 模式
# `handler._prepared_future=Future()`,一旦 futrue 完成就表明 `handler` 已准备好接收 body
# 数据。对于非 `streaming body` 模式,没有准备接收 body 数据这个前提,所以 `handler._prepared_future=None`
return self.execute()

def set_request(self, request):
self.request = request
self._find_handler()
# 检查目标请求处理器类是否支持 `streaming body`,实际上也就是看看类型是否被
# `stream_request_body` 装饰。`stream_request_body` 装饰器由 Tornado v4.0
# 开始引入,在之前版本中客户端上传的 body 默认会被解析到 request.arguments 和 request.files
# 中。这就预示着在处理文件上传时,文件被整个暂存在内存中,很显然非常不适合较大文件上传。
# `streaming body` 模式的引入,可以让使用者去直接处理 raw body 数据,一边接收一
# 边写到存储中,减少内存的开销。详细一点说明请参考 `stream_request_body` 装饰器说
# 明。
# 注:对于文件上传, raw body 是 multipart/form-data 包装的数据,不仅仅包括文件数据。对
# raw body 的数据解析,可以参考 httputil.parse_body_arguments/parse_multipart_form_data 方法。
self.stream_request_body = _has_stream_request_body(self.handler_class)

def _find_handler(self):
# Identify the handler to use as soon as we have the request.
# Save url path arguments for later.
#
# 根据请求的主机和资源路径来定位请求处理器类。
# 1. 查找对应主机的所有请求处理器。Tornado 支持虚拟主机,可以为不同的虚拟主机配置不同的请求处理器,
# 参见 Application.add_handlers。 不过,一般生产环境都不会直接把 Tornado 进程暴露在请求处
# 理最前端,这个功能可以由处理前端来支持,比如 nginx。
# 2. 没有为请求的虚拟主机配置处理器时由 `RedirectHandler` 重新定位到默认主机。
# 3. 接着根据资源路径来定位具体的请求处理器。如有需要便将 path parameters 解析出来放到 path_kwargs
# 或 path_args 字段中。放到哪一个字段取决于资源路径匹配的正则表达式分组是否命名。
# 4. 若上一步没有定位到请求处理器,则尝试交由默认的处理器进行处理,否则由 `ErrorHandler` 处理。
app = self.application
handlers = app._get_host_handlers(self.request)
if not handlers:
self.handler_class = RedirectHandler
self.handler_kwargs = dict(url="http://" + app.default_host + "/")
return
for spec in handlers:
match = spec.regex.match(self.request.path)
if match:
self.handler_class = spec.handler_class
self.handler_kwargs = spec.kwargs
if spec.regex.groups:
# Pass matched groups to the handler. Since
# match.groups() includes both named and
# unnamed groups, we want to use either groups
# or groupdict but not both.
if spec.regex.groupindex:
self.path_kwargs = dict(
(str(k), _unquote_or_none(v))
for (k, v) in match.groupdict().items())
else:
self.path_args = [_unquote_or_none(s)
for s in match.groups()]
return
if app.settings.get('default_handler_class'):
self.handler_class = app.settings['default_handler_class']
self.handler_kwargs = app.settings.get(
'default_handler_args', {})
else:
self.handler_class = ErrorHandler
self.handler_kwargs = dict(status_code=404)

def data_received(self, data):
if self.stream_request_body:
# streaming body 模式,由 handler 来自行处理 body stream data
return self.handler.data_received(data)
else:
self.chunks.append(data)

def finish(self):
if self.stream_request_body:
self.request.body.set_result(None)
else:
self.request.body = b''.join(self.chunks)
self.request._parse_body()
self.execute()

def on_connection_close(self):
if self.stream_request_body:
self.handler.on_connection_close()
else:
self.chunks = None

def execute(self):
# If template cache is disabled (usually in the debug mode),
# re-compile templates and reload static files on every
# request so you don't need to restart to see changes
if not self.application.settings.get("compiled_template_cache", True):
with RequestHandler._template_loader_lock:
for loader in RequestHandler._template_loaders.values():
loader.reset()
if not self.application.settings.get('static_hash_cache', True):
StaticFileHandler.reset()

self.handler = self.handler_class(self.application, self.request,
**self.handler_kwargs)
transforms = [t(self.request) for t in self.application.transforms]

if self.stream_request_body:
self.handler._prepared_future = Future()
# Note that if an exception escapes handler._execute it will be
# trapped in the Future it returns (which we are ignoring here).
# However, that shouldn't happen because _execute has a blanket
# except handler, and we cannot easily access the IOLoop here to
# call add_future.
#
# handler._execute 返回的是一个 Future 对象,但这里不关心 Future 的结果。
# 一方面是由于 handler._execute 内部包含在一个异常捕获块中,所以实际上不必担心
# 其会抛出异常;另一方面在这里不太方便访问 IOLoop 以调用 add_future 获取结果。
self.handler._execute(transforms, *self.path_args, **self.path_kwargs)
# If we are streaming the request body, then execute() is finished
# when the handler has prepared to receive the body. If not,
# it doesn't matter when execute() finishes (so we return None)
#
# streaming 模式时 handler._prepared_future 将在 handler 准备好接收 body 数据时完成。
return self.handler._prepared_future