浅谈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的垃圾回收机制深入分析
Jul 16 Python
把MySQL表结构映射为Python中的对象的教程
Apr 07 Python
Python的Flask框架中实现简单的登录功能的教程
Apr 20 Python
Python的Asyncore异步Socket模块及实现端口转发的例子
Jun 14 Python
python实现的二叉树定义与遍历算法实例
Jun 30 Python
python使用Turtle库绘制动态钟表
Nov 19 Python
Python代码太长换行的实现
Jul 05 Python
matplotlib.pyplot画图并导出保存的实例
Dec 07 Python
python使用bs4爬取boss直聘静态页面
Oct 10 Python
python中的时区问题
Jan 14 Python
python工具——Mimesis的简单使用教程
Jan 16 Python
python中requests库+xpath+lxml简单使用
Apr 29 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
Ajax PHP 边学边练 之三 数据库
2009/11/26 PHP
使用PHP实现密保卡功能实现代码<打包下载直接运行>
2011/10/09 PHP
Zend Framework使用Zend_Loader组件动态加载文件和类用法详解
2016/12/09 PHP
通过javascript把图片转化为字符画
2013/10/24 Javascript
javascript实现阻止iOS APP中的链接打开Safari浏览器
2014/06/12 Javascript
学习JavaScript正则表达式
2015/11/13 Javascript
JavaScript tab选项卡插件实例代码
2016/02/23 Javascript
在JSP中如何实现MD5加密的方法
2016/11/02 Javascript
详解AngularJS中的表单验证(推荐)
2016/11/17 Javascript
js中的触发事件对象event.srcElement与event.target详解
2017/03/15 Javascript
微信小程序实现选项卡功能
2020/06/19 Javascript
Vue.js用法详解
2017/11/13 Javascript
详解VUE中常用的几种import(模块、文件)引入方式
2018/07/03 Javascript
微信小程序实现循环动画效果
2018/07/16 Javascript
详解在网页上通过JS实现文本的语音朗读
2019/03/28 Javascript
在layui框架中select下拉框监听更改事件的例子
2019/09/20 Javascript
[01:43]3.19DOTA2发布会 三代刀塔人第三代
2014/03/25 DOTA
Python编程实现控制cmd命令行显示颜色的方法示例
2017/08/14 Python
python3+PyQt5实现自定义分数滑块部件
2018/04/24 Python
python中while和for的区别总结
2019/06/28 Python
tensorflow模型转ncnn的操作方式
2020/05/25 Python
Django用户登录与注册系统的实现示例
2020/06/03 Python
OpenCV 之按位运算举例解析
2020/06/19 Python
Python虚拟环境的创建和使用详解
2020/09/07 Python
Appium+Python实现简单的自动化登录测试的实现
2021/01/26 Python
经济实惠的名牌太阳镜和眼镜:Privé Revaux
2021/02/07 全球购物
连锁酒店店长职责范本
2014/02/13 职场文书
大学学风建设方案
2014/05/04 职场文书
餐饮周年庆活动方案
2014/08/14 职场文书
天猫活动策划方案
2014/08/21 职场文书
实习协议书
2015/01/27 职场文书
综合测评个人总结
2015/03/03 职场文书
2015年度公共机构节能工作总结
2015/05/26 职场文书
汽车销售员工作总结
2015/08/12 职场文书
CentOS7和8下安装Maven3.8.4
2022/04/07 Servers
Android开发手册Chip监听及ChipGroup监听
2022/06/10 Java/Android