WSGI 和 Werkzeug ———— Flask 系列文章(一)

Flask 系列文章前言

记得差不多两年前第一次听说 flask 的时候,是在知乎@萧井陌的答案中

只要能读懂 flask 的源码了,就能找到一份软件开发的工作。

网上关于 flask 源码的的教程很多,如:flask 源码解析,大部分都是从 flask 某个版本(主要是 0.1)的代码结构入手,横向分析各个部分的功能。本文尝试从 flask 的代码提交入手,从第一个提交到 0.1 版本,纵向比较各个提交之间的差异,期望在 Python 代码以外,从 Armin Ronacher 大神身上学习到更多关于软件工程和开源项目相关的知识。每个知识点将独立成文,长短不一,每篇博文以用能用示例将概念理解透彻为目标。

传送门:

  1. WSGI 和 Werkzeug ———— Flask 系列文章(一)

WSGI —— Web Server Gateway Interface

WSGI 是一个用来解耦 web 应用(框架)和 web 服务器的通用接口规范。符合该接口规范的 web 应用(框架)和 web 服务器之间可以自由组合,即某个 web 服务器可以驱动任意 web 应用(框架),某个 web 应用(框架)也可以由任意 web 服务器驱动。

廖雪峰的教程 WSGI 接口 简单易懂,很形象地说明了 WSGI 是什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf-8 -*-
""" The Application/Framework Side """
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return '<h1>Hello, web!</h1>'


""" The Server/Gateway Side """
# 从wsgiref模块导入:
from wsgiref.simple_server import make_server

# 创建一个服务器,IP地址为空,21024,处理函数是application:
httpd = make_server('', 21024, application)
print "Serving HTTP on port 21024..."
# 开始监听HTTP请求:
httpd.serve_forever()

直接在终端执行, 启动成功后,打开浏览器,输入http://localhost:21024/,就可以看到结果了。更多内容可以学习原教程。

WSGI 官方定义

官方 WSGI 的定义和详细的说明都在这里:

PEP 333 – Python Web Server Gateway Interface v1.0

PEP 3333 – Python Web Server Gateway Interface v1.0.1 这是升级版,改变不大。

不是带着目标看这种英文文档很容易犯困,所以我在快速浏览过 PEP 333 之后,又找中文版的好好看了一遍,下面这个算是翻译得还不错的:

PEP 333 中文版 译者的 github

看官方文档能够对细节了解得更多一点,期望以后看服务器实现和框架实现能理解更多背后的原因,挑我印象比较深的几个细节记录:

  1. WSGI 只是服务器与应用框架通信的规范,像 Cookies、Sessions 等特性往往实现在应用框架中,而 hop-by-hop 等对客户端跟服务器之间的持久连接产生影响的类似特性或头信息必须实现在 Web 服务器中。
  2. 中间件可以扮演两端角色,意味着在服务器看来中间件是应用程序,而在应用程序看来中间件是服务器,中间件对两端来说是透明的,例如 run_with_cgi(Latinator(foo_app)) 中的 Latinator 就是一个中间件。
  3. WSGI 规范中参数的传递都是位置参数,而不是关键字参数。
  4. start_response(status, response_headers, exc_info=None)只在抛出异常的情况下调用两次。正常调用处理好返回头缓存起来,返回 writer 准备接受 body 数据。异常调用则主要处理非空的 exc_info,如果之前缓存了返回头,则可能进行一些修改。
  5. environ变量用在 application(environ, start_response) 中,由服务器传入应用。服务器已经事先将请求解析成了 environ 规定的变量,并且按照规范尽量补全变量,再传入应用程序中。
  6. 大多数应用只用到environ里面的唯一一个配置值来指示应用自身的配置文件地址。

Werkzeug —— HTTP 和 WSGI 工具集

0.12版本文档

Werkzeug 是 Flask 的作者 Armin Ronacher 很早就开发的,据说 Flask 被开发出来的初衷也是展示如何用 Werkzeug 定制出一个 Web 框架,可见 Werkzeug 的实用性和在 Flask 中的核心位置。

Werkzeug 本身是一套 WSGI 工具集,包括强大的 debugger、全特性的 request 和 response 类,处理 Etag 头、cache control 头的 HTTP 工具函数、HTTP 日期、cookie 处理、文件上传,以及一个强大的路由系统,还有一系列社区贡献的插件。它支持 Unicode,对模板引擎、数据库接口、乃至请求的处理方式都十分灵活。

这套工具集在使用的时候大部分参数都要以关键字参数的方式传入,以避免重构的时候出错。这一点正好与 WSGI 的定义相反。

下面就几个主要的功能进行阅读。

lazy-loading 模块

__init__.py文件中实现了一个 lazy-loading 的模块替换了原有的sys.modules['werkzeug']

替换过程为

1
2
3
4
5
6
7
8
9
10
new_module = sys.modules['werkzeug'] = module('werkzeug')
new_module.__dict__.update({
'__file__': __file__,
'__package__': 'werkzeug',
'__path__': __path__,
'__doc__': __doc__,
'__version__': __version__,
'__all__': tuple(object_origins) + tuple(attribute_modules),
'__docformat__': 'restructuredtext en'
})

实现 lazy-loading 的类实现为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class module(ModuleType):

"""Automatically import objects from the modules."""

def __getattr__(self, name):
if name in object_origins:
module = __import__(object_origins[name], None, None, [name])
for extra_name in all_by_module[module.__name__]:
setattr(self, extra_name, getattr(module, extra_name))
return getattr(module, name)
elif name in attribute_modules:
__import__('werkzeug.' + name)
return ModuleType.__getattribute__(self, name)

def __dir__(self):
"""Just show what we want to show."""
result = list(new_module.__all__)
result.extend(('__file__', '__path__', '__doc__', '__all__',
'__docformat__', '__name__', '__path__',
'__package__', '__version__'))
return result

可以看到,这里 module 重写了 __getattr____dir__方法,可以看到 new_module.__dict__.updateresult.extend的内容基本上是一致的,__dir__仅多了一个__name__,并且疑似多写了一个__path__

以后实现 lazy-loading 模块可以参考这里的实现。

强大的路由和 URI and IRI utilities

routing.py中的类主要分为异常、Rule、Converter 和 Map 四种,我们主要关注后面三种。

Map

Map 相关的类包括 Map 和 MapAdapter 两个。Map 类中存储了所有的 URL 规则和一些影响所有规则的配置参数。 Map 类的实例通过 bind 方法和 bind_to_environ 方法返回 MapAdapter 实例,下面主要看看 bind 方法,bind_to_environ 与之类似,只不过 bind_to_environ 里很多参数都是从 environ 字典中提取出来的。

Map host_matching 特性是用 rules 里面的 host 参数替换 subdomain 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Map(object):
def bind(self, server_name, script_name=None, subdomain=None,
url_scheme='http', default_method='GET', path_info=None,
query_args=None):
server_name = server_name.lower()
if self.host_matching and subdomain is not None:
raise RuntimeError('host matching enabled and a '
'subdomain was provided')
elif subdomain is None:
subdomain = self.default_subdomain
if script_name is None:
script_name = '/'
try:
server_name = _encode_idna(server_name)
except UnicodeError:
raise BadHost()
return MapAdapter(self, server_name, script_name, subdomain,
url_scheme, path_info, default_method, query_args)

bind 的这些参数在 environ 变量中都能找到对应的变量,比较 bind 和 bind_to_environ 很容易知道其间的对应关系。这个 bind 方法把一个一组通用的规则应用到了一个具体的网站配置中,得到了一个 MapAdapter 实例,即实际上用于把 URL 路由到 view_function 的类。

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
class MapAdapter(object):
def dispatch(self, view_func, path_info=None, method=None,
catch_http_exceptions=False):
try:
try:
endpoint, args = self.match(path_info, method)
except RequestRedirect as e:
return e
return view_func(endpoint, args)
except HTTPException as e:
if catch_http_exceptions:
return e
raise
def match(self, path_info=None, method=None, return_rule=False,
query_args=None):
self.map.update()
if path_info is None:
path_info = self.path_info
else:
path_info = to_unicode(path_info, self.map.charset)
if query_args is None:
query_args = self.query_args
method = (method or self.default_method).upper()

path = u'%s|%s' % (
self.map.host_matching and self.server_name or self.subdomain,
path_info and '/%s' % path_info.lstrip('/')
)

have_match_for = set()
for rule in self.map._rules:
try:
rv = rule.match(path, method)
except RequestSlash:
raise RequestRedirect(self.make_redirect_url(
url_quote(path_info, self.map.charset,
safe='/:|+') + '/', query_args))
except RequestAliasRedirect as e:
raise RequestRedirect(self.make_alias_redirect_url(
path, rule.endpoint, e.matched_values, method, query_args))
if rv is None:
continue
if rule.methods is not None and method not in rule.methods:
have_match_for.update(rule.methods)
continue

if self.map.redirect_defaults:
redirect_url = self.get_default_redirect(rule, method, rv,
query_args)
if redirect_url is not None:
raise RequestRedirect(redirect_url)

if rule.redirect_to is not None:
if isinstance(rule.redirect_to, string_types):
def _handle_match(match):
value = rv[match.group(1)]
return rule._converters[match.group(1)].to_url(value)
redirect_url = _simple_rule_re.sub(_handle_match,
rule.redirect_to)
else:
redirect_url = rule.redirect_to(self, **rv)
raise RequestRedirect(str(url_join('%s://%s%s%s' % (
self.url_scheme or 'http',
self.subdomain and self.subdomain + '.' or '',
self.server_name,
self.script_name
), redirect_url)))

if return_rule:
return rule, rv
else:
return rule.endpoint, rv

if have_match_for:
raise MethodNotAllowed(valid_methods=list(have_match_for))
raise NotFound()
1
2
3
4
5
6
1.  rules without any arguments come first for performance
reasons only as we expect them to match faster and some
common ones usually don't have any arguments (index pages etc.)
2. The more complex rules come first so the second argument is the
negative length of the number of weights.
3. lastly we order by the actual weights.

Rule

Converter

如何处理 HTTP header

占坑

request 和 response

占坑

debugger

占坑

unicode

占坑

占坑

Cookbook

对列表类型变量赋值和清空

  1. 链式赋值:status, response_headers = headers_sent[:] = headers_set
    这里有个坑,python 链式赋值跟 C 语言语法上和效果上都不一样,效果上可以理解成把最右边的值从左往右赋值。我看到的用 dis 分析的过程
  2. 删除:del transform_ok[:]
坚持原创技术分享,您的支持将鼓励我继续创作!