RE:从0开始阅读Flask源码 Ⅱ

我们已经基本理解了 Flask 的框架原理,即 Flask 通过与 WerkZeug WSGI 框架进行交互实现 HTTP Web Server 所需要具备的功能,但这些框架原理的讨论实际上是将 Flask 框架视作为一个黑箱,或者说视作了 Web Application Framework 的一个实例为前提进行的。我们所探究的是较为一般的 Web Application Framework 与 HTTP Server 如何协同工作从而实现 Web Server 的方式,并基本了解了 Flask 如何作为 Web Application Framework 与 WSGI 交互以启动 WebServer 的。但我们仍不了解 Flask 在抛开 Web Application Framework 所需要做的基本工作以外,其工作流具体是什么样的。我们将路由匹配,上下文,异常处理的具体实现暂时忽略,单纯的从 Flask 接收到请求并响应的工作流程这一抽象层级上来看看 Flask 是如何工作的
请求处理
首先我们知道,当 WerkZeug 获取到一个 HTTP 请求后,会通过 WSGI 接口以调用 Flask 实例的 __call__
方法的方式将该请求传递给 Flask
1 | def __call__( |
随后,__call__
方法会通过调用 wsgi_app
方法进行处理
1 | def wsgi_app( |
忽略掉异常处理与上下文部分的逻辑,wsgi_app
方法的功能就是通过调用其它方法针对获取到的请求生成并返回 response 。而如果没有出现异常情况,这一 response 会通过 full_dispatch_request
方法进行请求调度后生成
在这段代码的注释中 Flask 框架的开发团队写下了他们在此处进行的设计理念。为什么 Flask 不直接在
__call__
方法中就进入到处理流程,而是要让__call__
方法调用wsgi_app
方法,再在wsgi_app
方法中进行处理。其目的是为了解耦以提高框架的可拓展性。如果直接通过__call__
方法进行处理,那么中间件需要重载__call__
方法,即用一个新的类继承 Flask 类来达到目的
1 app = MyMiddleware(app)而 Flask 框架的开发团队认为更好的拓展方式应该是
1 app.wsgi_app = MyMiddleware(app.wsgi_app)这样,中间件的开发者就可以保持着对原本的 Flask 类实例的引用进行接下来的开发
1 | def full_dispatch_request(self) -> Response: |
full_dispatch_request
方法首先会标记自身已处理过一个请求,以便有着针对接收到的第一个请求进行特殊处理需求的业务对此进行拓展。在发送了请求到达信号后,full_dispatch_request
方法会通过 preprocess_request
方法对到达的请求进行预处理
1 | def preprocess_request(self) -> ft.ResponseReturnValue | None: |
进行的预处理操作来自于蓝图与通过 before_first_request
与 before_request
两个方法进行注册的预处理 Hook 函数。若所有预处理 Hook 函数执行完毕后返回值都为 None ,则 preprocess_request
方法也会返回 None ,full_dispatch_request
方法会通过 dispatch_request
方法进行路由分发。
路由分发
1 | def dispatch_request(self) -> ft.ResponseReturnValue: |
dispatch_request
方法会通过上下文获取接收到的请求,若该请求无 URL 路由异常,即无 routing_exception ,则根据 URL 规则的 endpoint 执行相应的视图函数,视图函数如 Flask Demo 所示通过 route
方法进行注册。视图函数执行完毕则向上传递视图函数的返回值给 full_dispatch_request
方法
1 | def full_dispatch_request(self) -> Response: |
最后,在以上流程无异常的情况下, full_dispatch_request
方法通过 finalize_request
方法接受视图函数的返回值并生成响应
响应生成
1 | def finalize_request( |
finalize_request
主要做两件事,首先,它会通过 make_response
方法将视图函数的返回值封装为 HTTP 响应
1 | def make_response(self, rv: ft.ResponseReturnValue) -> Response: |
make_response
方法会根据视图函数的返回值类型进行响应的封装,封装后得到的类型取决于 response_class 成员,默认情况下其为继承了 WerkZeug 的 ResponseBase 类型的 Response 类型。对于接收了视图函数返回值的 rv 变量:
- 若 rv 为一个元组
- rv 为一个三元组,则直接 unpacking 为 rv , HTTP status , HTTP headers
- rv 为一个二元组
- 若二元组的最后一个成员是字典,元组或列表,则将该二元组 unpacking 为 rv 与 HTTP headers
- 否则将二元组 unpacking 为 rv 与 HTTP status
- rv 为其它格式则抛出 TypeError
- 若 rv 为 None ,则抛出 TypeError
- 若 rv 不是一个 response_class 实例
- rv 为 str ,byte , bytearray 类型或者是一个迭代器,则将 rv 封装为一个 response_class 实例
- rv 为字典或列表,则将其封装为一个 json.response 实例
- rv 为 WSGI 底层的 BaseResponse 对象实例或为一个函数(应为满足 WSGI 要求的函数),则通过 response_class.force_type 方法强制转换为 response_class 实例
以上处理逻辑按照顺序执行,最终 rv 会被作为 response_class 实例被返回给 finalize_request
方法
finalize_request
方法在获取到 make_response
方法生成的响应对象后,会再通过 process_response
方法对生成的请求通过 Hook 函数再进行响应处理
1 | def process_response(self, response: Response) -> Response: |
处理响应的 Hook 函数通过 after_request
方法进行注册。
process_response
方法处理过后的 response 会返回给 finalize_request
方法,发送响应信号后若执行无异常则 finalize_request
方法会向上传递 response 给 full_dispatch_request
方法,wsgi_app
方法,最终通过 WSGI 接口响应给请求方。
总结
Flask Web Server 是这样工作的:
Flask 通过 WSGI 接口获取到 HTTP 请求,并通过调用 wsgi_app
方法对请求进行处理。wsgi_app
方法创建该请求的上下文,并调用 full_dispatch_request
方法进行进一步的处理。如果该请求为 Flask Web Server 收到的第一个请求 , full_dispatch_request
会将自己的 _got_first_request 成员设置为 True 以标记已接受过请求(这一成员在 Flask Web Server 启动时被初始化为 False )。在接收到请求后,full_dispatch_request
方法会做三件事
- 通过
preprocess_request
方法对请求进行预处理preprocess_request
方法会执行注册好的 Hook 函数对请求进行预处理,预处理阶段就可能会直接生成响应而非通过视图函数生成响应这一设计是出于业务场景的需求设计的,例如预处理阶段需要对请求方进行鉴权,鉴权不通过便直接响应 403 而非执行相应业务逻辑- Hook 函数通过
before_request
方法或通过蓝图的app_url_value_preprocessor
方法注册 - Hook 函数是顺序执行的,若出现非 None 的 Hook 函数返回值则
preprocess_request
方法会立即返回以将 Hook 函数返回值向上传递
- 通过
dispatch_request
方法进行路由分发- Hook 函数全部执行完毕后没有提前进行返回,即
preprocess_request
方法的返回值为 None 时,Flask 会进入dispatch_request
方法进行路由分发,从请求中提取出 URL 规则,并根据 URL 的 endpoint 执行对应视图函数,并在视图函数执行完毕后向上传递其返回值 - 视图函数通过
route
方法注册 - 路由分发时会对路由异常的请求抛出异常以进行处理
- Hook 函数全部执行完毕后没有提前进行返回,即
- 通过
finalize_request
方法生成响应- 无论响应是由视图函数生成还是由 Hook 函数生成,它们都需要被
make_response
方法封装为 response_class 类型,这一类型默认为 Flask 的 Response 类型 - 响应被
make_response
方法封装为 Response 类型后会被process_response
方法再次进行处理,对响应进行处理的 Hook 函数通过after_request
方法注册
- 无论响应是由视图函数生成还是由 Hook 函数生成,它们都需要被
经过以上处理流程后,full_dispatch_request
方法将响应传递给 wsgi_app
方法,后者将响应通过 WSGI 接口响应给请求方并销毁该请求的上下文
尽管我们在讨论中忽略了 Flask 异常处理,上下文管理,路由匹配与信号相关机制的实现,但这并不妨碍我们基本理解 Flask Web Server 的工作流程的大概样貌。在理解了 Flask 是如何通过它的请求响应循环实现 Web Server 的基本功能后,我们将进一步深入 Flask 框架的路由,上下文管理,异常处理与信号等模块的原理与实现
Tips
Flask 中针对其功能实现定义了自己的 Request 类型与 Response 类型,不过并未在 WerkZeug 的 RequestBase 与 ResponseBase 类的基础上做太多修改
Request 类
1 | class Request(RequestBase): |
Flask 的 Request 类在它所继承的 RequestBase 类的基础上通过 @property 装饰器添加了 Flask 框架的蓝图,视图函数 endpoint 等属性,
Response 类
1 | class Response(ResponseBase): |
Reference
- Title: RE:从0开始阅读Flask源码 Ⅱ
- Author: 7erry
- Created at : 2024-04-11 16:08:24
- Updated at : 2024-04-11 16:08:24
- Link: http://7erry.com/2024/04/11/RE-从0开始阅读Flask源码-Ⅱ/
- License: This work is licensed under CC BY-NC 4.0.