flask工作原理与机制解析

阅读flask源码

为什么要阅读源码呢?

  • 了解某个功能的具体实现。
  • 学习Flask的设计模式和代码组织方式

获取flask源码

# 我们可以查看Flask从诞生(第一次提交)到最新版本的所有提交记录和发布版本。
git clone https://github.com/pallets/flask.git

如何阅读源码

使用pycharm的好处

  • 了解模块代码结构
  • 理清函数的调用关系
  • 进行断点调试

查看pycharm的快捷键

  • 导航栏 -> Help -> Keymap Reference

1. 立足整体

从结构上来说,Flask各个模块联系紧密,并不适合挨个模块从头到尾的线性阅读。我们需要先从整体上了解Flask,就像是读书先看目录一样。对于一个项目来说,我们需要了解flask包由哪些包和模块组成,各个模块又包含哪些类和函数,分别负责实现什么功能。

掌握Flask的整体结构还有一个有效的方法就是阅读Flask的API文档。API文档中包含了所有Flask的主要类、函数以及它们的文档字符串(Doc-string)。这些文档字符串描述了主要功能,还列出了各个参数的类型和作用。在阅读源码时,我们也可以通过这些文档字符串来了解相关的用法。掌握Flask的整体结构会为我们的进一步阅读打下基础。

  • Flask程序包各模块分析表
模块/包 说明
json/ 提供JSON支持
__init__.py 构造文件,导入了所有其它模块中开放的类和函数
__main__.py 用来启动flask命令
_compat.py 定义python2与python3版本兼容代码
app.py 主脚本,实现了WSGI程序对象, 包含Flask类
blueprint.py 蓝本支持, 包含BluePrint类定义
cli.py 提供命令行支持, 包含内置的几个命令
config.py 实现配置相关的对象
ctx.py 实现上下文对象, 比如请求上下文RequestContext
debughelpers.py 一些辅助开发的函数/类
globals.py 定义全局对象,比如request、session等
helpers.py 包含一些常用的辅助函数,比如flash(), url_for()
logging.py 提供日志支持
session.py 实现session功能
signals.py 实现信号支持, 定义了内置的信号
templating.py 模板渲染功能
testing.py 提供用于测试的辅助函数
views.py 提供了类似Django中的类视图, 我们用于编写 Web API 的 MethodView 就在这里定义
wrappers.py 实现 WSGI 封装对象, 比如代表请求和响应的 Request 对象和 Response 对象

2. 逐个击破

在了解了Flask的整体结构后,我们就可以尝试从某一个功能点入手,了解具体的实现方法。在这种阅读方式下,我们可以从某一个函数或类开始,不断地深入。

  • 全局搜索
    双击Shift键。或 单击Navigate -> Search EveryWhere
  • 找到整个项目中所有使用某个类/函数/方法等的位置。
    在某个symbol的名称上单击右键 -> FindUsages
  • 跳转到某个类/函数/方法等的定义处
    在某个symbol的名称上按住Ctrl键, 然后单击

在阅读源码时,我们需要带着两个问题去读:

  • 这段代码实现了什么功能?
  • 它是如何实现的?

3. 由简入繁

早期的代码仅保留了核心特性,而且代码量较少,容易阅读和理解。Flask最早发行的0.1版本只包含一个核心脚本——flask.py,不算空行大概只有四百多行代码,非常mini。

  • 签出0.1版本的代码
cd flask
git checkout 0.1

通过将早期版本和最新版本进行对比,能让我们了解到Flask的变化, 思考这些变化能加深我们对相关只是的理解。在0.1版本的Flask中,有些用法和Flask的当前版本已经有很大出入,所以不要把其中的代码实现直接应用到开发中。

  • 查看提交记录
    在这里插入图片描述

4. 单步调试

在阅读源码时,我们也可以使用这种方式来了解代码执行和调用的流程(调用栈)。

flask发行版本分析

如果打算从头开始了解Flask的变化,那么比较值得阅读的版本是0.1、0.4、0.5、0.7以及最新版本1.0,其他版本大部分的变动都是在重构、优化代码以及修复错误,因此可以略过。
在这里插入图片描述

flask的设计理念

"微"框架

保留核心且易于扩展

两个核心依赖

Flask与Werkzeug的联系非常紧密。从路由处理,到请求解析,再到响应的封装,以及上下文和各种数据结构都离不开Werkzeug,有些函数(比如redirect、abort)甚至是直接从Werkzeug引入的。如果要深入了解Flask的实现原理,必然躲不开Werkzeug。

引入Jinja2主要是因为大多数Web程序都需要渲染模板,与Jinja2集成可以减少大量的工作。除此之外,Flask扩展常常需要处理模板,而集成Jinja2方便了扩展的开发。不过,Flask并不限制你选择其他模板引擎,比如MakoGenshi等。

显式程序对象

  • 显式的程序对象允许多个程序实例存在。
  • 允许你通过子类化Flask类来改变程序行为。
  • Flask需要通过传入的包名称来定位资源(模板和静态文件)。
  • 允许通过工厂函数来创建程序实例,可以在不同的地方传入不同的配置来创建不同的程序实例。
  • 允许通过蓝本来模块化程序。

本地上下文

在多线程环境下,要想让所有视图函数都获取请求对象。最直接的方法就是在调用视图函数时将所有需要的数据作为参数传递进去,但这样一来程序逻辑就变得冗余且不易于维护。另一种方法是将这些数据设为全局变量,但是如果直接将请求对象设为全局变量,那么必然会在不同的线程中导致混乱(非线程安全)。本地线程(thread locals)的出现解决了这些问题。

本地线程就是一个全局对象,你可以使用一种特定线程且线程安全的方式来存储和获取数据。也就是说,同一个变量在不同的线程内拥有各自的值,互不干扰。实现原理其实很简单,就是根据线程的ID来存取数据。Flask没有使用标准库的threading.local(),而是使用了Werkzeug自己实现的本地线程对象werkzeug.local.Local(),后者增加了对Greenlet的优先支持。

Flask使用本地线程来让上下文代理对象全局可访问,比如request、session、current_app、g,这些对象被称为本地上下文对象(context locals)。因此,在不基于线程、greenlet或单进程实现的并发服务器上,这些代理对象将无法正常工作,但好在仅有少部分服务器不被支持。Flask的设计初衷是为了让传统Web程序的开发更加简单和迅速,而不是用来开发大型程序或异步服务器的。但是Flask的可扩展性却提供了无限的可能性,除了使用扩展,我们还可以子类化Flask类,或是为程序添加中间件。

三种程序状态

Flask提供的四个本地上下文对象分别在特定的程序状态下绑定实际的对象。如果我们在访问或使用它们时还没有绑定,那么就会看到初学者经常见到的RuntimeError异常。

在Flask中存在三种状态,分别是程序设置状态(application setup state)、程序运行状态(application runtime state)和请求运行状态(request runtime state)。

1. 程序设置状态

当Flask类被实例化,也就是创建程序实例app后,就进入了程序设置状态。这时所有的全局对象都没有被绑定:

>>> from flask import Flask, current_app, g, request, session
>>> app = Flask(__name__)
>>> current_app, g, request, session
(<LocalProxy unbound>, <LocalProxy unbound>, <LocalProxy unbound>, <LocalProxy unbound>)

2. 程序运行状态

当Flask程序启动,但是还没有请求进入时,Flask进入了程序运行状态。在这种状态下,程序上下文对象current_app和g都绑定了各自的对象。使用flask shell命令打开的Python shell默认就是这种状态,我们也可以在普通的Python shell中通过手动推送程序上下文来模拟:

>>> from flask import Flask, current_app, g, request, session
>>> app = Flask(__name__)
>>> ctx = app.app_context()
>>> ctx.push()
>>> current_app, g, request, session
(<Flask '__main__'>, <flask.g of '__main__'>, <LocalProxy unbound>, <LocalProxy unbound>)

在上面的代码中,我们手动使用app_context()方法创建了程序上下文,然后调用push()方法把它推送到程序上下文堆栈里。默认情况下,当请求进入的时候,程序上下文会随着请求上下文一起被自动激活。但是在没有请求进入的场景,比如离线脚本、测试,或是进行交互式调试的时候,手动推送程序上下文以进入程序运行状态会非常方便。

3. 请求运行状态

当请求进入的时候,或是使用test_request_context()方法、test_client()方法时,Flask会进入请求运行状态。因为当请求上下文被推送时,程序上下文也会被自动推送,所以在这个状态下4个全局对象都会被绑定,我们可以通过手动推送请求上下文模拟:

>>> from flask import Flask, current_app, g, request, session
>>> app = Flask(__name__)
>>> ctx = app.test_request_context()
>>> ctx.push()
>>> current_app, g, request, session
(<Flask '__main__'>, <flask.g of '__main__'>, <Request 'http://localhost/' [GET]>, <NullSession {
   }>)
# 这里因为没有设置程序密钥,所以session是表示无效session的NullSession类实例。
>>> ctx.pop()

这也是为什么你可以直接在视图函数和相应的回调函数里直接使用这些上下文对象,而不用推送上下文——Flask在处理请求时会自动帮你推送请求上下文和程序上下文。

丰富的自定义支持

Flask的灵活不仅体现在易于扩展,不限制项目结构,也体现在其内部的高度可定制化。

from flask import Flask, Request, Response

# 自定义FLask
class MyFlask(Flask):   
	pass

# 自定义请求类
class MyRequest(Request):
	pass

# 自定义响应类
class MyResponse(Response):
	pass

app = MyFlask(__name__)
app.request_class = MyRequest
# make_response()并不是直接实例化Response类,而是实例化被存储在Flask.response_class属性上的类,默认为Response类。
app.response_class = MyResponse

flask与WSGI

WSGI指Python Web Server Gateway Interface,它是为了让Web服务器与Python程序能够进行数据交流而定义的一套接口标准/规范。

客户端和服务器端进行沟通遵循了HTTP协议,可以说HTTP就是它们之间沟通的语言。从HTTP请求到我们的Web程序之间,还有另外一个转换过程——从HTTP报文到WSGI规定的数据格式。WSGI则可以视为WSGI服务器和我们的Web程序进行沟通的语言。

WSGI程序

根据WSGI的规定,Web程序(或被称为WSGI程序)必须是一个可调用对象(callable object)。

  1. 接受environ 和start_response两个参数
  2. 内部调用 start_respons生成header
  3. 返回一个可迭代的响应体
    在这里插入图片描述

wsgi实现示例

# wsgi_test.py
import os, sys
#application是一个可调用的对象,作为参数传入服务器函数

# WSGI服务器
def run_with_cgi(application):
    #wsgi参数设置,解析请求并将其传入environ字典(这里并没有给出解析请求的代码)
    environ = dict(os.environ.items())
    environ['wsgi.input']        = sys.stdin
    environ['wsgi.errors']       = sys.stderr
    environ['wsgi.version']      = (1, 0)
    environ['wsgi.multithread']  = False
    environ['wsgi.multiprocess'] = True
    environ['wsgi.run_once']     = True

    if environ.get('HTTPS', 'off') in ('on', '1'):
        environ['wsgi.url_scheme'] = 'https'
    else:
        environ['wsgi.url_scheme'] = 'http'

    headers_set = []
    headers_sent = []

    def write(data):
        if not headers_set:
             raise AssertionError("write() before start_response()")

        elif not headers_sent:
             # Before the first output, send the stored headers
             status, response_headers = headers_sent[:] = headers_set
             sys.stdout.write('Status: %s\r\n' % status)
             for header in response_headers:
                 sys.stdout.write('%s: %s\r\n' % header)
             sys.stdout.write('\r\n')

        sys.stdout.write(data)
        sys.stdout.flush()
    #start_response的作用是生成响应头
    def start_response(status, response_headers, exc_info=None):
        if exc_info:
            try:
                if headers_sent:
                    # Re-raise original exception if headers sent
                    raise exc_info[0], exc_info[1], exc_info[2]
            finally:
                exc_info = None     # avoid dangling circular ref
        elif headers_set:
            raise AssertionError("Headers already set!")

        headers_set[:] = [status, response_headers]
        return write
    #获取application生成的http报文,application返回一个可迭代对象
    result = application(environ, start_response)
    try:
        for data in result:
            if data:    # don't send headers until body appears
                #如果获取到data,就调用write函数输出header和data
                write(data)
        if not headers_sent:
            write('')   # send headers now if body was empty
    finally:
        if hasattr(result, 'close'):
            result.close()

# 函数实现web程序
def simple_app(environ, start_response):
    """Simplest possible application object
    	environ:包含了请求的所有信息的字典。
    	start_response:需要在可调用对象中调用的函数,用来发起响应,参数是状态码、响应头部等。
	"""
    status = '200 OK'
    #构建响应头
    response_headers = [('Content-type', 'text/plain')]
    #调用作为参数传入的start_response,其已经在server中定义
    start_response(status, response_headers)
    #返回可迭代的响应体(即使只有一行,也要写成列表的形式)
    return [b'Hello world!\n']

# 类实现web程序
class AppClass:
    """Produce the same output, but using a class

    (Note: 'AppClass' is the "application" here, so calling it
    returns an instance of 'AppClass', which is then the iterable
    return value of the "application callable" as required by
    the spec.

    If we wanted to use *instances* of 'AppClass' as application
    objects instead, we would have to implement a '__call__'
    method, which would be invoked to execute the application,
    and we would need to create an instance for use by the
    server or gateway.
    """

    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response

    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        self.start(status, response_headers)
        #__iter__和yield结合,实现了生成器函数,
        #result = AppClass(environ, start_response)将返回一个可迭代的实例
        yield b"Hello world!\n"

run_with_cgi(simple_app)
run_with_cgi(AppClass)

Flask中的WSGI实现

class Flask(_PackageBoundObject):
	...
	def wsgi_app(self, environ, start_response):
		...
	def __call__(self, environ, start_response):
		"""Shortcut for :attr:`wsgi_app`."""
		return self.wsgi_app(environ, start_response)

WSGI服务器

Python提供了一个wsgiref库,可以在开发时使用

from wsgiref.simple_server import make_server

def hello(environ, start_response):
    ...

server = make_server('localhost', 5000, hello)
server.serve_forever()

中间件

WSGI允许使用中间件(Middleware)包装(wrap)程序,为程序在被调用前添加额外的设置和功能。当请求发送来后,会先调用包装在可调用对象外层的中间件。这个特性经常被用来解耦程序的功能,这样可以将不同功能分开维护,达到分层的目的,同时也根据需要嵌套。

wsgi使用中间件示例

from wsgiref.simple_server import make_server

def hello(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/html')]
    start_response(status, response_headers)
   	return [b'<h1>Hello, web!</h1>']

class MyMiddleware(object):
    def __init__(self, app):
    	self.app = app
   	def __call__(self, environ, start_response):
   		def custom_start_response(status, headers, exc_info=None):
   		    headers.append(('A-CUSTOM-HEADER', 'Nothing'))           
   		    return start_response(status, headers)
   		return self.app(environ, custom_start_response)

wrapped_app = MyMiddleware(hello)
server = make_server('localhost', 5000, wrapped_app)
server.serve_forever()

Flask使用中间件示例

class MyMiddleware(object):
    pass

app = Flask(__name__)
# 因为Flask中实际的WSGI可调用对象是Flask.wsgi_app()方法
app.wsgi_app = MyMiddleware(app.wsgi_app)

Werkzeug内置中间件

  • ProxyFix: 可以用来对反向代理转发的请求进行修正
  • werkzeug.contrib.profiler.ProfilerMiddleware: 可以在处理请求时进行性能分析
  • werkzeug.wsgi.DispatcherMiddleware: 可以让你将多个WSGI程序作为一个“程序集”同时运行,你需要传入多个程序实例,并为这些程序设置对应的URL前缀或子域名来分发请求。

Flask的工作流程与机制

Flask中的请求响应循环

1. 程序启动

def run_simple(hostname, port, application, use_reloader=False,
               use_debugger=False, use_evalex=True,
               extra_files=None, reloader_interval=1,
               reloader_type='auto', threaded=False,
               processes=1, request_handler=None, static_files=None,
               passthrough_errors=False, ssl_context=None):

    if use_debugger:	# 判断是否使用调试器
        from werkzeug.debug import DebuggedApplication
        # 使用DebuggedApplication中间件为程序添加调试功能。
        application = DebuggedApplication(application, use_evalex)
    if static_files:
        from werkzeug.wsgi import SharedDataMiddleware
        # 使用SharedDataMiddleware中间件为程序添加提供(serve)静态文件的功能。
        application = SharedDataMiddleware(application, static_files)
	...
    def inner():
        try:
            fd = int(os.environ['WERKZEUG_SERVER_FD'])
        except (LookupError, ValueError):
            fd = None
        srv = make_server(hostname, port, application, threaded,
                          processes, request_handler,
                          passthrough_errors, ssl_context,
                          fd=fd)	# 创建服务器
        if fd is None:
            log_startup(srv.socket)
        srv.serve_forever()		# 运行服务器

    if use_reloader:	# 判断是否使用重载器
    	...
        from werkzeug._reloader import run_with_reloader
        run_with_reloader(inner, extra_files, reloader_interval,
                          reloader_type)
    else:
        inner()

2. 请求In

  • WSGI程序
    请求上下文在Flask类的wsgi_app方法的开头创建,在这个方法的最后没有直接调用pop()方法,而是调用了auto_pop()方法来移除。也就是说,请求上下文的生命周期开始于请求进入调用wsgi_app()时,结束于响应生成后。
# flask/app.py

class Flask(_PackageBoundObject):
	...
    def wsgi_app(self, environ, start_response):
       """The actual WSGI application.  This is not implemented in
       `__call__` so that middlewares can be applied without losing a
       reference to the class.  So instead of doing this::

           app = MyMiddleware(app)

       It's a better idea to do this instead::

           app.wsgi_app = MyMiddleware(app.wsgi_app)

       Then you still have the original application object around and
       can continue to call methods on it.

       .. versionchanged:: 0.7
          The behavior of the before and after request callbacks was changed
          under error conditions and a new callback was added that will
          always execute at the end of the request, independent on if an
          error occurred or not.  See :ref:`callbacks-and-errors`.

       :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)
       ctx.push()
       error = None
       try:
       	# 首先尝试从Flask.full_dispatch_request()方法获取响应,如果出错那么就根据错误类型来生成错误响应。
           try:
               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)

    def __call__(self, environ, start_response):
        """Shortcut for :attr:`wsgi_app`."""
        return self.wsgi_app(environ, start_response)
  • 请求调度
# flask/app.py

class Flask(_PackageBoundObject):
	...
	def full_dispatch_request(self):
	      """Dispatches the request and on top of that performs request
	      pre and postprocessing as well as HTTP exception catching and
	      error handling.
	
	      .. versionadded:: 0.7
	      """
	      self.try_trigger_before_first_request_functions()
	      try:
	          request_started.send(self)	 # 发送请求进入信号
	          rv = self.preprocess_request()	 # 预处理请求, 这会执行所有使用before_re-quest钩子注册的函数。
	          if rv is None:
	              rv = self.dispatch_request()	# 会匹配并调用对应的视图函数,获取其返回值
	      except Exception as e:
	          rv = self.handle_user_exception(e)	# 处理异常
	      return self.finalize_request(rv)	# 接收视图函数返回值生成响应
	...

3. 响应Out

# flask/app.py

class Flask(_PackageBoundObject):
    def finalize_request(self, rv, from_error_handler=False):
        """Given the return value from a view function this finalizes
        the request by converting it into a response and invoking the
        postprocessing functions.  This is invoked for both normal
        request dispatching as well as error handlers.

        Because this means that it might be called as a result of a
        failure a special safe mode is available which can be enabled
        with the `from_error_handler` flag.  If enabled, failures in
        response processing will be logged and otherwise ignored.

        :
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值