Flask 請求處理流程(二):Context 對象

上下文一直是計算機中難理解的概念,在知乎的一個問題下面有個很通俗易懂的回答:

每一段程序都有很多外部變量。只有像 Add 這種簡單的函數纔是沒有外部變量的。一旦你的一段程序有了外部變量,這段程序就不完整,不能獨立運行。你爲了使他們運行,就要給所有的外部變量一個一個寫一些值進去。這些值的集合就叫上下文(context)
–– vzch

比如,在 flask 中,視圖函數(views.py)需要知道它執行的請求信息(請求的 url,參數,方法等)以及應用信息(應用中初始化的數據庫等),才能夠正確運行和給出相應的返回。

最直觀地做法是把這些信息封裝成一個對象,作爲參數傳遞給視圖函數。但是這樣的話,所有的視圖函數都需要添加對應的參數,即使該函數內部並沒有使用到它。

flask 的做法是把這些信息作爲類似**全局變量的東西**,視圖函數需要的時候,可以使用 from flask import request 獲取。但是這些對象和全局變量不同的是——它們必須是動態的,因爲在多線程或者多協程的情況下,每個線程或者協程獲取的都是自己獨特的對象,不會互相干擾。

Context(上下文)


Request Context 請求上下文

  • Request:請求的對象,封裝了 Http 請求(environ)的內容
  • Session:根據請求中的 cookie,重新載入該訪問者相關的會話信息。

App Context 程序上下文

  • current_app:當前激活程序的程序實例
  • g:處理請求時用作臨時存儲的對象。每次請求都會重設這個變量

生命週期

  • current_app 的生命週期最長,只要當前程序實例還在運行,都不會失效。
  • Requestg 的生命週期爲一次請求期間,當請求處理完成後,生命週期也就完結了
  • Session 就是傳統意義上的 session 。只要它還未失效(用戶未關閉瀏覽器、沒有超過設定的失效時間),那麼不同的請求會共用同樣的 session。

處理流程

  1. 創建上下文
  2. 入棧
  3. 請求分發
  4. 上下文對象出棧
  5. 響應WSGI
第一步:創建上下文

Flask 根據 WSGI Server 封裝的請求信息(存放在environ),新建 RequestContext 對象 和 AppContext 對象

# 聲明對象
# LocalStack  LocalProxy 都由 Werkzeug 提供
# 我們不深究他的細節,那又是另外一個故事了,我們只需知道他的作用就行了
# LocalStack 是棧結構,可以將對象推入、彈出
# 也可以快速拿到棧頂對象。當然,所有的修改都只在本線程可見。
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

# 如果調用一個 LocalStack 實例, 能返回一個 LocalProxy 對象
# 這個對象始終指向這個 LocalStack 實例的棧頂元素。
# 如果棧頂元素不存在,訪問這個 LocalProxy 的時候會拋出 RuntimeError 異常
# LocalProxy 對象你只需暫時理解爲棧裏面的元素即可了
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

部分 RequestContext 源碼

# RequestContext
class RequestContext(object):
    def __init__(self, app, environ, request=None):    
        self.app = app    
        if request is None:        
                request = app.request_class(environ)    
        self.request = request    
        self.url_adapter = app.create_url_adapter(self.request)    
        self.flashes = None    
        self.session = None

部分 AppContext 源碼

#AppContext
class AppContext(object):
    def __init__(self, app):    
        self.app = app    
        self.url_adapter = app.create_url_adapter(None)    
        self.g = app.app_ctx_globals_class()    
        self._refcnt = 0

這裏需要注意的是,RequestContext 在初始化的時候,當前 Flask 的實例作爲參數被傳進來。雖然每次的請求處理都會創建一個 RequestContext 對象,但是每一次傳入的 app 參數卻是同一個。通過這個機制,可以使得:

由同一個 Flask 實例所創建的 RequestContext,其成員變量 app 都是同一個 Flask 實例對象 。實現了多個 RequestContext 對應同一個 current_app 的目的。

第二步:入棧

RequestContext 對象 push 進 _request_ctx_stack 裏面。
在這次請求期間,訪問 request、session 對象將指向這個棧的棧頂元素

class RequestContext(object):
    def push(self):   
        ....
        _app_ctx_stack.push(self)   
        appcontext_pushed.send(self.app)

AppContext 對象 push 進 _app_ctx_stack裏面。
在這次請求期間,訪問 g 對象將指向這個棧的棧頂元素

class AppContext(object):
    def push(self):   
        ....
        _request_ctx_stack.push(self)
第三步:請求分發
response = self.full_dispatch_request()

Flask 將調用 full_dispatch_request 函數進行請求的分發,之所以不用給參數,是因爲我們可以通過 request 對象獲得這次請求的信息。full_dispatch_request 將根據請求的 url 找到對應的藍本里面的視圖函數,並生成一個 response 對象。注意的是,在請求之外的時間,訪問 request 對象是無效的,因爲 request 對象依賴請求期間的 _request_ctx_stack 棧。

第四步:上下文對象出棧

這次 HTTP 的響應已經生成了,就不需要兩個上下文對象了。分別將兩個上下文對象出棧,爲下一次的 HTTP 請求做出準備。

第五步:響應 WSGI

調用 Response 對象,向 WSGI Server 返回其結果作爲 HTTP 正文。Response 對象是一個可調用對象,當調用發生時,將首先執行 WSGI 服務器傳入的 start_response() 函數,發送狀態碼和 HTTP 報文頭。

最後再來看下 Flask 處理請求的 wsgi_app 函數:
# environ: WSGI Server 封裝的 HTTP 請求信息
# start_response: WSGI Server 提供的函數,調用可以發送狀態碼和 HTTP 報文頭
def wsgi_app(self, environ, start_response):
    # 根據 environ 創建上下文
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # 把當前的 request context,app context 綁定到當前的 context
            ctx.push()
            # 根據請求的 URL,分發請求,經過視圖函數處理後返回響應對象
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.make_response(self.handle_exception(e))
        except:
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        # 最後出棧
        ctx.auto_pop(error)

ctx.py 源碼


代碼信息:

總共代碼行數:468 行,
有效行數:184 行,
註釋:187 行,
空行:97 行

從這裏可以看出 flask 是如此簡約而不簡單的微框架,可見一斑。

三個 class,分別爲:

  • _AppCtxGlobals() : 一個“純對象” A plain object,應用上下文存儲數據的命名空間
  • AppContext() : application context 應用上下文,隱式地綁定 application 對象到當前線程或 greenlet, 類似於 RequestContext 綁定請求信息一樣。當請求上下文被創建的時候,應用上下文也會被隱式的創建,但應用不會存在於單個應用上下文的頂部
class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0
  • RequestContext() :request context 請求上下文,包括了所有請求相關的信息。在剛發生請求的時候就會創建請求上下文並將它推到 _request_ctx_stack 堆中,當請求結束時會被刪除。它會爲所提供的 WSGI 環境創建 URL 適配器和請求對象。
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.

    Do not attempt to use this class directly, instead use
    :meth:`~flask.Flask.test_request_context` and
    :meth:`~flask.Flask.request_context` to create this object.

    When the request context is popped, it will evaluate all the
    functions registered on the application for teardown execution
    (:meth:`~flask.Flask.teardown_request`).

    The request context is automatically popped at the end of the request
    for you.  In debug mode the request context is kept around if
    exceptions happen so that interactive debuggers have a chance to
    introspect the data.  With 0.4 this can also be forced for requests
    that did not fail and outside of ``DEBUG`` mode.  By setting
    ``'flask._preserve_context'`` to ``True`` on the WSGI environment the
    context will not pop itself at the end of the request.  This is used by
    the :meth:`~flask.Flask.test_client` for example to implement the
    deferred cleanup functionality.

    You might find this helpful for unittests where you need the
    information from the context local around for a little longer.  Make
    sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
    that situation, otherwise your unittests will leak memory.
    """

    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = None
        try:
            self.url_adapter = app.create_url_adapter(self.request)
        except HTTPException as e:
            self.request.routing_exception = e
        self.flashes = None
        self.session = session

        # Request contexts can be pushed multiple times and interleaved with
        # other request contexts.  Now only if the last level is popped we
        # get rid of them.  Additionally if an application context is missing
        # one is created implicitly so for each level we add this information
        self._implicit_app_ctx_stack = []

        # indicator if the context was preserved.  Next time another context
        # is pushed the preserved context is popped.
        self.preserved = False

        # remembers the exception for pop if there is one in case the context
        # preservation kicks in.
        self._preserved_exc = None

        # Functions that should be executed after the request on the response
        # object.  These will be called before the regular "after_request"
        # functions.
        self._after_request_functions = []

        if self.url_adapter is not None:
            self.match_request()

注: A plain object:非常輕量,主要用來歸集一些屬性方便訪問。

四個 def,分別爲:

  • after_this_request():在該請求之後執行一個函數,這對於修改 response 響應對象很有用。它被傳遞給 response 對象並且必須返回相同或一個新的函數
@app.route('/')
def index():
    @after_this_request
    def add_header(response):
        response.headers['X-Foo'] = 'Parachute'  ## 響應頭中插入一個值
        return response
    return 'Hello World!'
  • copy_current_request_context():一個輔助函數,裝飾器,用來保留當前 request context。在與 greenlet 一起使用時非常有用。當函數被調用的時刻,會創建並推入被請求上下文複本裝飾後函數,當前 session 也會同時被包含在已複製的請求上下文中
import gevent
from flask import copy_current_request_context

@app.route('/')
def index():
    @copy_current_request_context
    def do_some_work():
        # do some work here, it can access flask.request or
        # flask.session like you would otherwise in the view function.
        ...
    gevent.spawn(do_some_work)
        return 'Regular response'
  • has_request_context():如果你有需要測試的代碼,該函數可以被使用。例如,如果請求對象可用,你或許想利用請求信息;但如果不可用,利用請求信息會失敗

  • has_app_context():與 has_request_context 類似,只是這個用於 application context。你可以使用對 current_app 對象的 boolean 檢查來代替它

參考:

  1. https://www.jianshu.com/p/2a2407f66438
  2. https://zhuanlan.zhihu.com/p/32457833
  3. https://blog.tonyseek.com/post/the-context-mechanism-of-flask/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章