浅谈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 相关文章推荐
Linux下Python获取IP地址的代码
Nov 30 Python
Python 常用string函数详解
May 30 Python
Python升级导致yum、pip报错的解决方法
Sep 06 Python
Python的多维空数组赋值方法
Apr 13 Python
TensorFlow基于MNIST数据集实现车牌识别(初步演示版)
Aug 05 Python
python修改字典键(key)的方法
Aug 05 Python
Python通过cv2读取多个USB摄像头
Aug 28 Python
解决python 找不到module的问题
Feb 12 Python
python实现文字版扫雷
Apr 24 Python
Django 解决开发自定义抛出异常的问题
May 21 Python
Python实现随机爬山算法
Jan 29 Python
Python操作Excel的学习笔记
Feb 18 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
使用VisualStudio开发php的图文设置方法
2010/08/21 PHP
ThinkPHP提示错误Fatal error: Allowed memory size的解决方法
2015/02/12 PHP
php实现阳历阴历互转的方法
2015/10/28 PHP
简单谈谈php延迟静态绑定
2016/01/26 PHP
PHP从零开始打造自己的MVC框架之入口文件实现方法详解
2019/06/03 PHP
accesskey 提交
2006/06/26 Javascript
JS 中document.URL 和 windows.location.href 的区别
2009/11/11 Javascript
Javascript 键盘keyCode键码值表
2009/12/24 Javascript
JavaScript学习点滴 call、apply的区别
2010/10/22 Javascript
javascript调试过程中找不到哪里出错的可能原因
2013/12/16 Javascript
Jquery插件仿百度搜索关键字自动匹配功能
2016/05/11 Javascript
浅谈JavaScript事件绑定的常用方法及其优缺点分析
2016/11/01 Javascript
深入理解vue.js双向绑定的实现原理
2016/12/05 Javascript
JavaScript适配器模式详解
2017/10/19 Javascript
微信小程序手机号码验证功能的实例代码
2018/08/28 Javascript
jQuery实现的模仿雨滴下落动画效果
2018/12/11 jQuery
详解javascript 变量提升(Hoisting)
2019/03/12 Javascript
Nodejs中的require函数的具体使用方法
2019/04/02 NodeJs
微信小程序 自定义复选框实现代码实例
2019/09/04 Javascript
ES6中let、const的区别及变量的解构赋值操作方法实例分析
2019/10/15 Javascript
jQuery实现B2B网站后台管理系统侧导航
2020/07/08 jQuery
vue+iview分页组件的封装
2020/11/17 Vue.js
对pandas中Series的map函数详解
2018/07/25 Python
Pandas删除数据的几种情况(小结)
2019/06/21 Python
Python解压 rar、zip、tar文件的方法
2019/11/19 Python
pandas实现导出数据的四种方式
2020/12/13 Python
Python爬虫后获取重定向url的两种方法
2021/01/19 Python
html5 自定义播放器核心代码
2013/12/20 HTML / CSS
文明学生标兵事迹
2014/01/21 职场文书
数控专业自荐书范文
2014/03/16 职场文书
某某同志考察材料
2014/05/28 职场文书
鲁迅故里导游词
2015/02/05 职场文书
应聘教师求职信范文
2015/03/20 职场文书
2016年教师党员公开承诺书
2016/03/24 职场文书
2016年万圣节活动个人总结
2016/04/05 职场文书
基于Python实现一个春节倒计时脚本
2022/01/22 Python