相信很多初学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会清理当前线程/协程产生的数据(请求上下文对象)。

https://segmentfault.com/a/1190000009152550

一个Flask应用运行过程剖析的更多相关文章

  1. python的运行过程剖析·编程语言分类

    总结: 编程语言的分类 编译型: 说明:与汇编语言类似,都有一个编译程序将源代码编译成硬件可执行的二进制代码 特点:执行速度快.同等情况下对系统要求低,适合于开发大型应用程序.数据库系统.操作系统等 ...

  2. linux下模拟一个木马程序运行过程

    预备知识: 将一个程序放入到后台,悄悄的执行 ./xxx.sh & 进程: 用户进程:由用户来管理 系统进程:由系统内核自行管理 系统中的每个进程,都有一个位置的ID,这就是pid,而且每次启 ...

  3. 第一部分 记事本搞定第一个C#程序和编译过程剖析

    记事本搞定第一个C#程序 进行下面三个步骤:编码,编译和托管运行. 1.记事本进行编码: using System; class Program{ public static void Main() ...

  4. 老李推荐:第8章7节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-小结

    老李推荐:第8章7节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-小结   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性 ...

  5. 老李推荐:第8章5节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-运行测试脚本

    老李推荐:第8章5节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-运行测试脚本   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化 ...

  6. 老李推荐:第8章1节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-运行环境初始化

    老李推荐:第8章1节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-运行环境初始化   首先大家应该清楚的一点是,MonkeyRunner的运行是牵涉到主机端和目 ...

  7. Java Executor并发框架(二)剖析ThreadPoolExecutor运行过程

    上一篇从整体上介绍了Executor接口,从上一篇我们知道了Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThrea ...

  8. 一个Flask运行分析

    当我们安装好Flask环境之后,创建好项目,就会自动生成下面这段代码: from flask import Flask app = Flask(__name__) @app.route('/') de ...

  9. Loadrunner11录制过程中报错:由于另一个程序正在运行中,此操作无法完成

    第一次安装LR11录制没有问题,第二次录制也没有问题,后面第二天录制就出现了如下截图的现象  报错内容:由于另一个程序正在运行中,此操作无法完成.请选择“切换到”来激活正在运行中的的程序,并更正问题 ...

随机推荐

  1. Go语言使用Godep进行包管理

    一.为什么要包管理 默认Go的第三方包都是放在Gopath的src目录下,而且这些包都没有版本号的概念,这样的可能会出现一些问题. 举个例子:当A同事将开发完程序之后,此时可能引用了一个第三方的包,过 ...

  2. idea的jdk版本问题

    问题描述: 解决: 检查以上是否版本一致. 不行就再检查下面的: 如果上面的方法还是不行的话,就建议你改一下pom文件中的maven插件编译级别,可以通过搜索所有的pom.xml,1.6这个关键词,就 ...

  3. c#压缩和解压缩

    C# 文件/文件夹压缩解压缩   项目上用到的,随手做个记录,哈哈. 直接上代码: 1 using System; 2 using System.Data; 3 using System.Config ...

  4. Codeforces Round #532 (Div. 2) E. Andrew and Taxi(二分+拓扑排序)

    题目链接:https://codeforces.com/contest/1100/problem/E 题意:给出 n 个点 m 条边的有向图,要翻转一些边,使得有向图中不存在环,问翻转的边中最大权值最 ...

  5. 洛谷 P2038 无线网络发射器选址 题解

    每日一题 day9 打卡 Analysis 这道题是个模拟,两个0~128( 注意不是1~128 )的循环枚举正方形中心点,判断正方形的边界,再用循环枚举公共场所的数量就好了. 时间复杂度 < ...

  6. 代码 | 自适应大邻域搜索系列之(7) - 局部搜索LocalSearch的代码解析

    前言 好了小伙伴们我们又见面了,咳咳没错还是我.不知道你萌接连被这么多篇代码文章刷屏是什么感受,不过,酸爽归酸爽.今天咱们依然讲代码哈~不过今天讲的依然很简单,关于局部搜索LocalSearch的代码 ...

  7. Turn Off Windows Firewall Using PowerShell and CMD

    If you want to turn off the Windows Firewall, there are three methods. One is using the GUI which is ...

  8. P1069 细胞分裂——数学题,质因数分解

    P1069 细胞分裂 我们求的就是(x^k)|(m1^m2) k的最小值: 先给m1分解质因数,再给每个细胞分解: 如果m1有的质因数,细胞没有就跳过: 否则就记录答案: 注意整数除法下取整的原则: ...

  9. luogu P4168 蒲公英+ 分块学习笔记

    传送门 题目描述 在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关. 为了简化起见,我们把所有的蒲公英看成一个长度为n的序列\((a_1,a_2..a_n)\),其中 \(a_i\)为一 ...

  10. java继承内存分配

    java继承内存分配 继承的基本概念: * Java不支持多继承,也就是说子类至多只能有一个父类. * 子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法. * 子类中定义的成员 ...