Flask 源码解析:请求和响应

Falsk 示例

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask


app = Flask(__name__)

@app.route('/index')
def index():
return "index"

if __name__ == '__main__':
app.run()

werkzeug 源码 讲到,执行 app.run() 最后执行的是 run_simple()run_simple() 做了两件事

  • 一件 make_server,创建服务器
  • 一件 server_forver,一直在监听端口

run_simple 三个参数

  • environ 为初步处理的 request 请求
  • start_response 为回调函数
  • Response 的实例化对象,也是具体的 request 入口,负责具体的逻辑,不同的框架其实是第三个参数不同。

当有请求过来之后,server_forever 会将 run_simple() 中的第三个参数加括号执行,并传入参数 (environ, start_response)

请求和响应整体流程

当请求到来,执行 __call__ 方法。看一下源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
class Flask(_PackageBoundObject):

def __call__(self, environ, start_response):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app` which can be
wrapped to applying middleware."""

# environ:请求相关的所有数据,wsgi 将原生请求 做了一个初步的处理,把字符串分割。(wsgi做了初步封装)
# start_response:用于设置响应相关的所有数据。
return self.wsgi_app(environ, start_response)


# 这个文件是 Flask 的整个执行流程的入口
def wsgi_app(self, environ, start_response):
"""
1、获取 environ 并再次封装。返回 request、session(此时为空)、app(其实就是 self)
ctx.request = Request(environ)
ctx.session = None
ctx.app=app
"""
ctx = self.request_context(environ) # 实际执行ctx = RequestContext(self, environ)
error = None
try:
try:
# 2、把 app_ctx,request_ctx 放到“某个神奇”的地方。
# 对 session 的操作,执行SecureCookieSessionInterface.open_session(),去 cookie 中获取 session 的值,反序列化解密之后给ctx.session 重新赋值(默认 session 的值存在 cookie 中)。
ctx.push()

# 3、执行视图函数,去“某个神奇”的地方获取 session
# 然后加密,序列化,写入cookie,然后返回
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
# 4、此处调用 flask.Response.__call__ 方法
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None

# 5、“某个神奇”的地方位置清空 (请求结束)
ctx.auto_pop(error)

# 3、执行视图函数
def full_dispatch_request(self):
self.try_trigger_before_first_request_functions()
try:
request_started.send(self)
rv = self.preprocess_request() # 获取 request
if rv is None:
# 3.1、调用视图函数,rv 就是 view_func 的返回值
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
# 3.2、视图函数执行完毕的善后工作
return self.finalize_request(rv)

# 3.2、视图函数执行完毕的善后工作
def finalize_request(self, rv, from_error_handler=False):
# 构建 Response 对象的具体方法
response = self.make_response(rv)
try:
# 执行一些hook,其中包括保存 session
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

# 注释说的很清楚:就是基于 view_func 返回值的格式,尝试构造一个 Response 对象并返回
def make_response(self, rv: ft.ResponseReturnValue) -> Response:
"""Convert the return value from a view function to an instance of
:attr:`response_class`.
"""

status = headers = None

# 如果返回的是tuple,会基于约定尝试拆包
# unpack tuple returns
if isinstance(rv, tuple):
len_rv = len(rv)

# a 3-tuple is unpacked directly
if len_rv == 3:
rv, status, headers = rv # type: ignore[misc]
# decide if a 2-tuple has status or headers
elif len_rv == 2:
if isinstance(rv[1], (Headers, dict, tuple, list)):
rv, headers = rv
else:
rv, status = rv # type: ignore[assignment,misc]
# other sized tuples are not allowed
else:
raise TypeError(
"The view function did not return a valid response tuple."
" The tuple must have the form (body, status, headers),"
" (body, status), or (body, headers)."
)

# the body must not be None
if rv is None:
raise TypeError(
f"The view function for {request.endpoint!r} did not"
" return a valid response. The function either returned"
" None or ended without a return statement."
)

# response_class 就是 flask.Response 类
# make sure the body is an instance of the response class
if not isinstance(rv, self.response_class):
if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, _abc_Iterator):
# let the response class set the status and headers instead of
# waiting to do it manually, so that the class can handle any
# special logic
rv = self.response_class(
rv,
status=status,
headers=headers, # type: ignore[arg-type]
)
status = headers = None
elif isinstance(rv, (dict, list)):
rv = self.json.response(rv)
elif isinstance(rv, BaseResponse) or callable(rv):
# evaluate a WSGI callable, or coerce a different response
# class to the correct type
try:
rv = self.response_class.force_type(
rv, request.environ # type: ignore[arg-type]
)
except TypeError as e:
raise TypeError(
f"{e}\nThe view function did not return a valid"
" response. The return type must be a string,"
" dict, list, tuple with headers or status,"
" Response instance, or WSGI callable, but it"
f" was a {type(rv).__name__}."
).with_traceback(sys.exc_info()[2]) from None
else:
raise TypeError(
"The view function did not return a valid"
" response. The return type must be a string,"
" dict, list, tuple with headers or status,"
" Response instance, or WSGI callable, but it was a"
f" {type(rv).__name__}."
)

rv = t.cast(Response, rv)
# prefer the status if it was provided
if status is not None:
if isinstance(status, (str, bytes, bytearray)):
rv.status = status
else:
rv.status_code = status

# extend existing headers with provided headers
if headers:
rv.headers.update(headers) # type: ignore[arg-type]

return rv

# 执行一些hook
def process_response(self, response):
ctx = _request_ctx_stack.top
bp = ctx.request.blueprint
funcs = ctx._after_request_functions
if bp is not None and bp in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
for handler in funcs:
response = handler(response)
if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response) # 保存session
return response

def request_context(self, environ): # self 是 app,即 Flask 的实例化
return RequestContext(self, environ)

下面来看下 Response 对象,HTTP 格式的返回就是三部分 status,headers, body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 和 request 一样没有啥内容,主要功能都是继承来的  
class Response(ResponseBase, JSONMixin):

default_mimetype = "text/html"

# 和 request 一样的组合类
class Response(
BaseResponse,
ETagResponseMixin,
WWWAuthenticateMixin,
CORSResponseMixin,
ResponseStreamMixin,
CommonResponseDescriptorsMixin,
):
pass

class BaseResponse(object):
# 类属性作为实例属性的默认值
#: the charset of the response.
charset = "utf-8"


def __init__(
self,
response=None,
status=None,
headers=None,
mimetype=None,
content_type=None,
direct_passthrough=False,
):
# 构建header
if isinstance(headers, Headers):
self.headers = headers
elif not headers:
self.headers = Headers()
else:
self.headers = Headers(headers)

# 构建 response
if response is None:
self.response = []
elif isinstance(response, (text_type, bytes, bytearray)):
# str 或者bytes类型的返回值处理
self.set_data(response)
else:
self.response = response

def set_data(self, value):
# str类型就encode为bytes
if isinstance(value, text_type):
value = value.encode(self.charset)
else:
value = bytes(value)
# 最终的response
self.response = [value]
if self.automatically_set_content_length:
self.headers["Content-Length"] = str(len(value))

# 此方法对应上面的 response(environ, start_response)
# 以wsgi app的形式返回数据
def __call__(self, environ, start_response):
# 仔细看doc
"""
Process this response as WSGI application.
:param environ: the WSGI environment.
:param start_response: the response callable provided by the WSGI server.
:return: an application iterator
"""
# 把当前已有的响应信息 转换为wsgi格式返回
app_iter, status, headers = self.get_wsgi_response(environ)
# 响应 status 和 headers
# start_response 为 WSGI server调用flask_app() 是传入的可调用对象
start_response(status, headers)
# 返回一个可迭代对象
return

可以看出,最终的 response 转换为 二进制形式。下面再来看下 Headers 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Headers(object):
"""
An object that stores some headers. It has a dict-like interface
but is ordered and can store the same keys multiple times.

This data structure is useful if you want a nicer way to handle WSGI
headers which are stored as tuples in a list.
"""
def __init__(self, defaults=None):
# 底层是一个 list
self._list = []
if defaults is not None:
if isinstance(defaults, (list, Headers)):
self._list.extend(defaults)
else:
self.extend(defaults)

# 对应[]或者get()
def __getitem__(self, key, _get_mode=False):
# 在列表中搜索对应的header
ikey = key.lower()
for k, v in self._list:
if k.lower() == ikey:
return

流程

  1. 当一个 HTTP 请求来临时,WSGI server 将 HTTP 格式数据转换为 WSGI 格式数据,并调用 Flask.__call__(environ, start_response)__call__ 方法调用调用 wsgi_app 方法,具体流程如下:
    1. 使用 environ 来构建 flask.Request 对象,这样后续 view_function 中我们可以通过 flask.request 从中获取需要的请求信息
    2. 根据 environ 路由到具体的 view_function 执行,并返回
    3. view_function 的返回转换为 flask.Response 对象,并最终以 WSGI 的方式将 status、headers、response body 返回给 WSGI Server
  2. 最后,WSGI Server 再将 WSGI 格式的数据转换为 HTTP 格式的数据返回给客户端

可见:WSGI Server 主要进行请求数据的格式转换:HTTP <–> WSGI。然后 Flask 作为 WSGI app 以 WSGI 的方式被调用、执行、返回。

session 流程

源码 wsgi_app 中,request_context 把我们的 environ 封装到了一个类 RequestContext。我们看下类 RequestContext 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
class RequestContext(object):
"""The request context contains all request relevant information. It is
created at the beginning of the request and pushed to the
`_request_ctx_stack` and removed at the end of it. It will create the
URL adapter and request object for the WSGI environment provided.
"""

def __init__(self, app, environ, request=None):
self.app = app # 对 app 进行封装
if request is None: # 对 environ 进行第二次封装,封装成一个 Request 对象
request = app.request_class(environ) # request_class = Request 实际执行为 request = Request(environ)
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.flashes = None
self.session = None # 为session 赋值 None
self._implicit_app_ctx_stack = []
self.preserved = False
self._preserved_exc = None
self._after_request_functions = []
self.match_request()

def match_request(self):
"""Can be overridden by a subclass to hook into the matching
of the request.
"""
try:
url_rule, self.request.view_args = \
self.url_adapter.match(return_rule=True)
self.request.url_rule = url_rule
except HTTPException as e:
self.request.routing_exception = e

def push(self): # 点开ctx.push(),实际执行这里
"""Binds the request context to the current context."""
# If an exception occurs in debug mode or if context preservation is
# activated under exception situations exactly one context stays
# on the stack. The rationale is that you want to access that
# information under debug situations. However if someone forgets to
# pop that context again we want to make sure that on the next push
# it's invalidated, otherwise we run at risk that something leaks
# memory. This is usually only a problem in test suite since this
# functionality is not active in production environments.
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)

# Before we push the request context we have to ensure that there
# is an application context.
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)

# Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context.
# Only open a new session if this is the first time the request was
# pushed, otherwise stream_with_context loses the session.
# 这里这里
# 当请求进来时,session 肯定为空,因为上面设置的是空。Flask 的 session 加密,序列化之后保存在 cookie 中
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)

if self.session is None:
self.session = session_interface.make_null_session(self.app)

if self.url_adapter is not None:
self.match_request()

def pop(self, exc=_sentinel):
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the
:meth:`~flask.Flask.teardown_request` decorator.

.. versionchanged:: 0.9
Added the `exc` argument.
"""
app_ctx = self._implicit_app_ctx_stack.pop()

try:
clear_request = False
if not self._implicit_app_ctx_stack:
self.preserved = False
self._preserved_exc = None
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)

# If this interpreter supports clearing the exception information
# we do that now. This will only go into effect on Python 2.x,
# on 3.x it disappears automatically at the end of the exception
# stack.
if hasattr(sys, "exc_clear"):
sys.exc_clear()

request_close = getattr(self.request, "close", None)
if request_close is not None:
request_close()
clear_request = True
finally:
rv = _request_ctx_stack.pop()

# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
rv.request.environ["werkzeug.request"] = None

# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)

assert rv is self, "Popped wrong request context. (%r instead of %r)" % (
rv,
self,
)

def auto_pop(self, exc):
if self.request.environ.get("flask._preserve_context") or (
exc is not None and self.app.preserve_context_on_exception
):
self.preserved = True
self._preserved_exc = exc
else:
self.pop(exc)

def __enter__(self):
self.push()
return self

def __exit__(self, exc_type, exc_value, tb):
# do not pop the request stack if we are in debug mode and an
# exception happened. This will allow the debugger to still
# access the request object in the interactive shell. Furthermore
# the context can be force kept alive for the test client.
# See flask.testing for how this works.
self.auto_pop(exc_value)

if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)

def __repr__(self):
return "<%s '%s' [%s] of %s>" % (
self.__class__.__name__,
self.request.url,
self.request.method,
self.app.name,
)

回到 wsgi_app 中的 ctx 实例,ctx 会得到:

  1. ctx.request=Request(environ)。environ 是一个原始的请求对象。我们就可以执行 .args.form.method"等。
  2. ctx.session=None
  3. ctx.app=app

session_interface 源码

1
2
class Flask(PackageBoundObject):
session_interface = SecureCookieSessionInterface()

push 其实调用的是 open_sessionpop 其实调用的是 save_session,看 open_sessionsave_session 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class SecureCookieSessionInterface(SessionInterface):

session_class = SecureCookieSession # 其实就是 dict 字典类型

# 在 cookie 中取出 session 的 key,然后获取对应的 session 值,并返回
def open_session(self, app, request):
s = self.get_signing_serializer(app) # 加密
if s is None:
return None
# 去 cookie 中取值。这个 val 就是 session 的值,session_cookie_name这个就是配置文件的session_cookie_name
val = request.cookies.get(app.session_cookie_name)
if not val: # 如果是空的
return self.session_class() # 则返回空字典
max_age = total_seconds(app.permanent_session_lifetime)
try:
# 在内部加载,解密,反序列化
data = s.loads(val, max_age=max_age)
# 实例化对象
return self.session_class(data) # {"k1":123}
except BadSignature:
return self.session_class()

def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)

# If the session is modified to be empty, remove the cookie.
# If the session is empty, return without setting the cookie.
if not session:
if session.modified:
response.delete_cookie(
app.session_cookie_name,
domain=domain,
path=path
)

return

# Add a "Vary: Cookie" header if the session was accessed at all.
if session.accessed:
response.vary.add('Cookie')

if not self.should_set_cookie(app, session):
return

httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app)
samesite = self.get_cookie_samesite(app)
expires = self.get_expiration_time(app, session)

# 前面不看,暂时用不到
# 对 session 进行加密,然后 dumps 编程字符串
val = self.get_signing_serializer(app).dumps(dict(session))
# 然后在写入用户的浏览器上
response.set_cookie(
app.session_cookie_name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite
)

open_session 返回啥 self.session 中就是啥。ctx.push() 中关于 session 的流程:执行 SecureCookieSessionInterface.open_session(),去 cookie 中获取值,并反序列化解密之后给 ctx.session 重新赋值。

这就是 Flask 框架中 sesion 的请求流程。说了这么多,其实真正实现咱们想要的功能的就是两个方法:open_sessionsave_session。请求进来执行open_session,请求走的时候执行 save_session

加了 session 的流程

  1. 当一个 HTTP 请求来临时,WSGI server 将 HTTP 格式数据转换为 WSGI 格式数据,并调用 Flask.__call__(environ, start_response)__call__ 方法调用 wsgi_app 方法,具体流程如下:
    • 将 request 和 session 相关封装到 ctx = RequestContext 对象中,此时 session 为空,request 二次封装
    • 将 app 和 g 封装到 app_ctx = AppContext 对象中。
    • 再通过 LocalStack 对象将 ctx、app_ctx 封装到 Local 对象中。
    • 根据 environ 路由到具体的 view_function 执行,并返回
    • view_function 的返回转换为 flask.Response 对象,并最终以 WSGI 的方式将 status、headers、response body 返回给 WSGI Server
  2. 最后,WSGI Server 再将 WSGI 格式的数据转换为 HTTP 格式的数据返回给客户端
  • 获取数据:通过 LocalProxy 对象+偏函数,调用 LocalStack 去 Local 中获取相应 ctx、app_ctx 中封装的值。
  • 请求结束:调用 LocalStack 的 pop 方法,将 ctx 和 app_ctx 移除。

Reference


Flask 源码解析:请求和响应
https://flepeng.github.io/021-Python-32-框架-Flask-Flask-源码解析:请求和响应/
作者
Lepeng
发布于
2024年9月25日
许可协议