浅谈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计算方程式根的方法
May 07 Python
python转换字符串为摩尔斯电码的方法
Jul 06 Python
Python面向对象编程之继承与多态详解
Jan 16 Python
浅述python中深浅拷贝原理
Sep 18 Python
详解在python操作数据库中游标的使用方法
Nov 12 Python
Python数据可视化:泊松分布详解
Dec 07 Python
python GUI库图形界面开发之PyQt5美化窗体与控件(异形窗体)实例
Feb 25 Python
Python常用类型转换实现代码实例
Jul 28 Python
python“静态”变量、实例变量与本地变量的声明示例
Nov 13 Python
python中的测试框架
Nov 13 Python
Python读取图像并显示灰度图的实现
Dec 01 Python
Python利用capstone实现反汇编
Apr 06 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实现动态获取函数参数的方法示例
2018/04/02 PHP
php curl获取https页面内容,不直接输出返回结果的设置方法
2019/01/15 PHP
PHP开发的文字水印,缩略图,图片水印实现类与用法示例
2019/04/12 PHP
一个不错的用JavaScript实现的UBB编码函数
2007/03/09 Javascript
编辑浪子版表单验证类
2007/05/12 Javascript
Jquery优化效率 提升性能解决方案
2010/09/06 Javascript
javascript中有趣的反柯里化深入分析
2012/12/05 Javascript
主页面中的两个iframe实现鼠标拖动改变其大小
2013/04/16 Javascript
javascript实现显示和隐藏div方法汇总
2015/08/14 Javascript
浅谈JavaScript对象与继承
2016/07/10 Javascript
js自调用匿名函数的三种写法(推荐)
2016/08/19 Javascript
flexslider.js实现移动端轮播
2017/02/05 Javascript
解决vue中post方式提交数据后台无法接收的问题
2018/08/11 Javascript
解决vue props传Array/Object类型值,子组件报错的情况
2020/11/07 Javascript
Python中用altzone()方法处理时区的教程
2015/05/22 Python
python利用dir函数查看类中所有成员函数示例代码
2017/09/08 Python
Python类的继承、多态及获取对象信息操作详解
2019/02/28 Python
python3使用matplotlib绘制散点图
2019/03/19 Python
Python面向对象之多态原理与用法案例分析
2019/12/30 Python
解决Tensorflow 使用时cpu编译不支持警告的问题
2020/02/03 Python
Python图像处理库PIL的ImageFont模块使用介绍
2020/02/26 Python
解决PyCharm无法使用lxml库的问题(图解)
2020/12/22 Python
世界最大的海报和艺术印刷商店:AllPosters.com
2017/02/01 全球购物
Kate Spade美国官网:纽约新兴时尚品牌,以包包闻名于世
2017/11/09 全球购物
Shopty西班牙:缝纫机在线销售
2018/01/26 全球购物
护士实习自我鉴定
2013/10/22 职场文书
自考毕业生自我鉴定
2013/11/04 职场文书
打架检讨书300字
2014/02/02 职场文书
公司外出活动方案
2014/08/14 职场文书
2014年管理人员工作总结
2014/12/01 职场文书
劳动者解除劳动合同通知书
2015/04/16 职场文书
工作感言一句话
2015/08/01 职场文书
运动会跳远广播稿
2015/08/19 职场文书
JavaScript数组 几个常用方法总结
2021/11/11 Javascript
简单聊一聊SQL注入及防止SQL注入
2022/03/23 MySQL
win10截图快捷键win+shift+s没有反应无法截图怎么解决?
2022/08/14 数码科技