黑马程序员技术交流社区

标题: 【上海校区】一个Flask应用运行过程剖析 [打印本页]

作者: Mr.TR    时间: 2018-8-29 14:54
标题: 【上海校区】一个Flask应用运行过程剖析
本帖最后由 Mr.TR 于 2018-8-29 15:06 编辑

相信很多初学Flask的同学(包括我自己),在阅读官方文档或者Flask的学习资料时,对于它的认识是从以下的一段代码开始的:
[Python] 纯文本查看 复制代码
from flask import Flask

app = Flask(__name__)

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

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

运行如上代码,在浏览器中访问http://localhost:5000/,便可以看到Hello World!出现了。这是一个很简单的Flask的应用。
然而,这段代码怎么运行起来的呢?一个Flask应用运转的背后又有哪些逻辑呢?如果你只关心Web应用,那对这些问题不关注也可以,但从整个Web编程的角度来看,这些问题非常有意义。本文就主要针对一个Flask应用的运行过程进行简要分析,后续文章还会对Flask框架的一些具体问题进行分析。
为了分析方便,本文采用 Flask 0.1版本 的源码进行相关问题的探索。

一些准备知识
在正式分析Flask之前,有一些准备知识需要先了解一下:
本文暂时不对服务器或网关的具体内容进行介绍,只需对服务器、网关、Web应用之间有怎样的关系,以及它们之间如何调用有一个了解即可。

一个Flask应用运行的过程
1. 实例化一个Flask应用
使用app = Flask(__name__),可以实例化一个Flask应用。实例化的Flask应用有一些要点或特性需要注意一下:
2.调用Flask应用时会发生什么
上面部分分析了实例化的Flask应用长什么样子。当一个完整的Flask应用实例化后,可以通过调用app.run()方法运行这个应用。
Flask应用的run()方法会调用werkzeug.serving模块中的run_simple方法。这个方法会创建一个本地的测试服务器,并且在这个服务器中运行Flask应用。关于服务器的创建这里不做说明,可以查看werkzeug.serving模块的有关文档。
当服务器开始调用Flask应用后,便会触发Flask应用的__call__(environ, start_response)方法。其中environ由服务器产生,start_response在服务器中定义。
上面我们分析到当Flask应用被调用时会执行wsgi_app(environ, start_response)方法。可以看出,wsgi_app是真正被调用的WSGI应用,之所以这样设计,就是为了在应用正式处理请求之前,wsgi_app可以被一些“中间件”装饰,以便先行处理一些操作。为了便于理解,这里先举两个例子进行说明。
例子一: 中间件SharedDataMiddleware
中间件SharedDataMiddleware是werkzeug.wsgi模块中的一个类。该类可以为Web应用提供静态内容的支持。例如:
[Python] 纯文本查看 复制代码
import os
from werkzeug.wsgi import SharedDataMiddleware

app = SharedDataMiddleware(app, {
    '/shared': os.path.join(os.path.dirname(__file__), 'shared')
})

Flask应用通过以上的代码,app便会成为一个SharedDataMiddleware实例,之后便可以在http://example.com/shared/中访问shared文件夹下的内容。
对于中间件SharedDataMiddleware,Flask应用在初始实例化的时候便有所应用。其中有这样一段代码:
[Python] 纯文本查看 复制代码
self.wsgi_app = SharedDataMiddleware(self.wsgi_app, {
                self.static_path: target
            })

这段代码显然会将wsgi_app变成一个SharedDataMiddleware对象,这个对象为Flask应用提供一个静态文件夹/static。这样,当整个Flask应用被调用时,self.wsgi_app(environ, start_response)会执行。由于此时self.wsgi_app是一个SharedDataMiddleware对象,所以会先触发SharedDataMiddleware对象的__call__(environ, start_response)方法。如果此时的请示是要访问/static这个文件夹,SharedDataMiddleware对象会直接返回响应;如果不是,则才会调用Flask应用的wsgi_app(environ.start_response)方法继续处理请求。
例子二: 中间件DispatcherMiddleware
中间件DispatcherMiddleware也是werkzeug.wsgi模块中的一个类。这个类可以讲不同的应用“合并”起来。以下是一个使用中间件DispatcherMiddleware的例子。
[Python] 纯文本查看 复制代码
from flask import Flask
from werkzeug import DispatcherMiddleware

app1 = Flask(__name__)
app2 = Flask(__name__)
app = Flask(__name__)

@app1.route('/')
def index():
    return "This is app1!"

@app2.route('/')
def index():
    return "This is app2!"

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

app = DispatcherMiddleware(app, {
            '/app1':        app1,
            '/app2':        app2
        })

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 5000, app)

在上面的例子中,我们首先创建了三个不同的Flask应用,并为每个应用创建了一个视图函数。但是,我们使用了DispatcherMiddleware,将app1、app2和app合并起来。这样,此时的app便成为一个DispatcherMiddleware对象。
当在服务器中调用app时,由于它是一个DispatcherMiddleware对象,所以首先会触发它的__call__(environ, start_response)方法。然后根据请求URL中的信息来确定要调用哪个应用。例如:
3. 和请求处理相关的上下文对象
当Flask应用真正处理请求时,wsgi_app(environ, start_response)被调用。这个函数是按照下面的方式运行的:
[Python] 纯文本查看 复制代码
def wsgi_app(environ, start_response):
    with self.request_context(environ):
        ...

请求上下文
可以看到,当Flask应用处理一个请求时,会构造一个上下文对象。所有的请求处理过程,都会在这个上下文对象中进行。这个上下文对象是_RequestContext类的实例。
[Python] 纯文本查看 复制代码
# Flask v0.1
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):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None

    def __enter__(self):
        _request_ctx_stack.push(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.
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

根据_RequestContext上下文对象的定义,可以发现,在构造这个对象的时候添加了和Flask应用相关的一些属性:
LocalStack和一些“全局变量”
注意: 当进入这个上下文对象时,会触发_request_ctx_stack.push(self)。在这里需要注意Flask中使用了werkzeug库中定义的一种数据结构LocalStack。
[Python] 纯文本查看 复制代码
_request_ctx_stack = LocalStack()

LocalStack是一种栈结构,每当处理一个请求时,请求上下文对象_RequestContext会被放入这个栈结构中。数据在栈中存储的形式表现成如下:
[Python] 纯文本查看 复制代码
{880: {'stack': [<flask._RequestContext object>]}, 13232: {'stack': [<flask._RequestContext object>]}}

这是一个字典形式的结构,键代表当前线程/协程的标识数值,值代表当前线程/协程存储的变量。werkzeug.local模块构造的这种结构,很容易实现线程/协程的分离。也正是这种特性,使得可以在Flask中访问以下的“全局变量”:
[Python] 纯文本查看 复制代码
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)

其中_request_ctx_stack.top始终指向当前线程/协程中存储的“请求上下文”,这样像app、request、session、g等都可以以“全局”的形式存在。这里“全局”是指在当前线程或协程当中。
由此可以看出,当处理请求时:
4. 在上下文环境中处理请求
处理请求的过程定义在wsgi_app方法中,具体如下:
[Python] 纯文本查看 复制代码
def wsgi_app(environ, start_response):
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

从代码可以看出,在上下文对象中处理请求的过程分为以下几个步骤:


(文章来自网络)


作者: 不二晨    时间: 2018-8-30 17:07
奈斯,加油加油




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2