浅谈flask源码之请求过程
更新时间:2018年07月26日 09:51:36 作者:Dear、 我要评论
Flask
Flask是什么?
Flask是一个使用 Python 编写的轻量级 Web 应用框架, 让我们可以使用Python语言快速搭建Web服务, Flask也被称为 "microframework" ,因为它使用简单的核心, 用 extension 增加其他功能
为什么选择Flask?
我们先来看看python现在比较流行的web框架
- Flask
- Django
- Tornado
- Sanic
Flask: 轻, 组件间松耦合, 自由、灵活,可扩展性强,第三方库的选择面广的同时也增加了组件间兼容问题
Django: Django相当于一个全家桶, 几乎包括了所有web开发用到的模块(session管理、CSRF防伪造请求、Form表单处理、ORM数据库对象化、模板语言), 但是相对应的会造成一个紧耦合的情况, 对第三方插件不太友好
Tornado: 底层通过eventloop来实现异步处理请求, 处理效率高, 学习难度大, 处理稍有不慎很容易阻塞主进程导致不能正常提供服务, 新版本也支持asyncio
Sanic: 一个类Flask框架, 但是底层使用uvloop进行异步处理, 可以使用同步的方式编写异步代码, 而且运行效率十分高效.
WSGI
先来看看维基百科对WSGI的定义
Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口.
何为网关, 即从客户端发出的每个请求(数据包)第一个到达的地方, 然后再根据路由进行转发处理. 而对于服务端发送过来的消息, 总是先通过网关层, 然后再转发至客户端
那么可想而知, WSGI其实是作为一个网关接口, 来接受Server传递过来的信息, 然后通过这个接口调用后台app里的view function进行响应.
先看一段有趣的对话:
Nginx:Hey, WSGI, 我刚收到了一个请求,我需要你作些准备, 然后由Flask来处理这个请求.
WSGI:OK, Nginx. 我会设置好环境变量, 然后将这个请求传递给Flask处理.
Flask:Thanks. WSGI给我一些时间,我将会把请求的响应返回给你.
WSGI:Alright, 那我等你.
Flask:Okay, 我完成了, 这里是请求的响应结果, 请求把结果传递给Nginx.
WSGI:Good job! Nginx, 这里是响应结果, 已经按照要求给你传递回来了.
Nginx:Cool, 我收到了, 我把响应结果返回给客户端.大家合作愉快~
对话里面可以清晰了解到WSGI、nginx、Flask三者的关系
下面来看看Flask中的wsgi接口(注意:每个进入Flask的请求都会调用Flask.__call__)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# 摘自Flask源码 app.py class Flask(_PackageBoundObject): # 中间省略 def __call__( self , environ, start_response): return self .wsgi_app(environ, start_response) def wsgi_app( self , environ, start_response): # environ: 一个包含全部HTTP请求信息的字典, 由WSGI Server解包HTTP请求生成 # start_response: WSGI Server提供的函数, 调用可以发送响应的状态码和HTTP报文头, # 函数在返回前必须调用一次. :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. # 创建上下文 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) |
wsgi_app中定义的就是Flask处理一个请求的基本流程,
1.创建上下文
2.把上下文入栈
3.分发请求
4.上下文出栈
5.返回结果
其中response = self.full_dispatch_request()请求分发的过程我们需要关注一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
# 摘自Flask源码 app.py 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) return self .finalize_request(rv) def dispatch_request( self ): req = _request_ctx_stack.top.request if req.routing_exception is not None : self .raise_routing_exception(req) rule = req.url_rule if getattr (rule, 'provide_automatic_options' , False ) \ and req.method = = 'OPTIONS' : return self .make_default_options_response() return self .view_functions[rule.endpoint]( * * req.view_args) def finalize_request( self , rv, from_error_handler = False ): response = self .make_response(rv) try : response = self .process_response(response) 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 |
我们可以看到, 请求分发的操作其实是由dispatch_request来完成的, 而在请求进行分发的前后我们可以看到Flask进行了如下操作:
1.try_trigger_before_first_request_functions, 首次处理请求前的操作,通过@before_first_request定义,可以进行数据库连接
2.preprocess_request, 每次处理请求前进行的操作, 通过@before_request来定义, 可以拦截请求
3.process_response, 每次正常处理请求后进行的操作, 通过@after_request来定义, 可以统计接口访问成功的数量
4.finalize_request, 把视图函数的返回值转换成一个真正的响应对象
以上的这些是Flask提供给我们使用的钩子(hook), 可以根据自身需求来定义,
而hook中还有@teardown_request, 是在每次处理请求后执行(无论是否有异常), 所以它是在上下文出栈的时候被调用
如果同时定义了四种钩子(hook), 那么执行顺序应该是
graph LR
before_first_request --> before_request
before_request --> after_request
after_request --> teardown_request
在请求函数和钩子函数之间,一般通过全局变量g实现数据共享
现在的处理流程就变为:
1.创建上下文
2.上下文入栈
3.执行before_first_request操作(如果是第一次处理请求)
4.执行before_request操作
5.分发请求
6.执行after_request操作
7.执行teardown_request操作
8.上下文出栈
9.返回结果
其中3-7就是需要我们完成的部分.
如何使用Flask
上面我们知道, Flask处理请求的步骤, 那么我们来试试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
from flask import Flask app = Flask(__name__) @app .before_first_request def before_first_request(): print ( 'before_first_request run' ) @app .before_request def before_request(): print ( 'before_request run' ) @app .after_request def after_request(param): print ( 'after_request run' ) return param @app .teardown_request def teardown_request(param): print ( 'teardown_request run' ) @app .route( '/' ) def hello_world(): return 'Hello World!' if __name__ = = '__main__' : app.run() |
当运行flask进程时, 访问127.0.0.1:5000, 程序输出, 正好认证了我们之前说的执行顺序.
before_first_request run
before_request run
after_request run
teardown_request run
127.0.0.1 - - [03/May/2018 18:42:52] "GET / HTTP/1.1" 200 -
路由分发
看了上面的代码, 我们可能还是会有疑问, 为什么我们的请求就会跑到hello world 函数去处理呢?我们先来普及几个知识点:
- url: 客户端访问的网址
- view_func: 即我们写的视图函数
- rule: 定义的匹配路由的地址
- url_map: 存放着rule与endpoint的映射关系
- endpoint: 可以看作为每个view_func的ID
- view_functions: 一个字典, 以endpoint为key, view_func 为value
添加路由的方法:
1.@app.route
2.add_url_rule
我们先来看看@app.route干了什么事情
1
2
3
4
5
6
7
8
9
|
# 摘自Flask源码 app.py class Flask(_PackageBoundObject): # 中间省略 def route( self , rule, * * options): def decorator(f): endpoint = options.pop( 'endpoint' , None ) self .add_url_rule(rule, endpoint, f, * * options) return f return decorator |
我们可以看到, route函数是一个装饰器, 它在执行时会先获取endpoint, 然后再通过调用add_url_rule来添加路由, 也就是说所有添加路由的操作其实都是通过add_url_rule来完成的. 下面我们再来看看add_url_rule.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# 摘自Flask源码 app.py class Flask(_PackageBoundObject): # 中间省略 # 定义view_functions self .view_functions = {} # 定义url_map self .url_map = Map () def add_url_rule( self , rule, endpoint = None , view_func = None , provide_automatic_options = None , * * options): # 创建rule rule = self .url_rule_class(rule, methods = methods, * * options) rule.provide_automatic_options = provide_automatic_options # 把rule添加到url_map self .url_map.add(rule) if view_func is not None : old_func = self .view_functions.get(endpoint) if old_func is not None and old_func ! = view_func: raise AssertionError( 'View function mapping is overwriting an ' 'existing endpoint function: %s' % endpoint) # 把view_func 添加到view_functions字典 self .view_functions[endpoint] = view_func |
可以看到, 当我们添加路由时, 会生成一个rule, 并把它存放到url_map里头, 然后把view_func与其对应的endpoint存到字典.
当一个请求进入时, Flask会先根据用户访问的Url到url_map里边根据rule来获取到endpoint, 然后再利用view_functions获取endpoint在里边所对应的视图函数
graph LR
url1 -->url_map
url2 -->url_map
url3 -->url_map
urln -->url_map
url_map --> endpoint
endpoint --> view_functions
上下文管理
下面我们再来看看之前一直忽略的上下文,什么是上下文呢?
上下文即语境、语意,是一句话中的语境,也就是语言环境. 一句莫名其妙的话出现会让人不理解什么意思, 如果有语言环境的说明, 则会更好, 这就是语境对语意的影响. 而对应到程序里往往就是程序中需要共享的信息,保存着程序运行或交互中需要保持或传递的信息.
Flask中有两种上下文分别为:应用上下文(AppContext)和请求上下文(RequestContext).
按照上面提到的我们很容易就联想到:应用上下文就是保存着应用运行或交互中需要保持或传递的信息, 如当前应用的应用名, 当前应用注册了什么路由,
又有什么视图函数等. 而请求上下文就保存着处理请求过程中需要保持或传递的信息, 如这次请求的url是什么, 参数又是什么,
请求的method又是什么等.
我们只需要在需要用到这些信息的时候把它从上下文中取出来即可. 而上下文是有生命周期的, 不是所有时候都能获取到.
上下文生命周期:
- RequestContext: 生命周期在处理一次请求期间, 请求处理完成后生命周期也就结束了.
- AppContext: 生命周期最长, 只要当前应用还在运行, 就一直存在. (应用未运行前并不存在)
那么上下文是在什么时候创建的呢?我们又要如何创建上下文: 刚才我们提到, 在wsgi_app处理请求的时候就会先创建上下文, 那个上下文其实是请求上下文, 那应用上下文呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# 摘自Flask源码 ctx.py class RequestContext( object ): # 中间省略 def push( self ): top = _request_ctx_stack.top if top is not None and top.preserved: top.pop(top._preserved_exc) # 获取应用上下文 app_ctx = _app_ctx_stack.top # 判断应用上下文是否存在并与当前应用一致 if app_ctx is None or app_ctx.app ! = self .app: # 创建应用上下文并入栈 app_ctx = self .app.app_context() app_ctx.push() self ._implicit_app_ctx_stack.append(app_ctx) else : self ._implicit_app_ctx_stack.append( None ) if hasattr (sys, 'exc_clear' ): sys.exc_clear() # 把请求上下文入栈 _request_ctx_stack.push( self ) |
我们知道当有请求进入时, Flask会自动帮我们来创建请求上下文. 而通过上述代码我们可以看到,在创建请求上下文时会有一个判断操作,
如果应用上下文为空或与当前应用不匹配, 那么会重新创建一个应用上下文. 所以说一般情况下并不需要我们手动去创建, 当然如果需要,
你也可以显式调用app_context与request_context来创建应用上下文与请求上下文.
那么我们应该如何使用上下文呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from flask import Flask, request, g, current_app app = Flask(__name__) @app .before_request def before_request(): print 'before_request run' g.name = "Tom" @app .after_request def after_request(response): print 'after_request run' print (g.name) return response @app .route( '/' ) def index(): print (request.url) g.name = 'Cat' print (current_app.name) if __name__ = = '__main__' : app.run() |
访问127.0.0.1:5000时程序输出
before_request run
http://127.0.0.1:5000/
flask_run
after_request run
Cat
127.0.0.1 - - [04/May/2018 18:05:13] "GET / HTTP/1.1" 200 -
代码里边应用到的current_app和g都属于应用上下文对象, 而request就是请求上下文.
- current_app 表示当前运行程序文件的程序实例
- g: 处理请求时用作临时存储的对象. 每次请求都会重设这个变量 生命周期同RequestContext
- request 代表的是当前的请求
那么随之而来的问题是: 这些上下文的作用域是什么?
线程有个叫做ThreadLocal的类,也就是通常实现线程隔离的类. 而werkzeug自己实现了它的线程隔离类: werkzeug.local.Local. 而LocalStack就是用Local实现的.
这个我们可以通过globals.py可以看到
1
2
3
4
5
6
7
8
9
10
11
|
# 摘自Flask源码 globals.py from functools import partial from werkzeug.local import LocalStack, LocalProxy _request_ctx_stack = LocalStack() _app_ctx_stack = LocalStack() current_app = LocalProxy(_find_app) request = LocalProxy(partial(_lookup_req_object, 'request' )) session = LocalProxy(partial(_lookup_req_object, 'session' )) g = LocalProxy(partial(_lookup_app_object, 'g' )) |
_lookup_app_object思就是说, 对于不同的线程, 它们访问这两个对象看到的结果是不一样的、完全隔离的. Flask通过这样的方式来隔离每个请求.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
浅谈flask源码之请求过程的更多相关文章
- 浅谈C++源码的过国内杀软的免杀
以下只是简单的思路和定位.也许有人秒过,但是不要笑话我写的笨方法.定位永远是过期不了的. 其实这里废话一下 , 本人并不是大牛 ,今天跟大家分享下 .所以写出这篇文章.(大牛飘过) 只是个人实战的经验 ...
- flask源码剖析--请求流程
想了解这篇里面的内容,请先去了解我另外一篇博客Flask上下文 在了解flask之前,我们需要了解两个小知识点 偏函数 import functools def func(a1,a2): print( ...
- 【Flask源码分析——请求上下文与应用上下文】
Flask中有两种上下文,请求上下文和应用上下文.两者的作用域都处于一个请求的局部中. 查看源代码,上下文类在flask.ctx模块中定义 AppContext类定义应用上下文,app是当前应用Web ...
- 车大棒浅谈jQuery源码(二)
前言 本来只是一个自己学习jQuery笔记的简单分享,没想到获得这么多人赏识.我自己也是傻呵呵的一脸迷茫,感觉到受宠若惊. 不过还是有人向批判我的文章说,这是基本知识点,完全跟jQuery源码沾不上边 ...
- 车大棒浅谈jQuery源码(一)
背景 因为最近辞职找工作,投了许多家公司.结果简历要么石沉大海,一点音讯都没有,要么就是邮件回复说不匹配.后面加了一些QQ群,才发现原来我工作经验年限太少了.现在深圳都是3经验起步,北京据说更加恐怖. ...
- 一个QQ木马的逆向分析浅谈(附带源码)
程序流程:首先注册自己程序的窗口以及类等一系列窗口操作,安装了一个定时器,间隔为100ms,功能搜索QQ的类名,如果找到就利用FindWindow("5B3838F5-0C81-46D9-A ...
- flask 源码专题(二):请求上下文与全文上下文
源码解析 0. 请求入口 if __name__ == '__main__': app.run() def run(self, host=None, port=None, debug=None, lo ...
- 07 flask源码剖析之用户请求过来流程
07 Flask源码之:用户请求过来流程 目录 07 Flask源码之:用户请求过来流程 1.创建ctx = RequestContext对象 2. 创建app_ctx = AppContext对象 ...
- 用尽洪荒之力学习Flask源码
WSGIapp.run()werkzeug@app.route('/')ContextLocalLocalStackLocalProxyContext CreateStack pushStack po ...
随机推荐
- 转 史上最详细的Hadoop环境搭建
GitChat 作者:鸣宇淳 原文:史上最详细的Hadoop环境搭建 关注公众号:GitChat 技术杂谈,一本正经的讲技术 [不要错过文末活动哦] 前言 Hadoop在大数据技术体系中的地位至关重要 ...
- Golang(七)golang.org/x/time/rate 实现频率限制
1. 源码阅读 整个包实现原理基于令牌桶算法:随时间以 1/r 个令牌的速度向容积为 b 个令牌的桶中添加令牌,有请求就取走令牌,若令牌不足则不执行请求或者等待 Allow 方法的调用链:lim.Al ...
- Shell脚本之二 变量、字符串和数组
一.Shell 变量 1.1 定义变量 定义变量时,变量名不加美元符号($),如: your_name="runoob.com" 注意,变量名和等号之间不能有空格,这可能和你熟悉的 ...
- 在myecplice中关联svn
1:下载插件 site-1.8.22 2:找到myecplic的安装目录 下的dropins 文件夹(例如:C:\Users\han\AppData\Local\MyEclipse Professio ...
- Atlassian JIRA 插件开发之三 创建
之前的都是准备,真正的插件是从这里开始的 参考:https://developer.atlassian.com/server/framework/atlassian-sdk/modify-the-pl ...
- python入门之作用域
作用域的分类 1.全局作用域 全局可以调用的名字就存在于全局作用域 内置名称空间 + 全局名称空间 2.局部作用域 局部可以调用的名字就存放于局部作用域 局部名称空间 3. global 声明全局变量 ...
- Linux shell脚本单例模式实现
一.说明 关于单例模式,最开始的是一些小工具,运行起来后再点击运行时会提示已经运行了一个实例,觉得挺有意思但也没有很在意. 前段时间看了前领导的一段代码不太懂是做什么用的,同事查了下资料说是为了实现单 ...
- 谷歌浏览器扩展程序中安装vue-devtools插件
1.下载vue-devtools插件 地址https://github.com/vuejs/vue-devtools 2.进入刚刚下载文件的目录下(最好路径中没有中文) npm install 再执行 ...
- VUE后缀页面调试
在VUE中Js代码可以直接设置断点进行调试,但是vue文件中点击断点无反应,可以在想要断点的地方增加一行代码即可 debugger
- 59 网络编程(一)——端口与InetSocketAddress
端口与几个CMD命令 公认端口:0-1023 比如80端口分配给www,21端口分配给FTP等 注册端口:2014-49151 分配给用户进程或引用程序 动态/私有端口:49151-65535 需要 ...