一个Flask应用运行过程剖析

相信很多初学Flask的同学(包括我自己),在阅读官方文档或者Flask的学习资料时,对于它的认识是从以下的一段代码开始的:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Hello World!"

if __name__ == '__main__':
    app.run()

运行如上代码,在浏览器中访问http://localhost:5000/,便可以看到Hello World!出现了。这是一个很简单的Flask的应用。

然而,这段代码怎么运行起来的呢?一个Flask应用运转的背后又有哪些逻辑呢?如果你只关心Web应用,那对这些问题不关注也可以,但从整个Web编程的角度来看,这些问题非常有意义。本文就主要针对一个Flask应用的运行过程进行简要分析,后续文章还会对Flask框架的一些具体问题进行分析。

为了分析方便,本文采用 Flask 0.1版本 的源码进行相关问题的探索。

一些准备知识

在正式分析Flask之前,有一些准备知识需要先了解一下:

  1. 使用Flask框架开发的属于Web应用。由于Python使用WSGI网关,所以这个应用也可以叫WSGI应用;

  2. 服务器、Web应用的设计应该遵循网关接口的一些规范。对于WSGI网关,要求Web应用实现一个函数或者一个可调用对象webapp(environ, start_response)。服务器或网关中要定义start_response函数并且调用Web应用。关于这部分的内容可以参考:wsgiref包——符合WSGI标准的Web服务实现(一)

  3. Flask依赖于底层库werkzeug。相关内容可以参考:Werkzeug库简介

本文暂时不对服务器或网关的具体内容进行介绍,只需对服务器、网关、Web应用之间有怎样的关系,以及它们之间如何调用有一个了解即可。

一个Flask应用运行的过程

1. 实例化一个Flask应用

使用app = Flask(__name__),可以实例化一个Flask应用。实例化的Flask应用有一些要点或特性需要注意一下:

  1. 对于请求和响应的处理,Flask使用werkzeug库中的Request类和Response类。对于这两个类的相关内容可以参考:Werkzeug库——wrappers模块

  2. 对于URL模式的处理,Flask应用使用werkzeug库中的Map类和Rule类,每一个URL模式对应一个Rule实例,这些Rule实例最终会作为参数传递给Map类构造包含所有URL模式的一个“地图”。这个地图可以用来匹配请求中的URL信息,关于Map类和Rule类的相关知识可以参考:Werkzeug库——routing模块

  3. 当实例化一个Flask应用app(这个应用的名字可以随便定义)之后,对于如何添加URL模式,Flask采取了一种更加优雅的模式,对于这点可以和Django的做法进行比较。Flask采取装饰器的方法,将URL规则和视图函数结合在一起写,其中主要的函数是route。在上面例子中:

    @app.route('/')
    def index():
        pass

    这样写视图函数,会将'/'这条URL规则和视图函数index()联系起来,并且会形成一个Rule实例,再添加进Map实例中去。当访问'/'时,会执行index()。关于Flask匹配URL的内容,可以参考后续文章。

  4. 实例化Flask应用时,会创造一个Jinja环境,这是Flask自带的一种模板引擎。可以查看Jinja文档,这里先暂时不做相关介绍。

  5. 实例化的Flask应用是一个可调用对象。在前面讲到,Web应用要遵循WSGI规范,就要实现一个函数或者一个可调用对象webapp(environ, start_response),以方便服务器或网关调用。Flask应用通过__call__(environ, start_response)方法可以让它被服务器或网关调用。

    def __call__(self, environ, start_response):
        """Shortcut for :attr:`wsgi_app`"""
        return self.wsgi_app(environ, start_response)

    注意到调用该方法会执行wsgi_app(environ, start_response)方法,之所以这样设计是为了在应用正式处理请求之前,可以加载一些“中间件”,以此改变Flask应用的相关特性。对于这一点后续会详细分析。

  6. Flask应用还有一些其他的属性或方法,用于整个请求和响应过程。

2.调用Flask应用时会发生什么

上面部分分析了实例化的Flask应用长什么样子。当一个完整的Flask应用实例化后,可以通过调用app.run()方法运行这个应用。

Flask应用的run()方法会调用werkzeug.serving模块中的run_simple方法。这个方法会创建一个本地的测试服务器,并且在这个服务器中运行Flask应用。关于服务器的创建这里不做说明,可以查看werkzeug.serving模块的有关文档。

当服务器开始调用Flask应用后,便会触发Flask应用的__call__(environ, start_response)方法。其中environ由服务器产生,start_response在服务器中定义。

上面我们分析到当Flask应用被调用时会执行wsgi_app(environ, start_response)方法。可以看出,wsgi_app是真正被调用的WSGI应用,之所以这样设计,就是为了在应用正式处理请求之前,wsgi_app可以被一些“中间件”装饰,以便先行处理一些操作。为了便于理解,这里先举两个例子进行说明。

例子一: 中间件SharedDataMiddleware

中间件SharedDataMiddlewarewerkzeug.wsgi模块中的一个类。该类可以为Web应用提供静态内容的支持。例如:

import os
from werkzeug.wsgi import SharedDataMiddleware

app = SharedDataMiddleware(app, {
    '/shared': os.path.join(os.path.dirname(__file__), 'shared')
})

Flask应用通过以上的代码,app便会成为一个SharedDataMiddleware实例,之后便可以在http://example.com/shared/中访问shared文件夹下的内容。

对于中间件SharedDataMiddleware,Flask应用在初始实例化的时候便有所应用。其中有这样一段代码:

self.wsgi_app = SharedDataMiddleware(self.wsgi_app, {
                self.static_path: target
            })

这段代码显然会将wsgi_app变成一个SharedDataMiddleware对象,这个对象为Flask应用提供一个静态文件夹/static。这样,当整个Flask应用被调用时,self.wsgi_app(environ, start_response)会执行。由于此时self.wsgi_app是一个SharedDataMiddleware对象,所以会先触发SharedDataMiddleware对象的__call__(environ, start_response)方法。如果此时的请示是要访问/static这个文件夹,SharedDataMiddleware对象会直接返回响应;如果不是,则才会调用Flask应用的wsgi_app(environ.start_response)方法继续处理请求。

例子二: 中间件DispatcherMiddleware

中间件DispatcherMiddleware也是werkzeug.wsgi模块中的一个类。这个类可以讲不同的应用“合并”起来。以下是一个使用中间件DispatcherMiddleware的例子。

from flask import Flask
from werkzeug import DispatcherMiddleware

app1 = Flask(__name__)
app2 = Flask(__name__)
app = Flask(__name__)

@app1.route('/')
def index():
    return "This is app1!"

@app2.route('/')
def index():
    return "This is app2!"

@app.route('/')
def index():
    return "This is app!"

app = DispatcherMiddleware(app, {
            '/app1':        app1,
            '/app2':        app2
        })

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 5000, app)

在上面的例子中,我们首先创建了三个不同的Flask应用,并为每个应用创建了一个视图函数。但是,我们使用了DispatcherMiddleware,将app1app2app合并起来。这样,此时的app便成为一个DispatcherMiddleware对象。

当在服务器中调用app时,由于它是一个DispatcherMiddleware对象,所以首先会触发它的__call__(environ, start_response)方法。然后根据请求URL中的信息来确定要调用哪个应用。例如:

  • 如果访问/,则会触发app(environ, start_response)注意: 此时app是一个Flask对象),进而处理要访问app的请求;

  • 如果访问/app1,则会触发app1(environ, start_response),进而处理要访问app1的请求。访问/app2同理。

3. 和请求处理相关的上下文对象

当Flask应用真正处理请求时,wsgi_app(environ, start_response)被调用。这个函数是按照下面的方式运行的:

def wsgi_app(environ, start_response):
    with self.request_context(environ):
        ...

请求上下文

可以看到,当Flask应用处理一个请求时,会构造一个上下文对象。所有的请求处理过程,都会在这个上下文对象中进行。这个上下文对象是_RequestContext类的实例。

# Flask v0.1
class _RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.
    """

    def __init__(self, app, environ):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None

    def __enter__(self):
        _request_ctx_stack.push(self)

    def __exit__(self, exc_type, exc_value, tb):
        # do not pop the request stack if we are in debug mode and an
        # exception happened.  This will allow the debugger to still
        # access the request object in the interactive shell.
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

根据_RequestContext上下文对象的定义,可以发现,在构造这个对象的时候添加了和Flask应用相关的一些属性:

  • app ——上下文对象的app属性是当前的Flask应用;

  • url_adapter ——上下文对象的url_adapter属性是通过Flask应用中的Map实例构造成一个MapAdapter实例,主要功能是将请求中的URL和Map实例中的URL规则进行匹配;

  • request ——上下文对象的request属性是通过Request类构造的实例,反映请求的信息;

  • session ——上下文对象的session属性存储请求的会话信息;

  • g ——上下文对象的g属性可以存储全局的一些变量。

  • flashes ——消息闪现的信息。

LocalStack和一些“全局变量”

注意: 当进入这个上下文对象时,会触发_request_ctx_stack.push(self)。在这里需要注意Flask中使用了werkzeug库中定义的一种数据结构LocalStack

_request_ctx_stack = LocalStack()

关于LocalStack,可以参考:Werkzeug库——local模块LocalStack是一种栈结构,每当处理一个请求时,请求上下文对象_RequestContext会被放入这个栈结构中。数据在栈中存储的形式表现成如下:

{880: {'stack': [<flask._RequestContext object>]}, 13232: {'stack': [<flask._RequestContext object>]}}

这是一个字典形式的结构,键代表当前线程/协程的标识数值,值代表当前线程/协程存储的变量。werkzeug.local模块构造的这种结构,很容易实现线程/协程的分离。也正是这种特性,使得可以在Flask中访问以下的“全局变量”:

current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)

其中_request_ctx_stack.top始终指向当前线程/协程中存储的“请求上下文”,这样像apprequestsessiong等都可以以“全局”的形式存在。这里“全局”是指在当前线程或协程当中。

由此可以看出,当处理请求时:

  • 首先,会生成一个请求上下文对象,这个上下文对象包含请求相关的信息。并且在进入上下文环境时,LocalStack会将这个上下文对象推入栈结构中以存储这个对象;

  • 在这个上下文环境中可以进行请求处理过程,这个稍后再介绍。不过可以以一种“全局”的方式访问上下文对象中的变量,例如apprequestsessiong等;

  • 当请求结束,退出上下文环境时,LocalStack会清理当前线程/协程产生的数据(请求上下文对象);

  • Flask 0.1版本只有“请求上下文”的概念,在Flask 0.9版本中又增加了“应用上下文”的概念。关于“应用上下文”,以后再加以分析。

4. 在上下文环境中处理请求

处理请求的过程定义在wsgi_app方法中,具体如下:

def wsgi_app(environ, start_response):
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

从代码可以看出,在上下文对象中处理请求的过程分为以下几个步骤:

  1. 在请求正式被处理之前的一些操作,调用preprocess_request()方法,例如打开一个数据库连接等操作;

  2. 正式处理请求。这个过程调用dispatch_request()方法,这个方法会根据URL匹配的情况调用相关的视图函数;

  3. 将从视图函数返回的值转变为一个Response对象;

  4. 在响应被发送到WSGI服务器之前,调用process_response(response)做一些后续处理过程;

  5. 调用response(environ, start_response)方法将响应发送回WSGI服务器。关于此方法的使用,可以参考:Werkzeug库——wrappers模块

  6. 退出上下文环境时,LocalStack会清理当前线程/协程产生的数据(请求上下文对象)。

58 声望
9 粉丝
0 条评论
推荐阅读
Flask中的请求上下文和应用上下文
在Flask中处理请求时,应用会生成一个“请求上下文”对象。整个请求的处理过程,都会在这个上下文对象中进行。这保证了请求的处理过程不被干扰。处理请求的具体代码如下:

fanchunke19916阅读 8.1k评论 1

Ubuntu20.04 从源代码编译安装 python3.10
Ubuntu 22.04 Release DateUbuntu 22.04 Jammy Jellyfish is scheduled for release on April 21, 2022If you’re ready to use Ubuntu 22.04 Jammy Jellyfish, you can either upgrade your current Ubuntu syste...

ponponon1阅读 3.9k

日常Python 代码片段整理
1、简单的 HTTP Web 服务器 {代码...} 2、单行循环List {代码...} 3、更新字典 {代码...} 4、拆分多行字符串 {代码...} 5、跟踪列表中元素的频率 {代码...} 6、不使用 Pandas 读取 CSV 文件 {代码...} 7、将列表...

墨城2阅读 288

Unicode 正则表达式(qbit)
前言本文根据《精通正则表达式》和 Unicode Regular Expressions 整理。本文的示例默认以 Python3 为实现语言,用到 Python3 的 re 模块或 regex 库。基本的 Unicode 属性分类 {代码...} 基本的 Unicode 子属性Le...

qbit阅读 4.3k

Python + Sqlalchemy 对数据库的批量插入或更新(Upsert)
由于不同数据库对这种 upsert 的实现机制不同,Sqlalchemy 也就不再试图做一致性的封装了,而是提供了各自的方言 API,具体到 Mysql,就是给 insert statement ,增加了 on_duplicate_key_update 方法。

songofhawk1阅读 1.9k评论 4

封面图
Go for 循环有时候真的很坑。。。
大家好,我是煎鱼。不知道有多少 Go 的面试题和泄露,都和 for 循环有关。今天我在周末认真一看,发现了 redefining for loop variable semantics 。著名的硬核大佬 Russ Cox 表示他一直在研究这个问题,并表示十...

煎鱼阅读 3.4k

打脸了兄弟们,Go1.20 arena 来了!
大家好,我是煎鱼。大概半年前,我写过一篇文章《Go 要违背初心吗?新提案:手动管理内存》。有兴趣了深入解的同学,可以再回顾一下。当时我们还想着 Go 团队应该不会接纳,至少不会那么快:懒得翻也可以看我再次...

煎鱼阅读 3.1k

58 声望
9 粉丝
宣传栏