Flask源码学习【个人自学记录】-应用启动源码解读

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()的内容就是,配置网络参数,调用werkzeugrun_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

这里主要讲一下对environRequest封装: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,从而返回ruleendpoint和一些参数rv,将其封装进ctx.request.url_rulectx.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_rulectx.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

可以看到内部封装了appg。下面来看看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)

5. 总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值