Flask 作为wsgi app 启动流程

在wsgi server 每次处理请求时, 需要调用一个方法处理请求, 返回响应, 最简单的处理如下

def request_response(envrion, start_response):
    # do stuff
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['hello, world']

environ 作为请求参数传递, 是个字典, 包含了 http 请求所必须的环境变量, 比如:

REQUEST_METHOD: 请求方法,是个字符串,'GET', 'POST'

PATH_INFO:         HTTP请求的path中剩余的部分,也就是application要处理的部分

QUERY_STRING:     HTTP请求中的查询字符串URL中?后面的内容

CONTENT_TYPE:     HTTP headers中的content-type内容

CONTENT_LENGTH: HTTP headers中的content-length内容

SERVER_NAME和SERVER_PORT: 服务器名和端口,这两个值和前面的SCRIPT_NAME, PATH_INFO拼起来可以得到完整的URL路径

SERVER_PROTOCOL: HTTP协议版本HTTP/1.0或者HTTP/1.1

HTTP_:             HTTP请求中的headers对应

以及一下请求信息和服务参数

wsgi.version:         (1, 0) 元组,代表 WSGI 1.0 

wsgi.url_scheme:     字符串,表示应用请求的 URL 所属的协议,通常为「http」或「https

wsgi.input:         类文件对象的输入流,用于读取 HTTP 请求包体的内容。(服务端在应用端请求时开始读取,或者预读客户端请求包体内容缓存在内存或磁盘中,或者视情况而定采用任何其他技术提供此输入流。)

wsgi.errors:         类文件对象的输出流,用于写入错误信息,以集中规范地记录程序产生的或其他相关错误信息。这是一个文本流,即应用应该使用「n」来表示行尾,并假定其会被服务端正确地转换。(在 str 类型是 Unicode 编码的平台上,错误流应该正常接收并记录任意 Unicode 编码而不报错,并且允许自行替代在该平台编码中无法渲染的字符。)很多 Web 服务器中 wsgi.errors 是主要的错误日志,也有一些使用 sys.stderr 或其他形式的文件来记录。Web 服务器的自述文档中应该包含如何配置错误日志以及如何找到记录的位置。服务端可以在被要求的情况下,向不同的应用提供不同的错误日志。

wsgi.multithread:    如果应用对象可能会被同一进程的另一个线程同步调用,此变量值为真,否则为假。

wsgi.multiprocess:    如果同一个应用对象可能会被另一个进程同步调用,此变量值为真,否则为假。

wsgi.run_once:        如果服务端期望(但是不保证能得到满足)应用对象在生命周期中之辈调用一次,此变量值为真,否则为假。一般只有在基于类似 CGI 的网关服务器中此变量才会为真。

获取请求后, 在do stuff 部分, 处理业务, 调用start_response 然后响应给客户端(wsgi server), 先返回的是status, header, response body 在之后返回, 如本例中的hello, world

那Flask 是怎么做的呢

在flask.Flask 中, 有一个

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

这就是Flask请求的入口和响应的位置

Flask 的请求处理流程

1. 路由注册

其关键处理的函数流程如下

我们有个handlers 文件

内有一个test.py

test = Blueprint('test', __name__)

@test.route('/hello')
def show_page():
    return render_template('hello.html') # 加载视图

为什么通过蓝图的route 方法后, 请求就能找到相应的调用方法呢

启动过程1:

blueprint.Blueprint.route ==>

blueprint.Blueprint.add_url_rule ==>

blueprint.Blueprint.record

以下是blueprint.Blueprint.route 对应的实现

def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop("endpoint", f.__name__)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

decorator内的内容, 在服务启动的过程中, 是会先调用执行的, 这是装饰器执行的一个流程, 也是装饰器路由注册的一个先决条件

上面, rule 是路由规则, 我们这里是/hello, endpoint 是执行的模块函数名, 我们这里是test.show_page

然后调用self.add_url_rule, 实现如下

def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
    ...
    self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options))

这里将ruleendpoint 包裹在了一个匿名函数里, 其作用后面再说

record 方法中, 如下

def record(self, func):
    ...
    self.deferred_functions.append(func)

func 为 lambda s: s.add_url_rule(rule, endpoint, view_func, **options), 是一个匿名函数(这里我们暂且叫他N), 当然, 只有在这个匿名函数被调用的时候, s的值才知道,

至此, 路由rule 以及对应的处理函数的映射, 都最终保存在了 deferred_functions内, 但是还没有和Flask app 产生关系, 所有的内容都还在blueprint

启动过程2:

flask.Flask.register_blueprint

我们在使用蓝图的时候, 还需要做这一步,

这是将已经注册好的路由(每个路由存储在响应的Blueprint对象中)注册到实例化好的Flask对象,

怎么注册的呢

from handlers.test import test
app.register_blueprint(test)

在这register_blueprint 最后会调用 blueprint.register(self, options, first_registration)

其实现如下

def register(self, app, options, first_registration=False):
    self._got_registered_once = True
    state = self.make_setup_state(app, options, first_registration)
    if self.has_static_folder:
        state.add_url_rule(self.static_url_path + '/<path:filename>',
        view_func=self.send_static_file,
        endpoint='static')

    for deferred in self.deferred_functions:
        deferred(state)

在这里, 我们启动过程1保存了路由的deferred_functionsapp 产生了关系, deferred 就是上面record 里传递的匿名函数N, state 也就是匿名函数的s 参数

点击到make_setup_state里面可以看到, 实际返回了一个BlueprintSetupState对象(这里暂且叫S), 在这个对象实例化的时候, 把app传递到了进去, defferred(state)N(state),

N里, S.add_url_rule里, 执行self.app.add_url_rule(rule, '%s.%s' (self.blueprint.name, endpoint), view_func, defaults=defaults, **options),

到这里, 终于把路由rule对应的执行方法endpoint 和 flask APP 给关联起来了

在服务启动完成后, url 和对应的执行函数已经是一一映射了, 剩下的就是根据请求路由过去了

每次请求发送过来后, flask APP 根据 url rule 找到相应的endpoint, 执行endpoint, 再将响应返回给客户端,

right