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

1. 小記

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

Flask插件網站

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

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

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

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

我想: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應用的可移植性

這裏面的關鍵字就是:app

  • 可移植性: 這是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框架有DjangoFlaskPyramidweb2pyBottle 等等等.完整列表

最終, 咱們有:

 

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

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寫的. 這二位應該是在部署的時候的惟二之選.

   https://zhuanlan.zhihu.com/p/46983059

參考文章

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

WSGI接口 - 廖雪峯的官方網站

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