Flask应用启动阶段
1. 前情提要
封装的app
在前面的flask应用程序准备阶段,app
中封装了一些重要的参数,运行程序代码后,可通过下面的代码查看各属性的值:
if __name__ == "__main__":
# app.run()
print(app.config)
print(app.before_request_funcs)
print(app.after_request_funcs)
print(app.before_first_request_funcs)
print(app.view_functions)
print(app.url_map)
值分别为
app.config = Config()------------------键值对{key:value}
app.before_request_funcs = {None:[f1,f2]}
app.after_request_funcs = {None:[f3,f4]}
app.before_first_request_funcs = [f5,f6]
# 键值对{endpoint:view_func}
app.view_functions = {
'hello':hello,
'static':_PackageBoundObject.send_static_file
}
app.url_map = Map([<Rule '/hello' (GET, OPTIONS, HEAD) -> hello>,
<Rule '/static/<filename>' (GET, OPTIONS, HEAD) -> static>])
# app.url_map是一个Map类对象
# Rule对象中还封装了一些别的数据,我只挑了重要的写出来了
# 其中methods的OPTIONS从哪来的可以暂略
url的构成
URL 遵守一种标准的语法,它由协议、主机名、域名、端口、路径、以及文件名这六个部分构成,其中端口可以省略。具体语法规则如下:
scheme://host.domain:port/path/filename
在上述语法规则中,scheme 表示协议,host 表示主机名,domain 表示域名,port 表示端口(可以省略),path 表示文件的路径,filename 表示文件名称。接下来我们详细看一下这几部分到底是如何使用的。
参考:URL结构解析
1) 协议url_scheme
协议用来指明客户端和服务器之间通信的类型。我们经常用到的协议有四种:http、https、ftp 以及 file。这四种协议的使用场景如下表所示:
协议使用场景
协议 | 使用场景 |
---|---|
http(HyperText Transfer Protocol) | 超文本传输协议。http 协议可以将编码为超文本的数据从一台计算机传送到另一台计算机,不进行加密。 |
https(HyperText Transfer Protocol over SecureSocket Layer) | 安全超文本传输协议。以安全为目标的 http 通道,安全网页,加密所有信息交换。 |
ftp(File Transfer Protocol) | 文件传输协议。 |
file | 本机上的文件。 |
2) 主机名host
主机名可以向浏览器提供文件站点的名称。www 是我们常见的主机名,例如百度的网址 https://www.baidu.com/、淘宝的网址 https://www.taobao.com/ 使用的都是 www 的主机名。除此之外,还有很多网站使用的是其它主机名。例如C语言中文网的网址 http://c.biancheng.net/ 的主机名是c,网易云音乐的网址 https://music.163.com/ 的主机名是 music。
3) 域名damain
域名和主机名一起使用,被用来定义服务器的地址。Web 服务器遵守数字网际协议(Internet Protocol,IP),每一台连接到因特网的计算机都有一个固定的 IP 地址。域名即 IP 地址的别名,因为一般的 IP 地址都是长串的数字,为了方便记忆所以使用域名进行替代。简单来说,没有域名(IP)我们就不能上网。
例如www.taobao.com是淘宝网址的域名。
4) 端口port
端口用来定义主机上的端口号。如果不写,http 的默认端口号是 80,https 的默认端口号是 443,ftp 的默认端口号是 21。还是拿 C语言中文网举例说明,不论用户输入 http://c.biancheng.net/ 还是 http://c.biancheng.net/:80,浏览器都会解析为 C语言中文网的链接。
5) 路径path
路径指定服务器上文件的所在位置。就像我们自己在计算机上保存文件时所指定的文件夹一样,Web 服务器上的文件也有可能是存放在子目录(就是文件夹中的子文件夹)中的。如果是这样,路径中的相邻文件夹需要使用斜线(/)隔开。例如 http://c.biancheng.net/view/views/7410.html 这个网址,它的路径就是 /view/views。
6) 文件名filename
文件名用来定义文档或资源的名称。和路径类似,路径指的是文件夹,而它指的是文件夹中的文件。我们在《网站到底是什么》这篇文章中讲到过网页文件的后缀有很多种,比如.html
、.php
、 .jsp
、.asp
等。
协议需要与 URL 的其它部分用://
隔开。百度的网址后面的.com
以及 C语言中文网的.net
又称作域后缀(扩展名),用于表明该主机所在的域的类型。
2. app.run()
应用启动的代码是 app.run(host, port, debug)
,这个方法的代码如下:
def run(self, host=None, port=None, debug=None, **options):
from werkzeug.serving import run_simple
# 参数配置,若host和port未指定,设置 host 和 port 的默认值 127.0.0.1 和 5000
if host is None: # 若IP地址为空,IP地址是``127.0.0.1``
host = '127.0.0.1'
if port is None: # 若端口号为空,
server_name = self.config['SERVER_NAME']
if server_name and ':' in server_name:
port = int(server_name.rsplit(':', 1)[1])
else: # 端口号为5000
port = 5000
if debug is not None: # 若debug不为空,将其转化为布尔值,1为调试模式,0为非调试
self.debug = bool(debug)
# 其他参数
options.setdefault('use_reloader', self.debug)
options.setdefault('use_debugger', self.debug)
# 应用启动
try:
run_simple(host, port, self, **options)
finally:
self._got_first_request = False
- 默认情况下,IP地址是
127.0.0.1
,端口号为5000 - 当然,如果电脑连接了WIFI,可使用该WIFI的IP地址
host="0.0.0.0"
则flask程序可同时被127.0.0.1
和WIFI IP访问到。
方法app.run()
的内容就是,配置网络参数,调用werkzeug
的run_simple(host, port, app)
函数。这里简单了解一下该函数的功能:监听在指定的端口,收到 HTTP 请求的时候解析为 WSGI 格式,然后调用 app
去执行处理的逻辑。
从而app.run()
中的run_simple(host, port, self, **options)
即调用self()
,self
也就是我们创建的Flask
类对象app
,使用执行app()
,即app.__call__()
方法。
3. app.__call__
# Flask.__call__
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
在这里打一下断点,调试模式运行,访问。根据url的构成,我们知道
scheme://host.domain:port/path/filename
scheme = 'http'
domain = '127.0.0.1'
port = 5000
path = '/hello'
接下来看看environ
变量,详情参考wsgi的environ变量
4.app.wsgi_app
def wsgi_app(self, environ, start_response):
"""
2.1 创建RequestContext对象
内部封装了{app,request,session}
并进行了路由匹配self.match_request:通过请求信息返回endpoint和url参数
ctx.request.url_rule = url_rule # 请求url的endpoint
ctx.request.view_args = view_args # 请求url的参数【动态路由传参】
"""
ctx = self.request_context(environ)
"""
2.2 ctx.push()
2.2.1 创建AppContext对象app_ctx
2.2.2 将app_ctx放入应用上下文_app_ctx_stack中
2.2.3 将ctx放入请求上下文_request_cts_stack中
2.2.4 为session赋值
"""
ctx.push()
error = None
try:
try:
"""
try_trigger_before_first_request_functions:执行before_first_request函数
preprocess_request:执行before_request函数:
dispatch_request:执行视图函数【当before_request函数没有返回值时,才执行视图函数】
finalize_request:执行after_request函数,将session加密返回浏览器的cookie存储
"""
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
"""发出响应"""
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
"""销毁请求上下文"""
ctx.auto_pop(error)
下面根据app.wsgi_app
的代码逐一分析,
4.1 创建RequestContext对象ctx
ctx = self.request_context(environ)
查看app.request_context
函数
def request_context(self, environ):
return RequestContext(self, environ)
紧接着看RequestContext
类的初始化函数__init__
class RequestContext(object):
def __init__(self, app, environ, request=None):
# 将app封装于ctx.app
self.app = app
if request is None:
# Flask中定义了类属性request_class = Request
# 将原生请求environ封装为Request对象
request = app.request_class(environ)
# 将request封装到ctx.request
self.request = request
# 将request.environ相关信息与app.url_map一起封装进Mapadapter类
self.url_adapter = app.create_url_adapter(self.request)
# 创建空的session
self.session = None
# 获取请求的路由和视图函数并记录在ctx中
self.match_request()
4.1.1 封装请求数据request,session
# 将app封装于ctx.app
self.app = app
if request is None:
request = app.request_class(environ)
# 将request封装到ctx.request
self.request = request
# 将request.environ相关信息与app.url_map一起封装进Mapadapter类
self.url_adapter = app.create_url_adapter(self.request)
# ctx.session = None
self.session = None
这里主要讲一下对environ
的Request
封装:request = app.request_class(environ)
-
Flask
中定义了类属性request_class = Request
-
Request
类继承了RequestBase
类,且Request
类中没有定义初始化函数__init__
,故执行父类RequestBase
的初始化函数class Request(RequestBase): pass
-
RequestBase
类class Request( BaseRequest, AcceptMixin, ETagRequestMixin, UserAgentMixin, AuthorizationMixin, CommonRequestDescriptorsMixin, ): # 源码由C语言编译,无法查看,转而看看它的父类BaseRequest
-
BaseRequest
类的属性在图中可以看到其中的
args
,form
等属性,即flask程序获取请求数据时拿取的属性。
下面简单理解一下 create_url_adapter()
方法与 match_request()
方法。
4.1.2 MapAdapter对象【封装请求和路由】
ctx.url_adapter=create_url_adapter()
内部的代码执行顺序如下:
ctx.url_adapter = app.create_url_adapter(ctx.request)
-> # app.create_url_adapter
return app.url_map.bind_to_environ()
-> # app.url_map.bind_to_environ()
return app.url_map.bind()
-> # app.url_map.bind()
return MapAdapter()
-
app.url_map.bind_to_environ()
:总的来说,该函数就是获取request.environ
中的各类参数【即请求信息,服务器名,HTTP方法,请求url的path,请求参数等】,将其作为参数,传入app.url_map.bind()
函数""" app.url_map.bind_to_environ(request.environ,server_name=self.config['SERVER_NAME']) self.config['SERVER_NAME']=None 所以server_name = None """ def bind_to_environ(self, environ, server_name=None, subdomain=None): # environ = ctx.request.environ, # server_name = None # subdomain = None # _get_environ(obj):返回obj的environ属性,若该属性不存在,返回obj本身 environ = _get_environ(environ) """Note that because of limitations in the protocol there is no way to get the current subdomain and real `server_name` from the environment. If you don't provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or `HTTP_HOST` if provided) as used `server_name` with disabled subdomain feature. """ wsgi_server_name = get_host(environ).lower() # 请求的主机 = '127.0.0.1' # 未指定服务器名,则使用wsgi提供的服务器名 if server_name is None: server_name = wsgi_server_name else: server_name = server_name.lower() # 如果子域名为空,计算子域名 # server_name` is ``'example.com'`` and the `SERVER_NAME` # in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated # subdomain will be ``'staging.dev'`` if subdomain is None and not self.host_matching: cur_server_name = wsgi_server_name.split(".") real_server_name = server_name.split(".") offset = -len(real_server_name) if cur_server_name[offset:] != real_server_name: subdomain = "<invalid>" else: subdomain = ".".join(filter(None, cur_server_name[:offset])) def _get_wsgi_string(name): val = environ.get(name) if val is not None: return wsgi_decoding_dance(val, self.charset) script_name = _get_wsgi_string("SCRIPT_NAME") # url的path的开始部分 path_info = _get_wsgi_string("PATH_INFO") # url的path的剩余部分 query_args = _get_wsgi_string("QUERY_STRING") # 请求参数 return Map.bind( self, server_name, # 服务器名 ='127.0.0.1' script_name, # ='' subdomain, # 子域名 ='' environ["wsgi.url_scheme"],# url协议 ='http' environ["REQUEST_METHOD"], # 请求方法 path_info, # url的path = '/hello' query_args=query_args, # 请求参数None )
其中的
_get_host
函数: -
app.url_map.bind()
函数对传入的请求信息稍加处理,封装进
MapAdapter
对象# Map.bind() def bind( self, server_name, script_name=None, subdomain=None, url_scheme="http", default_method="GET", path_info=None, query_args=None, ): # 对请求信息做一定的处理 server_name = server_name.lower() if self.host_matching: if subdomain is not None: raise RuntimeError("host matching enabled and a subdomain was provided") elif subdomain is None: subdomain = self.default_subdomain if script_name is None: script_name = "/" if path_info is None: path_info = "/" try: server_name = _encode_idna(server_name) except UnicodeError: raise BadHost() # 创建MapAdatper对象 return MapAdapter( self, server_name, # '127.0.0.1' script_name, # '/' subdomain, # '' url_scheme, # 'http' path_info, # '/hello' default_method, # "GET" query_args, # '' ) # url = url_scheme + server_name + path_info
-
并且创建的
MapAdapter
对象中还封装了app.url_map
,即class MapAdapter(object): def __init__( self, map, # app.url_map server_name, # 域名 script_name, # '/' subdomain, # 子域名 = '' url_scheme, # url协议 = 'http' path_info, # '/hello' default_method, query_args=None, ): self.map = map #路由【url与endpoint的对应关系】 # 请求的相关信息 self.server_name = to_unicode(server_name) script_name = to_unicode(script_name) if not script_name.endswith(u"/"): script_name += u"/" self.script_name = script_name self.subdomain = to_unicode(subdomain) self.url_scheme = to_unicode(url_scheme) self.path_info = to_unicode(path_info) self.default_method = to_unicode(default_method) self.query_args = query_args
总的来说,就是创建了MapAdapter
对象,其内部封装了请求的相关信息【请求url,请求参数HTTP方法…】与app.url_map
【Rule1,Rule2,…】。
4.1.3 路由匹配
match_request()
内部代码的执行顺序如下:
ctx.match_request()->ctx.url_adapter.match()->Rule.match()
-
ctx.url_adapter.match()
:通过
ctx.url_adapter
内部封装的请求信息:path_info
(请求url路径)和method
(HTTP方法),从
ctx.url_adapter.map._rules
中找到匹配的rule
,从而返回rule
的endpoint
和一些参数rv
,将其封装进ctx.request.url_rule
和ctx.request.view_args
。# return_rule为True,所以返回Rule对象 url_rule, view_args =self.url_adapter.match(return_rule=True) self.request.url_rule = url_rule # 请求url对应的Rule对象 = Rule('/hello',methods=['GET', 'OPTIONS', 'HEAD'), endpoint = 'hello') self.request.view_args = view_args # 请求url的参数【动态路由传参】 = None
-
Rule.match()
:检查Rule
对象是否符合path_info
(url路径)和method
(HTTP方法),如若符合,返回请求url的参数
总的来说,就是ctx.url_adapter
中封装了请求信息【url路径等】和路由信息【app.url_map】,根据请求信息去路由信息中找到其对应的路由rule
,将rule
和一些参数rv
,将其封装进ctx.request.url_rule
和ctx.request.view_args
。
-
举例理解:
create_url_adapter()
方法与match_request()
方法。>>> m = Map([ ... Rule('/', endpoint='index'), ... Rule('/downloads/', endpoint='downloads/index'), ... Rule('/downloads/<int:id>', endpoint='downloads/show') ... ]) >>> urls = m.bind("example.com", "/") >>> urls.match("/", "GET") ('index', {}) >>> urls.match("/downloads/42") ('downloads/show', {'id': 42}) And here is what happens on redirect and missing URLs: >>> urls.match("/downloads") Traceback (most recent call last): ... RequestRedirect: http://example.com/downloads/ >>> urls.match("/missing") Traceback (most recent call last): ... NotFound: 404 Not Found
4.1.4 总结
"""
2.1 创建RequestContext对象
内部封装了{app,request,session}
并进行了路由匹配self.match_request:通过请求信息返回Rule对象 和url参数
ctx.request.url_rule = url_rule # 请求url对应的Rule对象
ctx.request.view_args = view_args # 请求url的参数【动态路由传参】
"""
4.2 RequestContext入栈
ctx.push()
查看RequestContext.Push
方法:
class RequestContext(object):
def push(self):
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
# 创建AppContext对象
app_ctx = self.app.app_context()
# 将应用上下文app_ctx放入_app_ctx_stack【LocalStack】栈中
app_ctx.push()
else:
...
# 将请求上下文放入_request_ctx_stack【LocalStack】栈中
_request_ctx_stack.push(self)
# session赋值
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
创建AppContext对象
app_ctx = self.app.app_context()
看看app.app_context()
方法
def app_context(self):
return AppContext(self)
紧接着AppContext
类
class AppContext(object):
def __init__(self, app):
self.app = app # app_ctx.app = app
self.url_adapter = app.create_url_adapter(None) # app_ctx = None
self.g = app.app_ctx_globals_class() # app_ctx.g = _AppCtxGlobals
可以看到内部封装了app
和g
。下面来看看g
Flask中定义了类属性
app_ctx_globals_class = _AppCtxGlobals
看看类_AppCtxGlobals
,其主要定义了取值,设值,删值的方法。不难发现,其实就是一次请求的全局变量。
class _AppCtxGlobals(object):
"""A plain object."""
# 取值
def get(self, name, default=None):
return self.__dict__.get(name, default)
# 删值
def pop(self, name, default=_sentinel):
if default is _sentinel:
return self.__dict__.pop(name)
else:
return self.__dict__.pop(name, default)
# 设值
def setdefault(self, name, default=None):
return self.__dict__.setdefault(name, default)
AppContext对象入栈
app_ctx.push()
查看AppContext.push
方法
class AppContext(object):
def push(self):
# 将app上下文放入Local
_app_ctx_stack.push(self)
RequestContext入栈
# 将请求上下文放入_request_ctx_stack【LocalStack】栈中
_request_ctx_stack.push(self)
session赋值
# 将请求数据传入session
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
4.3 路由分发
"""
执行:
before_first_request函数
before_request函数
视图函数
after_request函数
"""
response = self.full_dispatch_request()
看看app.full_dispatch_request
是何方神圣?
def full_dispatch_request(self):
"""执行before_first_request函数,只有第一次请求前会执行"""
self.try_trigger_before_first_request_functions()
try:
# 信号拓展,略
request_started.send(self)
# 执行before_request函数
rv = self.preprocess_request()
if rv is None:
# 当before_request返回值为空,执行视图函数,rv是视图函数的返回值
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
# 执行after_request函数
return self.finalize_request(rv)
before_first_request
def try_trigger_before_first_request_functions(self):
"""Flask._got_first_request默认为Flase,即尚未获取第一次请求"""
if self._got_first_request:
return
with self._before_request_lock:
# 如果已经获取过第一次请求,则执行before_first_request函数
if self._got_first_request:
return
# 否则如果是第一次请求,获取before_first_request列表中的函数逐一执行
for func in self.before_first_request_funcs:
func()
# 并将标识设为true
self._got_first_request = True
before_request函数
def preprocess_request(self):
...
"""before_request_func:{None:[f1,f2]}"""
funcs = self.before_request_funcs.get(None, ())
"""逐一执行before_request函数"""
for func in funcs:
rv = func()
"""如果某一函数有返回值,则直接跳转执行after_request函数"""
if rv is not None:
return rv
视图函数
def dispatch_request(self):
"""
还记得吗,
ctx.request.url_rule中存放了请求url对应的endpoint,
ctx.request.view_args中存放了动态url参数
"""
# 获取当前请求的request对象
req = _request_ctx_stack.top.request
# 获取请求url对应的Rule
rule = req.url_rule
、 # 获取url对应的endpoint并依此找到其视图函数,传入请求参数执行
return self.view_functions[rule.endpoint](**req.view_args)
after_request函数
def finalize_request(self, rv, from_error_handler=False):
"""将视图函数的返回值rv封装为response对象"""
response = self.make_response(rv)
try:
# 执行after_request函数
response = self.process_response(response)
request_finished.send(self, response=response)
except ...
return response
看看是如何执行after_request
函数的
def process_response(self, response):
# 获取当前请求上下文
ctx = _request_ctx_stack.top
bp = ctx.request.blueprint
funcs = ctx._after_request_functions
"""app.after_request_functions:{None:[f3,f4]}"""
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:
"""将after_request_functions[None]翻转再逐一执行"""
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
for handler in funcs:
response = handler(response)
"""将一系列session数据进行加密,返回浏览器cookie中"""
if not self.session_interface.is_null_session(ctx.session):
self.save_session(ctx.session, response)
return response
4.4 响应
return response(environ, start_response)
4.5 销毁请求上下文
finally:
ctx.auto_pop(error)
Request.autp_pop()
方法
def auto_pop(self, exc):
...
self.pop(exc)
Request.pop()
方法
def pop(self, exc=_sentinel):
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)