侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

浅谈flask源码之请求过程

技术  /  管理员 发布于 7年前   270

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.pyclass 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.pyclass 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 Flaskapp = Flask(__name__)@app.before_first_requestdef before_first_request():  print('before_first_request run')@app.before_requestdef before_request():  print('before_request run')@app.after_requestdef after_request(param):  print('after_request run')  return [email protected]_requestdef 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

添加路由的方法:

[email protected]
2.add_url_rule

我们先来看看@app.route干了什么事情

# 摘自Flask源码 app.pyclass 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.pyclass 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.pyclass 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_appapp = Flask(__name__)@app.before_requestdef before_request():  print 'before_request run'  g.name="Tom"  @app.after_requestdef after_request(response):  print 'after_request run'  print(g.name)  return [email protected]('/')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.pyfrom functools import partialfrom 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通过这样的方式来隔离每个请求.

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


  • 上一条:
    基于tensorflow加载部分层的方法
    下一条:
    TensorFlow利用saver保存和提取参数的实例
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 2024.07.09日OpenAI将终止对中国等国家和地区API服务(0个评论)
    • 2024/6/9最新免费公益节点SSR/V2ray/Shadowrocket/Clash节点分享|科学上网|免费梯子(1个评论)
    • 国外服务器实现api.openai.com反代nginx配置(0个评论)
    • 2024/4/28最新免费公益节点SSR/V2ray/Shadowrocket/Clash节点分享|科学上网|免费梯子(1个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-07
    • 2017-08
    • 2017-09
    • 2018-01
    • 2018-07
    • 2018-08
    • 2018-09
    • 2018-12
    • 2019-01
    • 2019-02
    • 2019-03
    • 2019-04
    • 2019-05
    • 2019-06
    • 2019-07
    • 2019-08
    • 2019-09
    • 2019-10
    • 2019-11
    • 2019-12
    • 2020-01
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-08
    • 2020-09
    • 2020-10
    • 2020-11
    • 2021-04
    • 2021-05
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-12
    • 2022-01
    • 2022-02
    • 2022-03
    • 2022-04
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-08
    • 2023-09
    • 2023-10
    • 2023-12
    • 2024-02
    • 2024-04
    • 2024-05
    • 2024-06
    • 2025-02
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客