Flask源码剖析(2)

看完了Flask最初0.1版本的源码后,接下来来看看下一次的大版本:1.0版本的Flask的源码。

1.0版本相比更新了很多东西较为重要的如下:

  1. CLI更加灵活。
  2. 开发服务器默认情况下多线程处理开发过程中的并发请求。
  3. test_client新增用于发布JSON数据的json参数,Response对象新增get_json方法来在测试中将数据解码为JSON。
  4. 新增test_cli_runner,用于测试应用程序的CLI命令。
  5. 大量的文档重写。

阅读源码,首先阅读Flask类的__call__的内容,和0.1版本的一致,依旧是调用wsgi_app()函数:

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                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
            ctx.auto_pop(error)

对比0.1版本,不再使用with语句块来控制请求上下文的入栈和出栈,而是将代码放入了try语句中,确保在接收请求的时候,程序不会因为出错而停止运行。

将请求上下文推入栈后,下一步就是将请求进行分发,观察full_dispatch_request函数,比较与初版本的异同:

    def full_dispatch_request(self):
        self.try_trigger_before_first_request_functions() #进行发生真实请求前的处理
        try:
            request_started.send(self) # 发送信号 socket部分的处理
            rv = self.preprocess_request() # 进行请求的预处理
            if rv is None:
                rv = self.dispatch_request()
        except Exception as e:
            rv = self.handle_user_exception(e)
        return self.finalize_request(rv)

其中,try_trigger_before_first_request_functions()函数是查看在第一次请求前,是否有函数需要执行,如果有,则执行。也就是说,将在实例的第一个请求开始前也会有需要调用的函数,而try_trigger_before_first_request_functions函数就是将这些函数调用并执行,并在最后将got_first_request参数设置为True,代表可以接收请求。

    def try_trigger_before_first_request_functions(self):
        """在每次请求前调用,并确保在第一次请求前触发该app的 before_first_request_funcs 函数,并且 每个函数只触发一次 """
        if self._got_first_request:
            return
        with self._before_request_lock:
            if self._got_first_request:
                return
            for func in self.before_first_request_funcs:
                func()
            self._got_first_request = True

然后下一步 ,在try语句里面,使用了signal机制,Flask的Signals机制和操作系统的signals系统很类似,都是通过信号来通知已经注册的回调函数,让回调函数自动开始执行。Flask框架定义了自己的一套核心signals和对应的函数。可以定义自己的回调函数,然后注册到对应的Signal,这样就可以在收到该信号的时候自动执行我们定义的回调函数。

再下一步,调用preprocess_request()函数,这个函数在0.1版本中的作用是调用被before_requested修饰过的函数,在0.9版本中有了很大的改动,但作用基本上没有变化,前面的try_trigger_before_first_request_functions函数是找到执行一次的伪中间件执行,而这个函数是找到所有的伪中间件执行。

    def preprocess_request(self):
        """在分发请求前调用,调用 url_value_preprocessors函数,注册到应用程序和当前蓝图中,然后调用 before_request_funcs 注册到应用程序和蓝图中。 """
        bp = _request_ctx_stack.top.request.blueprint
        funcs = self.url_value_preprocessors.get(None, ())
        if bp is not None and bp in self.url_value_preprocessors:
            funcs = chain(funcs, self.url_value_preprocessors[bp])
        for func in funcs:
            func(request.endpoint, request.view_args)
        funcs = self.before_request_funcs.get(None, ())
        if bp is not None and bp in self.before_request_funcs:
            funcs = chain(funcs, self.before_request_funcs[bp])
        for func in funcs:
            rv = func()
            if rv is not None:
                return rv

对比preprocess_request函数和try_trigger_before_first_request_functions函数,可以看出两者的区别:preprocess_request函数是在每次请求到来前都会对请求进行处理,而try_trigger_before_first_request_functions函数是在一个app实例的第一个请求处理前执行,之后的请求到来的时候并不执行。

观察完这两个函数后,接下来到了一个很重要的函数:dispatch_request

    def dispatch_request(self):
        """执行请求分派.匹配URL并返回视图或错误处理处理程序的返回值。这不必是一个响应对象,以便通过 make_response将返回值转换为适当的响应对象。 """
        req = _request_ctx_stack.top.request # 将请求对象赋值给req
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        # if we provide automatic options for this URL and the
        # request came with the OPTIONS method, reply automatically
        if getattr(rule, 'provide_automatic_options', False) \
           and req.method == 'OPTIONS':
            return self.make_default_options_response()
        # otherwise dispatch to the handler for that endpoint
        return self.view_functions[rule.endpoint](**req.view_args)

在每个url规则注册以后,它都会对应一个view_function。而dispatch_request请求分派函数,就是将request中的url规则与view_function相对应,找到相应的视图函数。值得注意的是,view_functions是一个字典形式,它的key和value对应的关系是endpoint->view function,所以在该函数的最后一行中才会返回的是view_function[rule.endpoint]

回到full_dispatch_request继续往下走:

        return self.finalize_request(rv)

finalize_request()函数是将处理请求后的返回值进行处理,返回reponse对象的一个过程。

    def finalize_request(self, rv, from_error_handler=False):
        response = self.make_response(rv) # 构造响应对象
        try:
            response = self.process_response(response)#查看是否有经过after_request修饰过的函数
            request_finished.send(self, response=response)#发送请求完成的信号
        except Exception:
            if not from_error_handler:
                raise
            self.logger.exception('Request finalizing failed with an '
                                  'error while handling an error')
        return response

大体逻辑与0.1版本的一致。但是在前面有说过,Flask0.9版本中多添加了一个应用上下文,那么这个应用上下文到底是用来干嘛的呢?

请求上下文和应用上下文

什么是上下文

上下文:相当于一个容器,保存了Flask程序运行过程中的一些信息.在计算机中,相对于进程而言,上下文就是进程执行时的环境。

Flask中有两种上下文:请求上下文和应用上下文

请求上下文:request_context

request和session都是请求上下文对象

request封装了HTTP请求的内容,针对的HTTP请求,request对象只有在上下文的生命周期内才有效,离开了请求的生命周期,其上下文环境就不存在了,也就无法获取request对象了.

session是用来记录请求回话中的信息,针对的用户信息。

应用上下文:app_context

current_app:当前运行程序的实例的引用,可以通过current_app.name打印当前应用程序实例的名字。

g(global):处理请求时用作临时存储的对象,专门用来保存用户数据,每次请求都会重置。

注意

  1. 当调用app=Flask(name)的时候,创建程序应用实例
  2. request在每次http请求发生时,WIGI服务器调用Flask.__call__();
  3. app的生命周期大于request和global,所以在一个app对象存活期间,可能会发生多次HTTP请求,所以会有多个request,global。

从一个Flask APP实例化并读入配置启动开始,就进入了App Context,在其中我们可以配置文件,打开资源文件,通过路由规则反向URL。

用较为简单的理解方式来解释这两种上下文对象:

应用上下文是指一个应用运行过程中的所有数据,请求上下文指的是一次请求中的所有数据

应用上下文的目的

在Flask的官方文档中对此的解释是这样的:

应用上下文存在的主要原因是,在过去,没有更好的方式来在请求上下文中附加一堆函数,因为Flask设立的支柱之一是你可以在一个Python进程中拥有多个应用。(为什么要有多应用?如果将各类服务都分为了不同应用的话,在要更新某个服务的时候只要更新服务对应的应用,而不用整个应用重启)

那么代码如何找到“正确的”应用?在过去,我们推荐显式地到处传递应用,但是这导致没有用这种想法设计地库,因为让库实现这种想法不太方便。

解决上述问题的常用方法是使用后面将会提到的current_app代理,它被限制在当前请求的应用引用。既然无论如何在没有请求时创建一个这样的请求上下文是一个没有必要的昂贵操作,那么就引入了应用上下文。

简单点说,设立应用上下文的作用就是是让数据能够保存在整个应用的生存周期中

为什么要使用栈结构(二)

我们知道对一个Flask APP调用app.run()之后,进程就会进入阻塞模式并开始监听请求。此时是不可能再让另一个Flask APP再主线程运行起来的。那么还有哪些场景是需要多个Flask APP共存的呢?前面说到的多服务多应用是一个应用场景。

如果仅仅在 Web Runtime 中,多个 Flask App 同时工作倒不是问题。毕竟每个请求被处理的时候是身处不同的 Thread Local 中的。但是 Flask App 不一定仅仅在 Web Runtime 中被使用 —— 有两个典型的场景是在非 Web 环境需要访问上下文代码的,一个是离线脚本,另一个是测试。这两个场景即所谓的“Running code outside of a request”(在请求之外运行代码)。

在非Web环境下运行Flask关联的代码

离线脚本或者测试这类非Web环境和Web环境不同——前者一般只在主线程运行。

设想,一个离线脚本需要操作两个Flask App关联的上下文,应该怎么办呢?这时候栈结构的APP Context优势就发挥出来了。

无论有多少个APP,只要主动去Push它的App Context,Context Stack中就会累积起来。这样,栈顶永远是当前操作的App Context。当一个App Context结束的时候,相应的栈顶元素也会随之出栈。如果在执行过程中抛出了异常,那么对应的App Context中注册的teardown函数会被传入带有异常信息的参数。

所以,在这种单线程运行环境中,只有栈结构才能保存多个Context并在其中定位哪个才是当前正在操作的app。而离线脚本只需要APP关联的上下文,不需要构造出请求,所以APP Context也应该和Request Context分离。

另一个手动推入Context的场景是测试,在测试中,我们可能会需要构造一个请求,并验证相关的状态是否符合预期。例如:

def test_app():
    app = create_app()
    client = app.test_client()
    resp = client.gei('/')
    assert 'home' in resp.data

这里调用 client.get 时,Request Context 就被推入了。其特点和 App Context 非常类似,这里不再赘述。