flask 源碼解析

本文簡單的分析了 Flask 的源碼,主要關注 WSGI、Flask 對象的數據結構、Flask 應用啓動過程、請求處理過程、視圖函數、URL 的映射、應用上下文和請求上下文。講解這些主題時也不會面面俱到,請按照你閱讀源碼的須要自行探索。要讀懂本文,你須要較爲熟悉 Flask,好比已經用 Flask 寫過一個小項目,而且有必定的閱讀代碼的能力,並對 web 框架的功能有基本瞭解。python

本文會不時更新,最近更新日期:2017年9月10日。web

這是 Flask 官方欽定的 Demo 代碼:正則表達式

from flask import Flask
app = Flask(__name__)

@app.route(‘/‘)
def index():
    return ‘Hello, world!’

if __name__ == ‘__main__’:
    app.run()
複製代碼

這篇文章從這個簡單的代碼開始,簡要介紹了 WSGI、Flask 對象的數據結構、Flask 應用啓動過程、請求處理過程、視圖函數、URL 的映射、request 和 response 類(應用上下文和請求上下文),這些主題涵蓋了一個 web 框架的核心。shell

WSGI

在用戶發起的請求到達服務器以後,會被一個 HTTP 服務器所接收,而後交給 web 應用程序作業務處理,這樣 HTTP 服務器和 web 應用之間就須要一個接口,在 Python web 開發的世界裏,Python 官方欽定了這個接口並命名爲 WSGI,由 PEP333 所規定。只要服務器和框架都遵照這個約定,那麼就能實現服務器和框架的任意組合。按照這個規定,一個面向 WSGI 的框架必需要實現這個方法:數據庫

def application(environ, start_response)
複製代碼

在工做過程當中,HTTP 服務器會調用上面這個方法,傳入請求信息,即名爲 environ 的字典和 start_response 函數,應用從 environ 中獲取請求信息,在進行業務處理後調用 start_response 設置響應頭,並返回響應體(必須是一個可遍歷的對象,好比列表、字典)給 HTTP 服務器,HTTP 服務器再返回響應給用戶。express

因此 Flask 做爲一個開發 web 應用的 web 框架,負責解決的問題就是:flask

  1. 做爲一個應用,可以被 HTTP 服務器所調用,必需要有 __call__ 方法
  2. 經過傳入的請求信息(URL、HTTP 方法等),找到正確的業務處理邏輯,即正確的視圖函數
  3. 處理業務邏輯,這些邏輯可能包括表單檢查、數據庫 CRUD 等(這個在這篇文章裏不會涉及)
  4. 返回正確的響應
  5. 在同時處理多個請求時,還須要保護這些請求,知道應該用哪一個響應去匹配哪一個請求,即線程保護

下面就來看看 Flask 是如何解決這些問題的。數組

參考閱讀:一塊兒寫一個 web 服務器,該系列文章可以讓你基本理解 web 服務器和框架是如何經過 WSGI 協同工做的。安全

應用的建立

源碼閱讀:app.pyFlask 類的代碼。服務器

Demo 代碼的第二行建立了一個 Flask 類的實例,傳入的參數是當前模塊的名字。咱們先來看看 Flask 應用究竟是什麼,它的數據結構是怎樣的。

Flask 是這樣一個類:

The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for the view functions, the URL rules, template configuration and much more.

The name of the package is used to resolve resources from inside the package or the folder the module is contained in depending on if the package parameter resolves to an actual python package (a folder with an __init__.py file inside) or a standard module (just a .py file).

一個 Flask 對象其實是一個 WSGI 應用。它接收一個模塊或包的名字做爲參數。它被建立以後,全部的視圖函數、URL 規則、模板設置等都會被註冊到它上面。之因此要傳入模塊或包的名字,是爲了定位一些資源。

Flask 類有這樣一些屬性:

  • request_class = Request 設置請求的類型
  • response_class = Response 設置響應的類型

這兩個類型都來源於它的依賴庫 werkzeug 並作了簡單的拓展。

Flask 對象的 __init__ 方法以下:

def __init__(self, package_name):
    #: Flask 對象有這樣一個字典來保存全部的視圖函數
    self.view_functions = {}

    #: 這個字典用來保存全部的錯誤處理視圖函數
    #: 字典的 key 是錯誤類型碼
    self.error_handlers = {}

    #: 這個列表用來保存在請求被分派以前應當執行的函數
    self.before_request_funcs = []

    #: 在接收到第一個請求的時候應當執行的函數
    self.before_first_request_funcs = []

    #: 這個列表中的函數在請求完成以後被調用,響應對象會被傳給這些函數
    self.after_request_funcs = []

    #: 這裏設置了一個 url_map 屬性,並把它設置爲一個 Map 對象
    self.url_map = Map()
複製代碼

到這裏一個 Flask 對象建立完畢並被變量 app 所指向,其實它就是一個保存了一些配置信息,綁定了一些視圖函數而且有個 URL 映射對象(url_map)的對象。但咱們還不知道這個 Map 對象是什麼,有什麼做用,從名字上看,彷佛其做用是映射 URL 到視圖函數。源代碼第 21 行有 from werkzeug.routing import Map, Rule,那咱們就來看看 werkzeug 這個庫中對 Map 的定義:

The map class stores all the URL rules and some configuration parameters. Some of the configuration values are only stored on the Map instance since those affect all rules, others are just defaults and can be overridden for each rule. Note that you have to specify all arguments besides the rules as keyword arguments!

能夠看到這個類的對象儲存了全部的 URL 規則和一些配置信息。因爲 werkzeug 的映射機制比較複雜,咱們下文中講到映射機制的時候再深刻了解,如今只要記住 Flask 應用(即一個 Flask 類的實例)存儲了視圖函數,並經過 url_map 這個變量存儲了一個 URL 映射機構就能夠了。

應用啓動過程

源碼閱讀:app.pyFlask 類的代碼和 werkzeug.serving 的代碼,特別注意 run_simple BaseWSGIServer WSGIRequestHandler

Demo 代碼的第 6 行是一個限制,表示若是 Python 解釋器是直接運行該文件或包的,則運行 Flask 程序:在 Python 中,若是直接執行一個模塊或包,那麼解釋器就會把當前模塊或包的 __name__ 設置爲爲 __main_

第 7 行中的 run 方法啓動了 Flask 應用:

def run(self, host=None, port=None, debug=None, **options):
    from werkzeug.serving import run_simple
    if host is None:
        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:
            port = 5000
    if debug is not None:
        self.debug = bool(debug)
    options.setdefault('use_reloader', self.debug)
    options.setdefault('use_debugger', self.debug)
    try:
        run_simple(host, port, self, **options)
    finally:
        # reset the first request information if the development server
        # reset normally. This makes it possible to restart the server
        # without reloader and that stuff from an interactive shell.
        self._got_first_request = False
複製代碼

能夠看到這個方法基本上是在配置參數,實際上啓動服務器的是 werkzeugrun_simple 方法,該方法在默認狀況下啓動了服務器 BaseWSGIServer,繼承自 Python 標準庫中的 HTTPServer.TCPServer。注意在調用 run_simple 時,Flask 對象把本身 self 做爲參數傳進去了,這是正確的,由於服務器在收到請求的時候,必需要知道應該去調用誰的 __call__ 方法。

按照標準庫中 HTTPServer.TCPServer 的模式,服務器必須有一個類來做爲 request handler 來處理收到的請求,而不是由 HTTPServer.TCPServer 自己的實例來處理,werkzeug 提供了 WSGIRequestHandler 類來做爲 request handler,這個類在被 BaseWSGIServer 調用時,會執行這個函數:

def execute(app):
    application_iter = app(environ, start_response)
    try:
        for data in application_iter:
            write(data)
        if not headers_sent:
            write(b'')
    finally:
        if hasattr(application_iter, 'close'):
            application_iter.close()
        application_iter = None
複製代碼

函數的第一行就是按照 WSGI 要求的,調用了 app 並把 environstart_response 傳入。咱們再看看 flask 中是如何按照 WSGI 要求對服務器的調用進行呼應的。

def __call__(self, environ, start_response):
    return self.wsgi_app(environ, start_response)

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            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)
複製代碼

能夠看到 Flask 按照 WSGI 的要求實現了 __call__ 方法,所以成爲了一個可調用的對象。但它不是在直接在 __call__ 裏寫邏輯的,而是調用了 wsgi_app 方法,這是爲了中間件的考慮,不展開談了。這個方法返回的 response(environ, start_response) 中,responsewerkzueg.response 類的一個實例,它也是個能夠調用的對象,這個對象會負責生成最終的可遍歷的響應體,並調用 start_response 造成響應頭。

請求處理過程

源碼閱讀:app.Flask 的代碼。

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            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)
複製代碼

wsgi_app 方法中裏面的內容就是對請求處理過程的一個高度抽象。

首先,在接收到服務器傳過來的請求時,Flask 調用 request_context 函數創建了一個 RequestContext 請求上下文對象,並把它壓入 _request_ctx_stack 棧。關於上下文和棧的內容下文會再講到,你如今須要知道的是,這些操做是爲了 flask 在處理多個請求的時候不會混淆。以後,Flask 會調用 full_dispatch_request 方法對這個請求進行分發,開始實際的請求處理過程,這個過程當中會生成一個響應對象並最終經過調用 response 對象來返回給服務器。若是當中出錯,就聲稱相應的錯誤信息。不論是否出錯,最終 Flask 都會把請求上下文推出棧。

full_dispatch_request 是請求分發的入口,咱們再來看它的實現:

def full_dispatch_request(self):
    self.try_trigger_before_first_request_functions()
    try:
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)
複製代碼

首先調用 try_trigger_before_first_request_functions 方法來嘗試調用 before_first_request 列表中的函數,若是 Flask_got_first_request 屬性爲 Falsebefore_first_request 中的函數就會被執行,執行一次以後,_got_first_request 就會被設置爲 True 從而再也不執行這些函數。

而後調用 preprocess_request 方法,這個方法調用 before_request_funcs 列表中全部的方法,若是這些 before_request_funcs 方法中返回了某種東西,那麼就不會真的去分發這個請求。好比說,一個 before_request_funcs 方法是用來檢測用戶是否登陸的,若是用戶沒有登陸,那麼這個方法就會調用 abort 方法從而返回一個錯誤,Flask 就不會分發這個請求而是直接報 401 錯誤。

若是 before_request_funcs 中的函數沒有返回,那麼再調用 dispatch_request 方法進行請求分發。這個方法首先會查看 URL 規則中有沒有相應的 endpointvalue 值,若是有,那麼就調用 view_functions 中相應的視圖函數(endpoint 做爲鍵值)並把參數值傳入(**req.view_args),若是沒有就由 raise_routing_exception 進行處理。視圖函數的返回值或者錯誤處理視圖函數的返回值會返回給 wsgi_app 方法中的 rv 變量。

def dispatch_request(self):
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        if getattr(rule, 'provide_automatic_options', False) \
           and req.method == 'OPTIONS':
            return self.make_default_options_response()
        return self.view_functions[rule.endpoint](**req.view_args)

def finalize_request(self, rv, from_error_handler=False):
    response = self.make_response(rv)
    try:
        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

def make_response(self, rv):
    if isinstance(rv, self.response_class):
        return rv
    if isinstance(rv, basestring):
        return self.response_class(rv)
    if isinstance(rv, tuple):
        return self.response_class(*rv)
    return self.response_class.force_type(rv, request.environ)
複製代碼

而後 Flask 就會根據 rv 生成響應,這個 make_response 方法會查看 rv 是不是要求的返回值類型,不然生成正確的返回類型。好比 Demo 中返回值是字符串,就會知足 isinstance(rv, basestring) 判斷並從字符串生成響應。這一步完成以後,Flask 查看是否有後處理視圖函數須要執行(在 process_response 方法中),並最終返回一個徹底處理好的 response 對象。

視圖函數註冊

在請求處理過程一節中,咱們已經看到了 Flask 是如何調用試圖函數的,這一節咱們要關注 Flask 如何構建和請求分派相關的數據結構。咱們將主要關注 view_functions,由於其餘的數據結構如 before_request_funcs 的構建過程大同小異,甚至更爲簡單。咱們也將仔細講解在應用的建立一節中遺留的問題,即 url_map 究竟是什麼。

Demo 代碼的第 4 行用修飾器 route 註冊一個視圖函數,這是 Flask 中受到普遍稱讚的一個設計。在 Flask 類的 route 方法中,能夠看到它調用了 add_url_rule 方法。

def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

def add_url_rule(self, rule, endpoint, **options):
    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)
    options['endpoint'] = endpoint
    methods = options.pop('methods', None)
    if methods is None:
        methods = getattr(view_func, 'methods', None) or ('GET',)
    if isinstance(methods, string_types):
        raise TypeError('Allowed methods have to be iterables of strings, '
                        'for example: @app.route(..., methods=["POST"])')
    methods = set(item.upper() for item in methods)

    required_methods = set(getattr(view_func, 'required_methods', ()))

    provide_automatic_options = getattr(view_func,
        'provide_automatic_options', None)

    if provide_automatic_options is None:
        if 'OPTIONS' not in methods:
            provide_automatic_options = True
            required_methods.add('OPTIONS')
        else:
            provide_automatic_options = False

    methods |= required_methods

    rule = self.url_rule_class(rule, methods=methods, **options)
    rule.provide_automatic_options = provide_automatic_options

    self.url_map.add(rule)
    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError('View function mapping is overwriting an '
                                 'existing endpoint function: %s' % endpoint)
        self.view_functions[endpoint] = view_func
複製代碼

這個方法負責註冊視圖函數,並實現 URL 到視圖函數的映射。首先,它要準備好一個視圖函數所支持的 HTTP 方法(基本上一半多的代碼都是在作這個),而後經過 url_rule_class 建立一個 rule 對象,並把這個對象添加到本身的 url_map 裏。咱們那個遺留問題在這裏就獲得解答了:rule 對象是一個保存合法的(Flask 應用所支持的) URL、方法、endpoint(在 **options 中) 及它們的對應關係的數據結構,而 url_map 是保存這些對象的集合。而後,這個方法將視圖函數添加到 view_functions 當中,endpoint 做爲它的鍵,其值默認是函數名。

咱們再來深刻了解一下 rule ,它被定義在 werkzeug.routing.Rule 中:

A Rule represents one URL pattern. There are some options for Rule that change the way it behaves and are passed to the Rule constructor. 一個 Rule 對象表明了一種 URL 模式,能夠經過傳入參數來改變它的許多行爲。

Rule 的 __init__ 方法爲:

def __init__(self, string, defaults=None, subdomain=None, methods=None, build_only=False, endpoint=None, strict_slashes=None, redirect_to=None, alias=False, host=None):
    if not string.startswith('/'):
        raise ValueError('urls must start with a leading slash')
    self.rule = string
    self.is_leaf = not string.endswith('/')

    self.map = None
    self.strict_slashes = strict_slashes
    self.subdomain = subdomain
    self.host = host
    self.defaults = defaults
    self.build_only = build_only
    self.alias = alias
    if methods is None:
        self.methods = None
    else:
        if isinstance(methods, str):
            raise TypeError('param `methods` should be `Iterable[str]`, not `str`')
        self.methods = set([x.upper() for x in methods])
        if 'HEAD' not in self.methods and 'GET' in self.methods:
            self.methods.add('HEAD')
    self.endpoint = endpoint
    self.redirect_to = redirect_to

    if defaults:
        self.arguments = set(map(str, defaults))
    else:
        self.arguments = set()
    self._trace = self._converters = self._regex = self._weights = None
複製代碼

一個 Rule 被建立後,經過 Mapadd 方法被綁定到 Map 對象上,咱們以前說過 flask.url_map 就是一個 Map 對象。

def add(self, rulefactory):
    for rule in rulefactory.get_rules(self):
        rule.bind(self)
        self._rules.append(rule)
        self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
    self._remap = True
複製代碼

Rulebind 方法的內容,就是添加 Rule 對應的 Map,而後調用 compile 方法生成一個正則表達式,compile 方法比較複雜,就不展開了。

def bind(self, map, rebind=False):
    """Bind the url to a map and create a regular expression based on the information from the rule itself and the defaults from the map. :internal: """
    if self.map is not None and not rebind:
        raise RuntimeError('url rule %r already bound to map %r' %
                           (self, self.map))
    self.map = map
    if self.strict_slashes is None:
        self.strict_slashes = map.strict_slashes
    if self.subdomain is None:
        self.subdomain = map.default_subdomain
    self.compile()
複製代碼

在 Flask 應用收到請求時,這些被綁定到 url_map 上的 Rule 會被查看,來找到它們對應的視圖函數。這是在請求上下文中實現的,咱們先前在 dispatch_request 方法中就見過——咱們是從 _request_ctx_stack.top.request 獲得 rule 並從這個 rule 找到 endpoint,最終找到用來處理該請求的正確的視圖函數的。因此,接下來咱們須要看請求上下的具體實現,而且看一看 Flask 是如何從 url_map 中找到這個 rule 的。

def dispatch_request(self):
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    if getattr(rule, 'provide_automatic_options', False) \
       and req.method == 'OPTIONS':
        return self.make_default_options_response()
    return self.view_functions[rule.endpoint](**req.view_args)
複製代碼

請求上下文

源碼閱讀:ctx.RequestContext 的代碼。

請求上下文是如何、在什麼時候被建立的呢?咱們先前也見過,在服務器調用應用的時候,Flask 的 wsgi_app 中有這樣的語句,就是建立了請求上下文並壓棧。

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
複製代碼

request_context 方法很是簡單,就是建立了 RequestContext 類的一個實例,這個類被定義在 flask.ctx 文件中,它包含了一系列關於請求的信息,最重要的是它自身的 request 屬性指向了一個 Request 類的實例,這個類繼承自 werkzeug.Request,在 RequestContext 的建立過程當中,它會根據傳入的 environ 建立一個 werkzeug.Request 的實例。

接着 RequestContextpush 方法被調用,這個方法將本身推到 _request_ctx_stack 的棧頂。

_request_ctx_stack 被定義在 flask.global 文件中,它是一個 LocalStack 類的實例,是 werkzeug.local 所實現的,若是你對 Python 的 threading 熟悉的話,就會發現這裏實現了線程隔離,就是說,在 Python 解釋器運行到 _request_ctx_stack 相關代碼的時候,解釋器會根據當前進程來選擇正確的實例。

可是,在整個分析 Flask 源碼的過程當中,咱們也沒發現 Flask 在被調用以後建立過線程啊,那麼爲何要作線程隔離呢?看咱們開頭提到的 run 函數,其實它能夠傳一個 threaded 參數。當不傳這個函數的時候,咱們啓動的是 BasicWSGIServer,這個服務器是單線程單進程的,Flask 的線程安全天然沒有意義,可是當咱們傳入這個參數的時候,咱們啓動的是 ThreadedWSGIServer,這時 Flask 的線程安全就是有意義的了,在其餘多線程的服務器中也是同樣。

總結

一個請求的旅程

這裏,咱們經過追蹤一個請求到達服務器並返回(固然是經過「變成」一個相應)的旅程,串講本文的內容。

  1. 在請求發出以前,Flask 註冊好了全部的視圖函數和 URL 映射,服務器在本身身上註冊了 Flask 應用。
  2. 請求到達服務器,服務器準備好 environmake_response 函數,而後調用了本身身上註冊的 Flask 應用。
  3. 應用實現了 WSGI 要求的 application(environ, make_response) 方法。在 Flask 中,這個方法是個被 __call__ 中轉的叫作 wsgi_app 的方法。它首先經過 environ 建立了請求上下文,並將它推入棧,使得 flask 在處理當前請求的過程當中均可以訪問到這個請求上下文。
  4. 而後 Flask 開始處理這個請求,依次調用 before_first_request_funcs before_request_funcs view_functions 中的函數,並最終經過 finalize_request 生成一個 response 對象,當中只要有函數返回值,後面的函數組就不會再執行,after_request_funcs 進行 response 生成後的後處理。
  5. Flask 調用這個 response 對象,最終調用了 make_response 函數,並返回了一個可遍歷的響應內容。
  6. 服務器發送響應。

Flask 和 werkzeug

在分析過程當中,能夠很明顯地看出 Flask 和 werkzeug 是強耦合的,實際上 werkzeug 是 Flask 惟一不可或缺的依賴,一些很是細節的工做,其實都是 werkzeug 庫完成的,在本文的例子中,它至少作了這些事情:

  1. 封裝 ResponseRequest 類型供 Flask 使用,在實際開發中,咱們在請求和響應對象上的操做,調用的實際上是 werkzeug 的方法。
  2. 實現 URL 到視圖函數的映射,而且能把 URL 中的參數傳給該視圖函數。咱們看到了 Flask 的 url_map 屬性而且看到了它如何綁定視圖函數和錯誤處理函數,可是具體的映射規則的實踐,和在響應過程當中的 URL 解析,都是由 werkzeug 完成的。
  3. 經過 LocalProxy 類生成的 _request_ctx_stack 對 Flask 實現線程保護。

對於 Flask 的源碼分析先暫時到這裏。有時間的話,我會分析 Flask 中的模板渲染、import request、藍圖和一些好用的變量及函數,或者深刻分析 werkzeug 庫。

參考閱讀

  1. flask 源碼解析系列文章,你能夠在讀完本文了解主線以後,再看這系列文章瞭解更加細節的東西。
  2. 一塊兒寫一個 web 服務器

文章更新記錄

  • 2017年9月10日:利用 0.12.0 版本進行分析,從新調整告終構和行文順序,增長了許多內容。
相關文章
相關標籤/搜索