# 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 相关代码。

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 代码热加载之类。
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. 这玩意会不会线程不安全,假如我想每一个请求都有单独的变量集合的话,线程怎么管理?

# 设计 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 代码。

# 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 然后导入

#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 一次。

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 类里面的比较关键的两个方法:

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 函数进行处理。

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

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

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

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 的核心代码。

# 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 的逻辑

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 重修文字