Django 從啟動到請求到響應全過程分析-入門版

ryoma發表於2019-01-27

本文所用 Django 程式碼版本:2.1.3

本文中進行的分析並不侷限於某一個 Django 版本但都會盡量討論版本 2.0+

流程總覽

Django 處理請求流程

概述:

Django 和其他 Web 框架的 HTTP 處理的流程大致相同:先通過 Request Middleware 對請求物件做定義處理,然後再通過預設的 URL 指向的方法,最後再通過 Response Middleware 對響應物件做自定義處理。

細則:

  1. [啟動->WSGI]通過任意方式啟動 Django 建立 WSGIServer 類的例項
  2. 使用者通過瀏覽器請求某個 Django 頁面
  3. [WSGI]Django WSGIServer 接收客戶端(瀏覽器)請求初始化 WSGIHandler 例項
  4. [WSGI->載入配置]匯入 setting 配置和 Django 異常類
  5. [WSGI->中介軟體]載入 setting 中設定的中介軟體
  6. [中介軟體]建立 _request_middleware,_view_middleware,_response_middleware,_exception_middleware 四個列表
  7. [中介軟體]遍歷執行 _request_middleware,對 request 進行處理:若返回 None 進入到 8;若直接返回 HttpResponse 物件進入到 12
  8. [URL Resolver]解析 url 並進行匹配(假設匹配成功)
  9. [中介軟體]遍歷執行 _view_middleware,對 request 進行處理:若返回 None 進入到 10;若直接返回 HttpResponse 物件進入到 12。
  10. [中介軟體]實現 url 匹配的 view 邏輯:若引發異常進入到 11;若正常返回 HttpResponse 物件進入到 12
  11. [中介軟體]遍歷執行 _exception_middleware
  12. [中介軟體]遍歷執行 _response_middleware,對 HttpResponse 進行處理並最終返回 response

啟動

在開發環境中,我們一般是通過命令列執行 runserver 命令,ruserver 命令是使用 Django 自帶的的 Web Server,而在正式的環境中,一般會使用 Nginx+uWSGI 模式。

無論通過哪種方式,啟動一個專案時,都會做兩件事:

  1. 建立一個 WSGIServer 類的例項,來接受使用者的請求。
  2. 當一個使用者的 HTTP 請求到達的時,為使用者指定一個 WSGIHandler,用於處理使用者請求與響應,這個 Handler 是處理整個 Request 的核心。

WSGI

WSGI:全稱 Web Server Gateway Interface

WSGI 不是伺服器,Python 模組,框架,API 或者任何軟體,只是一種規範,描述 Web Server 如何與 Web Application 通訊的規範。

WSGI 協議主要包括 serverapplication 兩部分:

  1. WSGI Server 負責從客戶端接收請求,將 request 轉發給 application,將application 返回的 response 返回給客戶端;
  2. WSGI Application 接收由 server 轉發的 request,處理請求,並將處理結果返回給 serverapplication 中可以包括多個棧式的中介軟體(middlewares),這些中介軟體需要同時實現 serverapplication,因此可以在 WSGI 伺服器與 WSGI 應用之間起調節作用:對伺服器來說,中介軟體扮演應用程式,對應用程式來說,中介軟體扮演伺服器。

Django WSGI Application

WSGI Application 應該實現為一個可呼叫物件,例如:函式、方法、類(包含 call 方法)。

需要接收兩個引數:

  1. 包含客戶端請求的資訊以及其他資訊的字典。可以認為是請求上下文,一般叫做environment(編碼中多簡寫為 environenv);
  2. 用於傳送 HTTP 響應狀態(HTTP Status)、響應頭(HTTP Headers)的回撥函式;

通過回撥函式將響應狀態和響應頭返回給 WSGI Server,同時返回響應正文,響應正文是可迭代的、幷包含了多個字串。下面是 Django WSGI Application 的具體實現:

class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 本文作者注:載入中介軟體
        self.load_middleware()

    def __call__(self, environ, start_response):
        set_script_prefix(get_script_name(environ))
        # 本文作者注:處理請求前傳送訊號
        signals.request_started.send(sender=self.__class__, environ=environ)
        request = self.request_class(environ)
        response = self.get_response(request)

        response._handler_class = self.__class__

        status = '%d %s' % (response.status_code, response.reason_phrase)
        response_headers = list(response.items())
        for c in response.cookies.values():
            response_headers.append(('Set-Cookie', c.output(header='')))
        # 本文作者注:將響應的 header 和 status 返回給 server
        start_response(status, response_headers)
        if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
            response = environ['wsgi.file_wrapper'](response.file_to_stream)
        return response
複製程式碼

可以看出 Django WSGI Application 的流程包括:

  1. 載入所有中介軟體,以及執行框架相關的操作,設定當前執行緒指令碼字首,傳送請求開始訊號;
  2. 處理請求,呼叫 get_response() 方法處理當前請求,該方法的的主要邏輯是通過urlconf 找到對應的 viewcallback,按順序執行各種 middlewarecallback
  3. 呼叫由 WSGI Server 傳入的 start_response() 方法將響應 headerstatus 返回給 WSGI Server
  4. 返回響應正文。

Django WSGI Server

負責獲取 HTTP 請求,將請求傳遞給 Django WSGI Application,由 Django WSGI Application 處理請求後返回 response。以 Django 內建 server 為例看一下具體實現。

通過 runserver 命令執行 Django 專案,在啟動時都會呼叫下面的 run 方法,建立一個 WSGIServer 的例項,之後再呼叫其 serve_forever() 方法啟動服務。

def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
    server_address = (addr, port)
    if threading:
        httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {})
    else:
        httpd_cls = server_cls
    httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
    if threading:
        httpd.daemon_threads = True
    httpd.set_app(wsgi_handler)
    httpd.serve_forever()
複製程式碼

下圖表示 WSGI Server 伺服器處理流程中關鍵的類和方法(來自:參考引用_1

WSGI Server 伺服器處理流程中關鍵的類和方法

1. WSGIServer

run() 方法會建立 WSGIServer 例項,主要作用是接收客戶端請求,將請求傳遞給WSGI Application,然後將 WSGI Application 返回的 response 返回給客戶端。

  • 建立例項時會指定 HTTP 請求的 handlerWSGIRequestHandler 類;
  • 通過 set_appget_app 方法設定和獲取 WSGIApplication 例項wsgi_handler;
  • 處理 HTTP 請求時,呼叫 handler_request 方法,會建立 WSGIRequestHandler 例項處理 HTTP 請求;
  • WSGIServerget_request 方法通過 socket 接受請求資料;

2. WSGIRequestHandler

  • WSGIServer 在呼叫 handle_request 時建立例項,傳入 request,cient_address,WSGIServer 三個引數,__init__ 方法在例項化同時還會呼叫自身的 handle 方法;
  • handle 方法會建立 ServerHandler 例項,然後呼叫其 run 方法處理請求;

3. ServerHandler

  • WSGIRequestHandler 在其 handle 方法中呼叫 run 方法,傳入self.server.get_app() 引數,獲取 WSGIApplication,然後呼叫例項(call),獲取 response,其中會傳入 start_response 回撥,用來處理返回的 headerstatus
  • 通過 application 獲取 response 以後,通過 finish_response 返回 response

4. WSGIHandler(即 Django WSGI Application)

  • WSGI 協議中的 application,接收兩個引數,environ 字典包含了客戶端請求的資訊以及其他資訊,可以認為是請求上下文,start_response 用於傳送返回 statusheader 的回撥函式

雖然上面一個 Django WSGI Server 涉及到多個類實現以及相互引用,但其實原理還是呼叫WSGIHandler,傳入請求引數以及回撥方法 start_response(),並將響應返回給客戶端。

Python wsgiref simple_server

Python3.7 的原始碼中給出了一個 simple_server 案例位於 python3.7/wsgiref/simple_server.py

模組實現了一個簡單的 HTTP 伺服器,並給出了一個簡單的 demo,可以直接執行,執行結果會將請求中涉及到的環境變數在瀏覽器中展示出來。其中包括上述描述的整個 HTTP 請求的所有元件:ServerHandler,WSGIServer,WSGIRequestHandler 以及 demo_app 表示的簡易版的 WSGIApplication

感興趣的話可以自己去看一下原始碼。

載入配置

Django 的配置都在 {project_name}/settings.py 中定義,可以是 Django 的配置,也可以是自定義的配置,並且都通過 django.conf.settings 訪問。

中介軟體-Middleware

概述:

Django 中的 Middleware 類似底層中一個輕量級的外掛系統,它能夠介入 Django 的請求和響應過程,在全域性修改 Django 的輸入和輸出內容。從流程總覽圖中可以看出 Django 請求處理過程的核心在於 MiddlewareDjango 中所有的請求和響應都有 Middleware 的參與。

細則:

中介軟體流程圖

一個 HTTP 請求,首先被轉化成一個 HttpRequest 物件,然後該物件被傳遞給 Request Middleware 處理,如果它返回了 HttpResponse 物件,則直接傳遞給 Response Middleware 做收尾處理。否則的話 Request Middleware 將訪問 URL 配置,確定目標 view 來處理 HttpRequest 物件,在確定了 view,但是還沒有執行時候,系統會把 HttpRequest 物件傳遞給 View Middleware 進行處理,如果它返回了 HttpResponse 物件,那麼該 HttpResponse 物件將被傳遞給 Response Middleware 進行後續處理,否則將執行確定的 view 函式處理並返回 HttpResponse 物件,在整個過程中如果引發了異常並丟擲,會被 Exception Middleware 進行處理。

中介軟體執行順序

中介軟體流程圖

在請求階段,呼叫檢視之前,Django 按照 setting.py 設定的順序,自頂向下應用遍歷執行 Request Middleware。你可以把它想象成一個洋蔥:每個中介軟體類都是一個“層”,它覆蓋了洋蔥的核心。如果請求通過洋蔥的所有層(每一個呼叫 get_response)以將請求傳遞到下一層,一直到核心的檢視,那麼響應將在返回的過程中通過每個層(以相反的順序)。

如何編寫自己的中介軟體即中介軟體的深入瞭解

編寫一個自己的中介軟體是很容易的,每個中介軟體元件都是一個獨立的 Python Class,你可以在自定義的 Class 下編寫一個或多個下面的方法:

process_request

函式樣式:process_request(request)

引數解析:request 是一個 HTTPRequest 物件;

呼叫時間:在 Django 決定執行哪個 view 之前,process_request() 會被請求呼叫;

產生響應:它應該返回一個 None 或一個 HttpResponse 物件,如果返回 NoneDjango 會繼續處理這個請求;如果它返回一個 HTTPResponse 物件,Django 會直接跳轉到 Response Middleware

process_view

函式樣式:process_view(request, view_func, view_args, view_kwargs)

引數解析:request 是一個 HTTPRequest 物件,view_funcDjango 會呼叫的一個函式(準確的說是一個函式物件而非一個表示函式名的字串),view_args 是一個會被傳遞到檢視的 *argsview_kwargs 是一個會被傳遞到檢視的 **kwargsview_argsview_kwargs 都不包括 request

呼叫時間:process_view() 會在 Django 呼叫 view 前被呼叫;

產生響應:它應該返回一個 None 或一個 HttpResponse 物件,如果返回 NoneDjango 會繼續處理這個請求;如果它返回一個 HTTPResponse 物件,Django 會直接跳轉到 Response Middleware

PS:除 CsrfViewMiddleware 外中介軟體執行時在檢視執行前或在 process_view() 中訪問 request.POST 會使得之後的所有檢視無法修改 request,所以應該儘量避免。

process_template_response

函式樣式:process_template_response(request, response)

引數解析:request 是一個 HttpRequest 物件,response 是一個 TemplateResponse 物件(或類似物件),由 Django 檢視或中介軟體返回;

呼叫時間:如果 response 的例項有 render() 方法,process_template_response() 在檢視剛好執行完畢之後被呼叫,這表明他是一個 TemplateResponse 物件(或類似物件);

產生響應:這個方法必須返回一個實現了 render() 方法的 TemplateResponse 物件(或類似物件),它可以修改給定的 response 物件,也可以建立一個全新的 TemplateResponse 物件(或類似物件);

PS:在響應處理階段,中介軟體以相反的順序執行,包括 process_template_response

process_response

函式樣式:process_response(request, response)

引數解析:request 是一個 HttpRequest 物件,response 是一個 HttpResponse 物件,由 Django 檢視或中介軟體返回;

呼叫時間:process_request 在所有響應返回客戶端前被呼叫;

產生響應:這個方法必須返回一個 HttpRequest 物件,它可以修改給定的 response 物件,也可以建立一個全新的 HttpRequest 物件;

PS:process_response 總是被呼叫,這意味著你的 process_response 不能依賴 process_request

process_exception

函式樣式:process_exception(request, exception)

引數解析:request 是一個 HttpRequest 物件,exception 是一個被檢視丟擲 Exception 物件;

呼叫時間:當一個檢視丟擲異常,Django 會呼叫 process_exception 來處理;

產生響應:它應該返回一個 None 或一個 HttpResponse 物件,如果返回 NoneDjango 會繼續處理這個請求;如果它返回一個 HTTPResponse 物件,模板物件和 Response Middleware 會被直接返回給客戶端;否則,將啟動預設異常處理。;

URL Resolver

概述:

假設:中介軟體便利執行完 _request_middleware,_view_middleware 後都返回 None

Django 遍歷執行完 _request_middleware 後會得到一個經過處理的 request 物件,此時 Django 將按順序進行對 url 進行正則匹配,如果匹配不成功,就會丟擲異常;如果匹配成功,Django 會繼續迴圈執行 _view_middleware 並在執行後繼續執行剛剛匹配成功的 view

setting 中有一個 ROOT_URLCONF,它指向 urls.py 檔案,根據這個檔案可以生產一個 urlconf,本質上,他就是 url 與檢視函式之間的對映表,然後通過 resolver 解析使用者的 url,找到第一個匹配的 view

細則:

URL Resolver流程圖

重要函式原始碼位置:

_path: django/urls/conf.py
URLPattern: django/urls/resolvers.py
ResolverMatch: django/urls/resolvers.py
URLResolver: django/urls/resolvers.py
複製程式碼

原始碼比較長,就不放出來了,感興趣的話自己去看吧。

  1. 通過 urlpatterns 的配置執行 _path 函式;
  2. _path 函式進行判斷:如果是一個 list 或者 tuple,就用 URLResolver 處理,跳至 4;如果是一個正常的可呼叫的 view 函式,則用 URLPattern 處理,跳至;如果匹配失敗,丟擲異常;
  3. URLPattern 初始化相應值後執行 resolve 方法:如果匹配成功,返回 ResolverMatch;如果匹配失敗,丟擲異常;
  4. URLResolver 匹配 path 如果匹配成功,則繼續匹配它的 url_patterns,跳至 5;匹配失敗,丟擲異常;
  5. 匹配 url_patterns:若為 urlpattern 匹配成功,返回 ResolverMatch;若為 URLResolver 遞迴呼叫 URLResolver 跳至 4;若匹配失敗,丟擲異常;

可以發現,整個過程的關鍵就是 ResolverMatchURLPatternURLResolver 三個類,其中: ResolverMatch 是匹配結果,包含匹配成功後需要的資訊; URLPattern 是一個 url 對映資訊的物件,包含了 url 對映對應的可呼叫物件等資訊; URLResolver 是實現 url 路由,解析 url 的關鍵的地方,它的 url_patterns既可以是URLPattern 也可以是 URLResolver,正是因為這種設計, 實現了對 url 的層級解析。

總述

真實的請求響應過程肯定是比我提到的這些還要複雜的多,但是我的能力實在有限,目前僅能理解到這個層面了,如果錯誤歡迎指正。

參考引用:

  1. 簡書:做Python Web開發你要理解:WSGI & uWSGI 作者:rainybowe
  2. 掘金:Django從請求到響應的過程 作者:__奇犽犽
  3. 現代魔法學院:Python 與 Django 篇-Django 架構流程分析
  4. 簡書:django原始碼分析之url路由(URLResolver) 作者:2hanson
  5. Django 官方文件

相關文章