一個Flask應用運行過程剖析

相信不少初學Flask的同窗(包括我本身),在閱讀官方文檔或者Flask的學習資料時,對於它的認識是從如下的一段代碼開始的:web

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的應用。shell

然而,這段代碼怎麼運行起來的呢?一個Flask應用運轉的背後又有哪些邏輯呢?若是你只關心Web應用,那對這些問題不關注也能夠,但從整個Web編程的角度來看,這些問題很是有意義。本文就主要針對一個Flask應用的運行過程進行簡要分析,後續文章還會對Flask框架的一些具體問題進行分析。數據庫

爲了分析方便,本文采用 Flask 0.1版本 的源碼進行相關問題的探索。編程

一些準備知識

在正式分析Flask以前,有一些準備知識須要先了解一下:flask

  1. 使用Flask框架開發的屬於Web應用。因爲Python使用WSGI網關,因此這個應用也能夠叫WSGI應用;瀏覽器

  2. 服務器、Web應用的設計應該遵循網關接口的一些規範。對於WSGI網關,要求Web應用實現一個函數或者一個可調用對象webapp(environ, start_response)。服務器或網關中要定義start_response函數而且調用Web應用。關於這部分的內容能夠參考:wsgiref包——符合WSGI標準的Web服務實現(一)服務器

  3. Flask依賴於底層庫werkzeug。相關內容能夠參考:Werkzeug庫簡介session

本文暫時不對服務器或網關的具體內容進行介紹,只需對服務器、網關、Web應用之間有怎樣的關係,以及它們之間如何調用有一個瞭解便可。數據結構

一個Flask應用運行的過程

1. 實例化一個Flask應用

使用app = Flask(__name__),能夠實例化一個Flask應用。實例化的Flask應用有一些要點或特性須要注意一下:app

  1. 對於請求和響應的處理,Flask使用werkzeug庫中的Request類和Response類。對於這兩個類的相關內容能夠參考:Werkzeug庫——wrappers模塊

  2. 對於URL模式的處理,Flask應用使用werkzeug庫中的Map類和Rule類,每個URL模式對應一個Rule實例,這些Rule實例最終會做爲參數傳遞給Map類構造包含全部URL模式的一個「地圖」。這個地圖能夠用來匹配請求中的URL信息,關於Map類和Rule類的相關知識能夠參考:Werkzeug庫——routing模塊

  3. 當實例化一個Flask應用app(這個應用的名字能夠隨便定義)以後,對於如何添加URL模式,Flask採起了一種更加優雅的模式,對於這點能夠和Django的作法進行比較。Flask採起裝飾器的方法,將URL規則和視圖函數結合在一塊兒寫,其中主要的函數是route。在上面例子中:

    @app.route('/')
    def index():
        pass

    這樣寫視圖函數,會將'/'這條URL規則和視圖函數index()聯繫起來,而且會造成一個Rule實例,再添加進Map實例中去。當訪問'/'時,會執行index()。關於Flask匹配URL的內容,能夠參考後續文章。

  4. 實例化Flask應用時,會創造一個Jinja環境,這是Flask自帶的一種模板引擎。能夠查看Jinja文檔,這裏先暫時不作相關介紹。

  5. 實例化的Flask應用是一個可調用對象。在前面講到,Web應用要遵循WSGI規範,就要實現一個函數或者一個可調用對象webapp(environ, start_response),以方便服務器或網關調用。Flask應用經過__call__(environ, start_response)方法可讓它被服務器或網關調用。

    def __call__(self, environ, start_response):
        """Shortcut for :attr:`wsgi_app`"""
        return self.wsgi_app(environ, start_response)

    注意到調用該方法會執行wsgi_app(environ, start_response)方法,之因此這樣設計是爲了在應用正式處理請求以前,能夠加載一些「中間件」,以此改變Flask應用的相關特性。對於這一點後續會詳細分析。

  6. 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

中間件SharedDataMiddlewarewerkzeug.wsgi模塊中的一個類。該類能夠爲Web應用提供靜態內容的支持。例如:

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應用在初始實例化的時候便有所應用。其中有這樣一段代碼:

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的例子。

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,將app1app2app合併起來。這樣,此時的app便成爲一個DispatcherMiddleware對象。

當在服務器中調用app時,因爲它是一個DispatcherMiddleware對象,因此首先會觸發它的__call__(environ, start_response)方法。而後根據請求URL中的信息來肯定要調用哪一個應用。例如:

  • 若是訪問/,則會觸發app(environ, start_response)注意: 此時app是一個Flask對象),進而處理要訪問app的請求;

  • 若是訪問/app1,則會觸發app1(environ, start_response),進而處理要訪問app1的請求。訪問/app2同理。

3. 和請求處理相關的上下文對象

當Flask應用真正處理請求時,wsgi_app(environ, start_response)被調用。這個函數是按照下面的方式運行的:

def wsgi_app(environ, start_response):
    with self.request_context(environ):
        ...

請求上下文

能夠看到,當Flask應用處理一個請求時,會構造一個上下文對象。全部的請求處理過程,都會在這個上下文對象中進行。這個上下文對象是_RequestContext類的實例。

# 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應用相關的一些屬性:

  • app ——上下文對象的app屬性是當前的Flask應用;

  • url_adapter ——上下文對象的url_adapter屬性是經過Flask應用中的Map實例構形成一個MapAdapter實例,主要功能是將請求中的URL和Map實例中的URL規則進行匹配;

  • request ——上下文對象的request屬性是經過Request類構造的實例,反映請求的信息;

  • session ——上下文對象的session屬性存儲請求的會話信息;

  • g ——上下文對象的g屬性能夠存儲全局的一些變量。

  • flashes ——消息閃現的信息。

LocalStack和一些「全局變量」

注意: 當進入這個上下文對象時,會觸發_request_ctx_stack.push(self)。在這裏須要注意Flask中使用了werkzeug庫中定義的一種數據結構LocalStack

_request_ctx_stack = LocalStack()

關於LocalStack,能夠參考:Werkzeug庫——local模塊LocalStack是一種棧結構,每當處理一個請求時,請求上下文對象_RequestContext會被放入這個棧結構中。數據在棧中存儲的形式表現成以下:

{880: {'stack': [<flask._RequestContext object>]}, 13232: {'stack': [<flask._RequestContext object>]}}

這是一個字典形式的結構,鍵表明當前線程/協程的標識數值,值表明當前線程/協程存儲的變量。werkzeug.local模塊構造的這種結構,很容易實現線程/協程的分離。也正是這種特性,使得能夠在Flask中訪問如下的「全局變量」:

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始終指向當前線程/協程中存儲的「請求上下文」,這樣像apprequestsessiong等均可以以「全局」的形式存在。這裏「全局」是指在當前線程或協程當中。

由此能夠看出,當處理請求時:

  • 首先,會生成一個請求上下文對象,這個上下文對象包含請求相關的信息。而且在進入上下文環境時,LocalStack會將這個上下文對象推入棧結構中以存儲這個對象;

  • 在這個上下文環境中能夠進行請求處理過程,這個稍後再介紹。不過能夠以一種「全局」的方式訪問上下文對象中的變量,例如apprequestsessiong等;

  • 當請求結束,退出上下文環境時,LocalStack會清理當前線程/協程產生的數據(請求上下文對象);

  • Flask 0.1版本只有「請求上下文」的概念,在Flask 0.9版本中又增長了「應用上下文」的概念。關於「應用上下文」,之後再加以分析。

4. 在上下文環境中處理請求

處理請求的過程定義在wsgi_app方法中,具體以下:

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)

從代碼能夠看出,在上下文對象中處理請求的過程分爲如下幾個步驟:

  1. 在請求正式被處理以前的一些操做,調用preprocess_request()方法,例如打開一個數據庫鏈接等操做;

  2. 正式處理請求。這個過程調用dispatch_request()方法,這個方法會根據URL匹配的狀況調用相關的視圖函數;

  3. 將從視圖函數返回的值轉變爲一個Response對象;

  4. 在響應被髮送到WSGI服務器以前,調用process_response(response)作一些後續處理過程;

  5. 調用response(environ, start_response)方法將響應發送回WSGI服務器。關於此方法的使用,能夠參考:Werkzeug庫——wrappers模塊

  6. 退出上下文環境時,LocalStack會清理當前線程/協程產生的數據(請求上下文對象)。

相關文章
相關標籤/搜索