標籤:

嘗試理解Flask源碼 之 搞懂WSGI協議

1. 小記

最近在學習Flask這個Web框架, 相比於Django, Flask算的上是微型的Web框架了,他只有路由和模板渲染兩個功能, 想干別的事都需要使用插件. 好在目前的插件數量也不少, 也不乏一些十分好用的插件, 讓Flask在企業Web應用開發中還是有一席之地的(我聽說知乎就是用的Flask+tornado).

Flask插件網站

這一路學下來, 基本上就會寫一些視圖函數, 完成簡單的業務邏輯, 對於框架執行流程其實知道得很少, 僅僅是對路由機制有一點點的了解.

Flask的路由機制主要依賴Werkzeug.routing模塊, 主要是Map類, Rule類, MapAdapter類等提供的功能

於是一直想看看Flask的源碼, 對這個框架進行進一步的研究, 但是無奈實在看不懂, 不知道從何看起是最大的問題.

於是我自己一直嘗試尋找切入點, 好讓我理清看源碼的思路, 接下來我就主要記錄一下自己的琢磨過程, 這個思考琢磨的過程對我一個新手開發者來說很重要.

我想:

  1. 一個Web應用都是從接受請求開始, 通過分析請求, 匹配對應的路由規則, 再去調用視圖函數, 返迴響應結果的, 那麼Flask中接受請求的入口在哪裡?

  2. 由於Flask是遵守WSGI協議的, WSGI協議是Python中的一種Web規範, 一定有大家共同遵守的規則, 那麼WGSI協議中, 大家遵守的規則是什麼?

  3. 通過大家都遵守的規則, 也許就能找到Flask中處理請求的入口在哪裡了. 同時, 任何遵循這個協議框架的請求入口都能找到.

實際上這個思考過程並沒有這麼順理成章, 我在學習Flask之前就看過相關WSGI的介紹, 無奈也不是特別能理解.

但是當先去了解了某些概念後, 以後再別的地方再次碰到這個問題, 首先就知道去哪裡查相關資料了, 這個時候再問題為導向去學習新的知識的時候就會理解的快得多, 並且當理解之後就會融會貫通.

那第一步就是研究一下究竟什麼是WSGI.

2. WSGI

WSGI全稱Web Server Gateway Interface, 不要被名字唬住, 先看這麼一句話一起體會一下WSGI是什麼:

This document specifies a proposed standard interface between web servers and Python web applications or frameworks, to promote web application portability across a variety of web servers.

這句話來自PEP3333的摘要部分, 簡單翻譯一下就是

這份文檔(指的就是PEP3333)詳細說明了Web伺服器和Web應用之間的標準介面, 旨在提升Web應用的可移植性

這裡面的關鍵字就是:

  • 可移植性: 這是WSGI存在的目的

  • 標準介面: 這是WSGI定義的規則

如果知道了WSGI的目的, 理解後續的內容就輕鬆不少, 那我們就先講一下WSGI的目的, 理解一下可移植性的含義吧

2.1 WSGI協議的作用

當我們訪問某個網站的時候:

  1. 瀏覽器作為用戶代理為我們發送了HTTP請求

  2. 這條請求經過長途的跋涉終於找到了能夠接受它的伺服器

  3. 伺服器上的HTTP伺服器軟體會將請求交給Web應用處理該請求,得到用戶想要的數據

  4. Web應用再將數據交給HTTP伺服器軟體, 再由HTTP伺服器軟體返迴響應結果

  5. 瀏覽器接受到響應, 顯示響應內容

基本上就是這麼一回事:

這裡面一共出現了三個角色, 分別是:

  1. 伺服器: 硬體層面, 也就是機房裡面的計算機(或者集群), 運行著操作系統+一些軟體.

  2. HTTP伺服器軟體: 運行在伺服器上的軟體, 用於監聽客戶端請求,將接受到的請求交給Web應用, 再將Web應用的響應結果返回給客戶端.

  3. Web應用: 接受伺服器軟體傳過來的請求, 處理請求, 並返回處理後的結果給伺服器軟體.(我們的Flask, Django都是這個層面的)

由於伺服器硬體咱們也沒什麼好說的, 因此接下來提到的伺服器(包括HTTP伺服器)指的都是HTTP伺服器軟體.

對於初學者來說伺服器好像就是硬體嘛, 和軟體沒有關係, 但實際上, 伺服器既可以指硬體, 也可以指軟體. 維基百科.

那WSGI協議的作用在這裡就是連接伺服器軟體和Web應用的橋樑, 這兩方規定一系列協議, 要求兩者之間傳輸的數據對方都能看得懂.

那這麼做有什麼好處呢?其實就是上面說的可移植性了, 如果對可移植性還是不太理解也沒有關係, 下面讓我們來看看這裡面一個問題.

既然伺服器軟體和Web應用都可接受請求, 返回請求, 為什麼搞這麼複雜?非要搞個協議, 接受請求,處理請求和返回請求的都是一個人不可以么?我和我交流還需要協議么?

沒錯, 上一個人也是這麼想的, 而且事實上, 我們確實可以這麼做.接下來我們來看一下三種模型, 來更好的理解一下WSGI協議和可移植性.

2.1.1 第一種模型 - tornado

tornado是由F打頭的404網站收購併且開源出來的Web框架, 他的第一個特點奏是將HTTP伺服器Web應用整合到了一起.

所以可以用tornado搭建出來的伺服器模型如下

這就是我們剛才說的接受請求, 處理請求, 返回請求都是一個人, 感覺也不錯.

但是請注意了, 雖然表面上看起來這好像是一個人處理的, 但是我們在編寫處理業務邏輯的代碼時, 肯定不會去碰伺服器相關的代碼, 只需要寫好視圖和路由就可以了, 因此對於tornado來說, 他的伺服器部分和邏輯處理部分還是分開的. 這就相當於雖然是一個人, 但是他的手和大腦是兩個部分.

實際上長這樣

這裡有兩個問題.

  1. 如果我不想用tornado這個框架寫邏輯部分, 那麼我也一定不能使用它的伺服器部分.

  2. 如果我就想用它的Web框架, 想換一個性能更加強大的伺服器(軟體), 似乎也做不到.

誰叫他們是一個人呢!

這就是所謂的沒有可移植性

請注意:

實際上, tornado的伺服器和Web框架是兼容WSGI協議的, 有興趣的話可以自己搜索一下相關內容.舉這個例子是因為在Python Web框架中, tornado實現了高性能的HTTP伺服器僅此而已.

如果百度過Tornado的也許知道他有一個很大的特點就是, 非同步, 非阻塞, 高性能之類的. 其實, 這個特點說是他的HTTP伺服器部分, tornado伺服器擅長處理多個長連接, 可以用於在線聊天等業務場景.

2.1.2 第二種模型 - WSGI伺服器+Web框架

終於輪到WSGI出場了.

對於一個遵守WSGI協議的伺服器和Web應用來說, 它並不在意到底是誰傳過來的數據, 只需要知道傳過來的數據符合某種格式, 兩邊都能處理對方傳入的數據.

打個比方, 你特別特別想吃水餃, 於是請了倆人, A專門擀麵皮, B專門包餃子, 由於擀麵皮的人動作比較快, A還要負責把包好的餃子拿過來下鍋, 這樣你才能吃到水餃.

A擀麵皮的時候需要遵守餃子協議, 一定要把麵皮擀成圓的(沒錯, 餛飩皮就是方的!), 並且皮還不能太大, 太厚, 這樣B才能保證包出餃子來.

同樣的, B也要遵守餃子協議, 再拿到餃子皮後使用靈巧的手法捏出造型各異的水餃, 但是他包的一定是餃子, 不能是一坨A看不懂的東西!

B把包好水餃拿給A去下餃子, 好在這二人都遵守了餃子協議, A一看, 不錯, 這傢伙包出的的確是餃子, 那我也保證我能夠最後煮熟的東西是水餃了.

最後, 你吃到了水餃, 但是A個B始終也不認識對方, 如果在這個過程中你吧B換了另一個遵循餃子協議的C, 他與A還是能夠緊密合作.

A只在乎自己擀出的是麵皮, 拿到的是餃子

B只在乎拿到的麵皮, 包出的是餃子

於是我們可以搭建像這樣的模型:

目前被廣泛應用的WSGI伺服器(又稱為WSGI容器), 主要有gunicornuWSGI.完整列表

而遵循WSGI協議的python web框架有Django, Flask, Pyramid, web2py, Bottle 等等等.完整列表

最終, 我們有:

隨便怎麼組合都可以, 怎麼樣, 是不是可移植性更強了?

2.1.3 第三種模型 - 最終形態

然而在真正的生產環境中, 以我們上面的模型是完全扛不住很多人一起訪問的, 於是就有了伺服器集群的概念, 使用一個性能更好的伺服器打頭陣, 然後它所做的事情就是把接收到的請求再分發給其他計算機去處理.

這就好像後來你開了個餃子館, 為了能夠接待更多的人, 你必須再雇對幾個包餃子的組合. 當然, 還必須有一個收銀人員.

這裡拿NGINX來舉例

在這個模型中, 我們的WSGI伺服器起到了承上啟下的作用, 它只處理NGINX丟給他的請求.

當然, 這也不意味著我們對WSGI伺服器性能要求不高了, 因為真正去調用Web應用的還是WSGI伺服器, 我們只不過使用NGINX去實現了負載均衡.

其實第三種形態遠沒有這麼簡單. 我了解的也不是很多, 但是到了這裡, 我們應該已經完全理解了WSGI協議的目的了. 那接下來就來看一下WSGI協議到底指定了哪些規則吧!

了解過部署的朋友可能知道還有另一個高性能的伺服器-Apache, 但是通過阿帕奇與WSGI應用的交互似乎是通過阿帕奇自帶的模塊去進行的.

How to use Django with Apache and mod_wsgi

2.2 WSGI協議簡單分析

WSGI協議這麼厲害!那一定很難實現吧!

其實WSGI協議內容沒有這麼神秘, 也特別容易實現, 它只是一系列簡單的規則, 這個規則有多簡單呢, 一會兒我們用3行代碼就可以寫完一個簡單的WSGI應用, 而且不需要導入任何模塊和包.

由於涉及伺服器與Web應用交互, WSGI的規則分為兩個部分, 分別對WSGI伺服器端和WSGI應用端做了要求. 作為Web後端開發人員, 我們和框架(應用)打交道, 只需要了解WSGI協議對應用的要求.(我才不會說我也沒有看伺服器端的協議內容)

假設現在已經有了一個WSGI伺服器, 現在需要編寫一個WSGI應用, 那麼我們的應用該如何和伺服器進行交互呢?

換句話說, 我們的應用需要接受什麼樣的數據, 需要返回什麼樣的數據呢?

OK, 讓我們把WSGI應用端規則羅列一下:

  1. Web應用必須是一個可調用對象, 它必須*接受*兩個參數

  2. 其中第一個參數是environ(字典類型), 另一個參數是start_response(函數)

  3. 需要在返迴響應之前調用start_response

  4. 最終返回的一個可迭代對象

什麼?你問我什麼是可調用對象, 什麼是可迭代對象?不如去問問他吧.

這兩個參數的具體內容為:

  1. environ 本次請求的所有內容, 我挑了幾個看著眼熟的.

{ REQUEST_METHOD: GET,
RAW_URI: /,
SERVER_PROTOCOL: HTTP/1.1,
HTTP_HOST: 127.0.0.1:5000,
HTTP_CONNECTION: keep-alive,
HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,
HTTP_ACCEPT_LANGUAGE: zh-CN,zh;q=0.9,en;q=0.8,
REMOTE_ADDR: 127.0.0.1,
REMOTE_PORT: 62445,
PATH_INFO: /,
...
}

  1. start_response函數, 返回數據之前需要調用, 用於設置狀態碼響應頭

  • 注意, 調用start_response函數時, 第一個參數用於設置狀態碼, 是一個字元串

  • 第二個參數用於設置響應頭, 要注意該參數的格式(見下方代碼).

根據這些個規則, 我們很容易寫出一個遵循WSGI協議的Web應用:

# 1. 必須是一個**可調用對象**, 它必須接受**兩個參數**
def demo_app(environ,start_response):
# 2. 其中第一個參數是`environ`(字典類型), 另一個參數是`start_response`(函數)

# print(environ)

# 3. 返回之前調用一次start_response 返回狀態碼和響應頭, 注意參數格式, 不同的請求頭信息用元組隔開
start_response("200 OK", [(Content-Type,text/html)])

# 4. 返回一個可迭代對象, 這裡就是最終的響應體
return [b"<h1>Hello WSGI!</h1>"]

是不是很簡單? 排除注釋一共就只有3行代碼.

至此, 我們就看完了WSGI協議應用端的部分. 那麼現在, 來看一下Flask中的WSGI是怎麼體現的吧

上面的函數是一個完全符合WSGI協議的函數, 因此他就可以作為一個WSGI應用, 你甚至可以將它跑起來!

如果你已經安裝了Gunicorn, 將上述代碼保存, 並命名為my_app.py, 你只需要將終端切換到該文件所在的路徑下, 然後輸入

gunicorn -b 127.0.0.1:5000 my_app:demo_app

就可以正常運行起來, 此時用瀏覽器訪問127.0.0.1:5000, 就能看到返回的結果!

你也可以將print所在行注釋掉, 看一下environ裡面包含的完整信息

注意: Gunicorn只支持類Unix系統! 不支持Windows!

2.3 Flask中的WSGI

我們知道了, 遵循WSGI協議的應用一定是一個可調用對象, 所以它既可以是一個函數, 也可以是一個實現了__call__()方法的類.

話不多說, 上源碼

class Flask(_PackageBoundObject):
.....

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

我們輕鬆找到了Flask中處理請求的入口, 但是, 它又調用了另一個方法, 趁熱打鐵, 讓我們來看一下這個方法.

class Flask(_PackageBoundObject):
...

def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
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)

...

這個就是Flask中真正請求的入口了, 在兜里一個圈子後, 最終將兩個參數傳給了wsgi_app()這個方法, 並交由它來處理.

所有的請求就將會在這幾行代碼中處理完成, 並且最終返回. 只要搞懂了這幾步, 就能知道Flask是怎麼處理請求的啦!

看上去很輕鬆, 但實際上的步驟還是相當複雜的, 我會慢慢嘗試去理解, 一旦有所收穫也會第一時間整理並且更新.

如果有什麼問題可以私信我, 內容有錯誤也歡迎大家幫我糾正!

2.4 補充

  1. 我們在上面只討論了伺服器和應用這兩個角色, 其實, 還有另外一個角色叫做中間件middleware, 顧名思義, 它存在於這兩者之間. 對於伺服器來說, 它是一個應用, 而對於應用來說, 它又是一個伺服器, 中間件必須也滿足兩方的WSGI協議.

  2. Flask中自帶的伺服器也是一個WSGI伺服器, 只不過它的性能不好, 只能用於測試, 但是原理都是和上面一樣的.

  3. Python的WSGI協議是參考了Java的servlet協議.

  4. 在其他語言裡面也有類似WSGI協議的規定, 比如Ruby中的Rack, Perl中的PSGI,他們的目的都是一樣的.

  5. uWSGI這個伺服器也有自己的協議, 就叫做uwsgi, 但是它也支持WSGI協議. 另外uWSGI是C寫的, Gunicorn是Python寫的. 這二位應該是在部署的時候的唯二之選.

參考文章

Python Web開發最難懂的WSGI協議,到底包含哪些內容

WSGI介面 - 廖雪峰的官方網站

PEP 3333

吐槽一下: 這個PEP 3333是WSGI協議的第二個版本(1.0.1)了, 第一個版本(1.0.0)記錄在PEP 333中. 這個pep命名方式也是很厲害的

推薦閱讀:

TAG:WSGI | Python | Flask |