Flask 源码初步解读

0x00 前言

系列文章先暂时停更一下。今天换换口味。

久闻 Flask 是众多 Pythonist 喜欢的框架。这次借着换工作的机会熟悉一下 Flask

  1. 本文先分享我阅读代码的一些小经验
  2. 接着通过最简单的一个 WSGI APP 开始,带着如何设计一个 Web 框架这个问题,先头脑风暴,从而脑补(而不是实现)出一个 Web 框架的基本要素。
  3. 从源码角度理解,Flask 从启动到接受第一个请求、返回第一个响应期间都发生了什么。
  4. 最后交代一些自己在这个过程中的一些突发的想法。

将解读 Flask 的源码放在一篇文章里,势必会造成广度有余而深度不足。所以本想定位于 Flask 源码初步解读。

0x01 阅读 Flask 代码的一种较好的姿势

之前在 https://www.zhihu.com/question/28509408/answer/299763091 分享过自己一点阅读代码的粗浅的经验,是以阅读一个 Django 的应用为案例的。这里借着读 Flask 本身分享一下我的看法。

读源码,是一个技术活。一是忌讳要想读懂全部,另一个忌讳是以为自己能一下子毫无障碍的读懂全部代码。

  1. 建议 0 : 看源码的时候,务必务必带着问题去读。每一次阅读其实都是在尝试回答或小或大的问题(当然,读书看文章莫不如是)。
  2. 建议 1 : 先读现成的文档,不要上来就对着代码一通瞎看。
  3. 建议 2 : 所谓『横看成岭侧成峰,远近高低都不同』 你需要从不同的角度来读源码。
  4. 建议 3 : 抓大放小,该略读就略读(比如知道 Nginx 的大致作用就好,做优化请求响应的时候再翻看文档),该精读则精读(具体一个关键的功能)。

好,坐好,预备,开车。

0x02 问题 1: 如何设计一个 Web 框架

头脑风暴

Flask 是一个微 Web 框架,换而言之,代码量少的 Web 框架。当然,其实 Flask 框架是一个微框架,但『常规的 Flask 应用』本身的代码加起来一点都不比『Django 应用』少。这个地方我们后面会讲到。

在阅读 Flask 相关代码的之前,先头脑风暴一下:

如何设计一个 Web 框架?

当心中对这个问题有一定的了解之后,读 Flask 代码会更好。

首先,Web 框架是为了提升 Web 开发的。(XX 框架是为了提升 XX 开发的), 这种提升可能会是 开发体验 / 性能。

我们来看看那个 Python 世界最基础的 wsgi app 相关代码。

1
2
3
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']

在之前的文章,我也借 Django 的 DRF 提到过这个极简的代码。

但这个简单的 webapp,显然是啥玩意都不够用的。比如说:

  • 没有路由,我访问啥玩意都是 hello world。
  • 单线程 IO 阻塞模型基本上啥都不能干。你比如说,启动这个 webapp 的时候在 return 数据之前直接 sleep 十秒,然后请求都进不来。
  • 没有数据存取,连个数据库链接 CURDE 啥玩意都没有
  • environ 太过于底层,如果是判断 headers 啥的太麻烦,要是像 django 里面一样能拿到一个 request 对象返回一个 response 对象就好了。
  • 没有模板语言
  • 还有其他能够提升开发体验的东西,比如自带 http server 代码热加载之类。
1
2
3
4
5
6
7
8
def application(environ, start_response):
# 直接 thread local 支持多个请求。
# 依据 environ 判断路由
# 依据 路由 执行相关 view 层方法
# 在相关 view 层方法内执行相关逻辑
start_response('200 OK', [('Content-Type', 'text/plain')])
# 返回对应响应
return response

当然,思路是这么个思路,这个思路也确实非常的命令式,非常的面向过程。

至于我们如何把这个面向过程的思路变成面向对象的设计与实现,则需要更加细致的思考这些问题。

  1. 能不能把 request 和 response 封装一下?方便在 view 里面处理?
  2. 能不能有个 URLDisparch 之类的东西,帮你解决 url 和 view 的 mapping 问题。或者路由能不能直接搞成 装饰器类型的比如 @router(“/“) 直接放在 view 层的 function 上。
  3. 能不能有个方便对数据库进行 CURDE 的东西?比如 ORM/ODM
  4. 这玩意会不会线程不安全,假如我想每一个请求都有单独的变量集合的话,线程怎么管理?
  5. ……

设计 Web 框架

利用 Flask 作者的另一个库 werkzeug 的案例中有这么一个东西。

https://github.com/pallets/werkzeug/blob/master/examples/shortly/shortly.py

几百行代码就不贴在这里了。仔细看看还是挺有趣的。 这个可以算作另一个超简的 Webapp 了。

Flask 算作在这个基础上进行一定的扩展而成。

看完上面这个就可以出去吹牛逼可以自己写一个极简 Web 框架了。

那么,你可能有疑问,为何有了 Flask 之后,是否需要看这个更底层的 Werkzeug 的库,当然,有必要咯,Python 世界除了老牌的比较流行的 Django/Flask, 还有一个新星,叫做 APIStar

https://github.com/encode/apistar

0x02 问题 2: 请求流程是怎么样的

我们就拿这个 flask 的极简案例,进行首次阅读 Flask 代码。

1
2
3
4
5
6
7
8
9
10
# hello.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
$ FLASK_APP=hello.py flask run

从请求到响应的整个流程,Flask 的是怎么处理请求的?

2.1 服务器是怎么起来的

首先 flask run 之后,发生了什么?

先初始化环境变量,然后导入 dotenv 文件,然后执行 run_command 方法,找到 hello.py 然后导入

1
2
3
4
5
6
7
#cli.py#run_command 方法
app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
# 上一行代表着其实我们每次在本地 flask run 的时候,起的服务并不是 flask_app, 而是被 DispatchingApp 包装了一层的 flask app
from werkzeug.serving import run_simple
run_simple(host, port, app, use_reloader=reload, use_debugger=debugger,
threaded=with_threads, ssl_context=cert)

进行这层包装之后,就可以显示 WERKZEUG 的所谓在浏览器中的 报错信息了。

通常开发时这里的 run_simple 最后会调用 run_with_reloader , 每当程序退出的时候,reloader 就依照策略重新跑一次 reload 一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def run_with_reloader(main_func, extra_files=None, interval=1,
reloader_type='auto'):
"""Run the given function in an independent python interpreter."""
import signal
reloader = reloader_loops[reloader_type](extra_files, interval)
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
t = threading.Thread(target=main_func, args=())
t.setDaemon(True)
t.start()
reloader.run()
else:
sys.exit(reloader.restart_with_reloader())
except KeyboardInterrupt:
pass

好,服务起来了。

2.2 请求-响应的流程

我们先看 Flask 类里面的比较关键的两个方法:

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
class Flask(_PackageBoundObject):
# 一些方法 ......
def full_dispatch_request(self):
# 主要是执行一些方法,最后返回响应
self.try_trigger_before_first_request_functions()
try:
request_started.send(self)
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
# ??? TODO
return self.finalize_request(rv)
# 这里是我们熟悉的 environ, 和 start_response
def wsgi_app(self, environ, start_response):
"""
:param environ: a WSGI environment
:param start_response: a callable accepting a status code,
a list of headers and an optional
exception context to start the response
"""
# 在这里对 environ 进行封装,创建请求上下文
ctx = self.request_context(environ)
error = None
try:
try:
# 这里将请求上下文压入 _request_ctx_stack
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
# 这里将创建的请求上下文从中 _request_ctx_stack pop 出来
ctx.auto_pop(error)

从 wsgi_app 泪看,就可以看到我们之前在当时在开脑洞时候看到的。

  1. 把 request 和 response 封装一下?方便在 view 里面处理?
  2. 有个 URLDisparch 之类的东西,帮你解决 url 和 view 的 mapping 问题。

话说回来?

这个 ctx 是啥?
当然,flask 不带 ORM, 这我们也就不研究了。

– TODO: 在这里需要重构一下

不过话说回来 请求上下文的容器 request_ctx_stack 到底是啥?

另一种本地数据存储方式。

在多线程的情况下,每一个请求都会创建一个线程,从这个请求被发起到销毁,我想拥有单独的变量(修改这个变量不会影响到其他变量),比如 sessions 之类。

显然,在多线程的情况下,以上的需求完全可以通过 threadlocal 来实现。

翻了 werkzeug 的文档,找到了原因:

因为 python 里面的并发模型并不只有多线程一种。比如 greenlets, 每一个请求,都在一个线程里面。

0x02 问题 2: Flask 中 Context 机制

在 Django 完成一个 View 层的逻辑是这样的,Django 封装好了请求,请求经过 middleware 的处理,最后调用 login 函数,并且传入 request 方便 view 函数进行处理。

1
2
3
4
def login(request):
if request.method == 'POST':
error = someerror
return render_template('login.html', error=error)

在 Flask 完成一个 View 层的逻辑是这样的

1
2
3
4
5
6
from flask import request
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
error = someerror
return render_template('login.html', error=error)

假如我是一个爱问问题的年轻人,这里肯定会有疑惑:

从外部 import 过来,那就是利用了 python 自带的 import 单例模式。 那么线程和线程之间拿到的肯定是同一个 request 呀。但 Django 里面每个 request 都是不一样的,否则一些很基础功能的比如已经认证的用户就无法拿到了。

我已经不是那个爱问问题的年轻人,因为年纪已经不小了。逃…

显然,每一次在 view 层引用的 request 肯定不是同一个 request , 那么,这是如何做到的呢?比如用 ThreadLocal , ThreadLocal 通过每个线程不同的 ID 拿到的本地变量,于是我们查看一下对应的实现。 这个 request 来自于 global.py , 使用了一个 werkzeug.local 里面的 LocalProxy

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
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
# 这就是我们需要的注意的地方,LocalProxy
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

看到这里一阵蛋疼,貌似没有 threadlocal ?再次查看相关实现最后还是定位到了如何区分不同的 request 的核心代码。

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
# since each thread has its own greenlet we can just use those as identifiers
# for the context. If greenlets are not available we fall back to the
# current thread ident depending on where it is.
try:
from greenlet import getcurrent as get_ident
# greenlet 的代码是 C, 时间长没看 C 代码了,看了半天没看明白
# 翻了文档返回当前的 greenlet, 也就是返回调用此函数的 greenlet
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
object.__setattr__(self, '__storage__', {})
# 这里传递的 ident 就可以直接
object.__setattr__(self, '__ident_func__', get_ident)
class LocalStack:
# 用 local 实现的栈
class LocalProxy:
# 一个 local 的代理器
def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)

即:

  1. 当 Flask 以多线程模型运行的时候,则使用的是 threadlocal 方式
  2. 当 Flask 以 greenlet 的模型运行的时候,则使用的是 greenlet 区分不同

接下来回头看一下处理 request 的逻辑

1
2
3
4
5
6
7
from flask import request
@app.route('/login', methods=['POST', 'GET'])
def login():
# 这个 request 哪里来?
if request.method == 'POST':
error = someerror
return render_template('login.html', error=error)

于是,我们就知道了,当引用 request 这个 LocalProxy 的时候,引用的确实是同一个名称为 request 变量,并且这个变量也确实是 LocalProxy 的实例

但是当使用 request.method 的时候,LocalProxy 重载了 取到的则是另一个『请求对象』的 method.

于是拿到当前请求的信息。

当然,其实我们也可以依据利用这个技巧写一个 currentuser 的 ProxyLocal, 然后在每个 view 层里面使用 user.has_something 进行操作。

0x03 问题 3: Flask 中官方的机制

0x04 问题 3: Flask 中是如何做到优雅扩展的

0x05 其他问题

Flask 应用


ChangeLog:

  • 2018-03-09 重修文字