浅谈flask源码之请求过程


Posted in Python onJuly 26, 2018

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__)

# 摘自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()请求分发的过程我们需要关注一下

# 摘自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处理请求的步骤, 那么我们来试试

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干了什么事情

# 摘自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.

# 摘自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处理请求的时候就会先创建上下文, 那个上下文其实是请求上下文, 那应用上下文呢?

# 摘自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来创建应用上下文与请求上下文.

那么我们应该如何使用上下文呢?

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可以看到

# 摘自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通过这样的方式来隔离每个请求.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python 'takes exactly 1 argument (2 given)' Python error
Dec 13 Python
Python 专题四 文件基础知识
Mar 20 Python
python基于twisted框架编写简单聊天室
Jan 02 Python
Python中list查询及所需时间计算操作示例
Jun 21 Python
纯用NumPy实现神经网络的示例代码
Oct 24 Python
python3.5安装python3-tk详解
Apr 26 Python
Django网络框架之HelloDjango项目创建教程
Jun 06 Python
pandas DataFrame的修改方法(值、列、索引)
Aug 02 Python
django 快速启动数据库客户端程序的方法示例
Aug 16 Python
Python中BeautifuSoup库的用法使用详解
Nov 15 Python
python连接mysql有哪些方法
Jun 24 Python
Python如何识别银行卡卡号?
Jun 10 Python
python画折线图的程序
Jul 26 #Python
TensorFlow利用saver保存和提取参数的实例
Jul 26 #Python
78行Python代码实现现微信撤回消息功能
Jul 26 #Python
python opencv实现图片旋转矩形分割
Jul 26 #Python
Flask之flask-session的具体使用
Jul 26 #Python
tensorflow saver 保存和恢复指定 tensor的实例讲解
Jul 26 #Python
python opencv旋转图像(保持图像不被裁减)
Jul 26 #Python
You might like
php UTF-8、Unicode和BOM问题
2010/05/18 PHP
Laravel的throttle中间件失效问题解决方法
2016/10/09 PHP
PDO::quote讲解
2019/01/29 PHP
jQuery ctrl+Enter shift+Enter实现代码
2010/02/07 Javascript
基于jquery的web页面日期格式化插件
2011/11/15 Javascript
jquery事件与函数的使用介绍
2013/09/29 Javascript
基于jQuery实现的文字按钮表单特效整理
2014/12/07 Javascript
javascript数据类型示例分享
2015/01/19 Javascript
纯js实现无限空间大小的本地存储
2015/06/18 Javascript
Javascript技术栈中的四种依赖注入小结
2016/02/27 Javascript
js获取对象、数组的实际长度,元素实际个数的实现代码
2016/06/08 Javascript
JS实现的五级联动菜单效果完整实例
2017/02/23 Javascript
javascript回调函数的概念理解与用法分析
2017/05/27 Javascript
jquery select插件异步实时搜索实例代码
2017/10/20 jQuery
简单实现jquery隔行变色
2017/11/09 jQuery
element-ui表格列金额显示两位小数的方法
2018/08/24 Javascript
原生js实现二级联动菜单
2019/11/27 Javascript
[02:14]完美“圣”典2016风云人物:xiao8专访
2016/12/01 DOTA
从零学Python之入门(五)缩进和选择
2014/05/27 Python
用Python实现服务器中只重载被修改的进程的方法
2015/04/30 Python
Python装饰器(decorator)定义与用法详解
2018/02/09 Python
Python中GeoJson和bokeh-1的使用讲解
2019/01/03 Python
python与字符编码问题
2019/05/24 Python
Python 计算任意两向量之间的夹角方法
2019/07/05 Python
Python ORM编程基础示例
2020/02/02 Python
Python模拟登入的N种方式(建议收藏)
2020/05/31 Python
Python无损压缩图片的示例代码
2020/08/06 Python
pymongo insert_many 批量插入的实例
2020/12/05 Python
奠基仪式策划方案
2014/05/15 职场文书
商务日语专业的自荐信
2014/05/23 职场文书
信用卡工资证明格式
2014/09/13 职场文书
党员民主评议个人总结
2014/10/20 职场文书
个人整改方案范文
2014/10/25 职场文书
2015年学生会纪检部工作总结
2015/03/31 职场文书
2016年学校禁毒宣传活动工作总结
2016/04/05 职场文书
大学学生会主席竞选稿怎么写?
2019/08/19 职场文书