一个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之前,有一些准备知识需要先了解一下:
使用Flask框架开发的属于Web应用。由于Python使用
WSGI
网关,所以这个应用也可以叫WSGI
应用;服务器、Web应用的设计应该遵循网关接口的一些规范。对于
WSGI
网关,要求Web应用实现一个函数或者一个可调用对象webapp(environ, start_response)
。服务器或网关中要定义start_response
函数并且调用Web应用。关于这部分的内容可以参考:wsgiref包——符合WSGI标准的Web服务实现(一)。Flask依赖于底层库
werkzeug
。相关内容可以参考:Werkzeug库简介。
本文暂时不对服务器或网关的具体内容进行介绍,只需对服务器、网关、Web应用之间有怎样的关系,以及它们之间如何调用有一个了解即可。
一个Flask应用运行的过程
1. 实例化一个Flask应用
使用app = Flask(__name__)
,可以实例化一个Flask应用。实例化的Flask应用有一些要点或特性需要注意一下:
对于请求和响应的处理,Flask使用
werkzeug
库中的Request
类和Response
类。对于这两个类的相关内容可以参考:Werkzeug库——wrappers模块。对于URL模式的处理,Flask应用使用
werkzeug
库中的Map
类和Rule
类,每一个URL模式对应一个Rule
实例,这些Rule
实例最终会作为参数传递给Map
类构造包含所有URL模式的一个“地图”。这个地图可以用来匹配请求中的URL信息,关于Map
类和Rule
类的相关知识可以参考:Werkzeug库——routing模块。当实例化一个Flask应用
app
(这个应用的名字可以随便定义)之后,对于如何添加URL模式,Flask采取了一种更加优雅的模式,对于这点可以和Django的做法进行比较。Flask采取装饰器的方法,将URL规则和视图函数结合在一起写,其中主要的函数是route
。在上面例子中:@app.route('/')
def index():
pass这样写视图函数,会将
'/'
这条URL规则和视图函数index()
联系起来,并且会形成一个Rule
实例,再添加进Map
实例中去。当访问'/'
时,会执行index()
。关于Flask匹配URL的内容,可以参考后续文章。实例化Flask应用时,会创造一个
Jinja
环境,这是Flask自带的一种模板引擎。可以查看Jinja文档,这里先暂时不做相关介绍。实例化的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应用的相关特性。对于这一点后续会详细分析。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
中间件SharedDataMiddleware
是werkzeug.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
,将app1
、app2
和app
合并起来。这样,此时的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
始终指向当前线程/协程中存储的“请求上下文”,这样像app
、request
、session
、g
等都可以以“全局”的形式存在。这里“全局”是指在当前线程或协程当中。
由此可以看出,当处理请求时:
首先,会生成一个请求上下文对象,这个上下文对象包含请求相关的信息。并且在进入上下文环境时,
LocalStack
会将这个上下文对象推入栈结构中以存储这个对象;在这个上下文环境中可以进行请求处理过程,这个稍后再介绍。不过可以以一种“全局”的方式访问上下文对象中的变量,例如
app
、request
、session
、g
等;当请求结束,退出上下文环境时,
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)
从代码可以看出,在上下文对象中处理请求的过程分为以下几个步骤:
在请求正式被处理之前的一些操作,调用
preprocess_request()
方法,例如打开一个数据库连接等操作;正式处理请求。这个过程调用
dispatch_request()
方法,这个方法会根据URL匹配的情况调用相关的视图函数;将从视图函数返回的值转变为一个
Response
对象;在响应被发送到
WSGI
服务器之前,调用process_response(response)
做一些后续处理过程;调用
response(environ, start_response)
方法将响应发送回WSGI
服务器。关于此方法的使用,可以参考:Werkzeug库——wrappers模块;退出上下文环境时,
LocalStack
会清理当前线程/协程产生的数据(请求上下文对象)。
https://segmentfault.com/a/1190000009152550
一个Flask应用运行过程剖析的更多相关文章
- python的运行过程剖析·编程语言分类
总结: 编程语言的分类 编译型: 说明:与汇编语言类似,都有一个编译程序将源代码编译成硬件可执行的二进制代码 特点:执行速度快.同等情况下对系统要求低,适合于开发大型应用程序.数据库系统.操作系统等 ...
- linux下模拟一个木马程序运行过程
预备知识: 将一个程序放入到后台,悄悄的执行 ./xxx.sh & 进程: 用户进程:由用户来管理 系统进程:由系统内核自行管理 系统中的每个进程,都有一个位置的ID,这就是pid,而且每次启 ...
- 第一部分 记事本搞定第一个C#程序和编译过程剖析
记事本搞定第一个C#程序 进行下面三个步骤:编码,编译和托管运行. 1.记事本进行编码: using System; class Program{ public static void Main() ...
- 老李推荐:第8章7节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-小结
老李推荐:第8章7节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-小结 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性 ...
- 老李推荐:第8章5节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-运行测试脚本
老李推荐:第8章5节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-运行测试脚本 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化 ...
- 老李推荐:第8章1节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-运行环境初始化
老李推荐:第8章1节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-运行环境初始化 首先大家应该清楚的一点是,MonkeyRunner的运行是牵涉到主机端和目 ...
- Java Executor并发框架(二)剖析ThreadPoolExecutor运行过程
上一篇从整体上介绍了Executor接口,从上一篇我们知道了Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThrea ...
- 一个Flask运行分析
当我们安装好Flask环境之后,创建好项目,就会自动生成下面这段代码: from flask import Flask app = Flask(__name__) @app.route('/') de ...
- Loadrunner11录制过程中报错:由于另一个程序正在运行中,此操作无法完成
第一次安装LR11录制没有问题,第二次录制也没有问题,后面第二天录制就出现了如下截图的现象 报错内容:由于另一个程序正在运行中,此操作无法完成.请选择“切换到”来激活正在运行中的的程序,并更正问题 ...
随机推荐
- Go语言使用Godep进行包管理
一.为什么要包管理 默认Go的第三方包都是放在Gopath的src目录下,而且这些包都没有版本号的概念,这样的可能会出现一些问题. 举个例子:当A同事将开发完程序之后,此时可能引用了一个第三方的包,过 ...
- idea的jdk版本问题
问题描述: 解决: 检查以上是否版本一致. 不行就再检查下面的: 如果上面的方法还是不行的话,就建议你改一下pom文件中的maven插件编译级别,可以通过搜索所有的pom.xml,1.6这个关键词,就 ...
- c#压缩和解压缩
C# 文件/文件夹压缩解压缩 项目上用到的,随手做个记录,哈哈. 直接上代码: 1 using System; 2 using System.Data; 3 using System.Config ...
- Codeforces Round #532 (Div. 2) E. Andrew and Taxi(二分+拓扑排序)
题目链接:https://codeforces.com/contest/1100/problem/E 题意:给出 n 个点 m 条边的有向图,要翻转一些边,使得有向图中不存在环,问翻转的边中最大权值最 ...
- 洛谷 P2038 无线网络发射器选址 题解
每日一题 day9 打卡 Analysis 这道题是个模拟,两个0~128( 注意不是1~128 )的循环枚举正方形中心点,判断正方形的边界,再用循环枚举公共场所的数量就好了. 时间复杂度 < ...
- 代码 | 自适应大邻域搜索系列之(7) - 局部搜索LocalSearch的代码解析
前言 好了小伙伴们我们又见面了,咳咳没错还是我.不知道你萌接连被这么多篇代码文章刷屏是什么感受,不过,酸爽归酸爽.今天咱们依然讲代码哈~不过今天讲的依然很简单,关于局部搜索LocalSearch的代码 ...
- 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 ...
- P1069 细胞分裂——数学题,质因数分解
P1069 细胞分裂 我们求的就是(x^k)|(m1^m2) k的最小值: 先给m1分解质因数,再给每个细胞分解: 如果m1有的质因数,细胞没有就跳过: 否则就记录答案: 注意整数除法下取整的原则: ...
- luogu P4168 蒲公英+ 分块学习笔记
传送门 题目描述 在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关. 为了简化起见,我们把所有的蒲公英看成一个长度为n的序列\((a_1,a_2..a_n)\),其中 \(a_i\)为一 ...
- java继承内存分配
java继承内存分配 继承的基本概念: * Java不支持多继承,也就是说子类至多只能有一个父类. * 子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法. * 子类中定义的成员 ...