花了兩個星期,我終於把 WSGI 整明白了
在 三百六十行,行行轉 IT 的現狀下,很多來自各行各業的同學,都選擇 Python 這門膠水語言做為踏入網際網路大門的第一塊敲門磚,在這些人裡,又有相當大比例的同學選擇了 Web 開發這個方向(包括我)。而從事 web 開發,繞不過一個知識點,就是 WSGI。
不管你是否是這些如上同學中的一員,都應該好好地學習一下這個知識點。
由於我本人不從事專業的 python web 開發,所以在寫這篇文章的時候,借鑑了許多優秀的網路部落格,並花了很多的精力閱讀了大量的 OpenStack 程式碼。
為了寫這篇文章,零零散散地花了大概兩個星期。本來可以拆成多篇文章,寫成一個系列的,經過一番思慮,還是準備一篇講完,這就是本篇文章這麼長的原因。
另外,一篇文章是不能吃透一個知識點的,本篇涉及的背景知識也比較多的,若我有講得不到位的,還請你多多查閱其他人的網路部落格進一步學習。
在你往下看之前,我先問你幾個問題,你帶著這些問題往下看,可能更有目的性,學習可能更有效果。
問1:一個 HTTP 請求到達對應的 application處理函式要經過怎樣的過程?
問2:如何不通過流行的 web 框架來寫一個簡單的web服務?
一個HTTP請求的過程可以分為兩個階段,第一階段是從客戶端到WSGI Server,第二階段是從WSGI Server 到WSGI Application
今天主要是講第二階段,主要內容有以下幾點:
- WSGI 是什麼,因何而生?
- 為什麼需要有 WSGI?(補充)
- HTTP請求是如何到應用程式的?
- 實現一個簡單的 WSGI Server
- 實現“高併發”的WSGI Server
- 第一次路由:PasteDeploy
- PasteDeploy 使用說明
- webob.dec.wsgify 裝飾器
- 第二次路由:中介軟體 routes 路由
01. WSGI 是什麼,因何而生?
WSGI是 Web Server Gateway Interface 的縮寫。
它是 Python應用程式(application)或框架(如 Django)和 Web伺服器之間的一種介面,已經被廣泛接受。
它是一種協議,一種規範,其是在 PEP 333提出的,並在PEP 3333進行補充(主要是為了支援 Python3.x)。這個協議旨在解決眾多 web 框架和web server軟體的相容問題。有了WSGI,你不用再因為你使用的web 框架而去選擇特定的 web server軟體。
常見的web應用框架有:Django,Flask等
常用的web伺服器軟體有:uWSGI,Gunicorn等
那這個 WSGI 協議內容是什麼呢?知乎上有人將 PEP 3333 翻譯成中文,寫得非常好,我將這段協議的內容搬運過來。
WSGI 介面有服務端和應用端兩部分,服務端也可以叫閘道器端,應用端也叫框架端。服務端呼叫一個由應用端提供的可呼叫物件。如何提供這個物件,由服務端決定。例如某些伺服器或者閘道器需要應用的部署者寫一段指令碼,以建立伺服器或者閘道器的例項,並且為這個例項提供一個應用例項。另一些伺服器或者閘道器則可能使用配置檔案或其他方法以指定應用例項應該從哪裡匯入或獲取。
WSGI 對於 application 物件有如下三點要求
- 必須是一個可呼叫的物件
- 接收兩個必選引數environ、start_response。
- 返回值必須是可迭代物件,用來表示http body。
02. 為什麼要有WSGI?
這是來自評論區的一個問題,我覺得問得很好,所以來答一下,更新在這裡。
請教下用wsgi協議的地方為何不直接用http?為什麼要翻譯一次?
以下是我的回答,個人理解,僅供交流。
web框架(即app)在生產中一般不用於直接接收http請求。
你可能會說,django不就可以直接接收http請求嗎,也不需要uwsgi之類的所謂的伺服器。
其實不是,django只是在其內部自己實現了一個簡易的web伺服器,以供開發除錯之用。所以初學者往往會誤以為,web app框架本身就可以接收http請求。
web 伺服器 和 web 框架,分工不同,職責不同(web 伺服器專注於接收並解析請求以呼叫的方式將請求的內容傳web框架),缺一不可,可以說它們是兩個元件,共同協作才能實現web網頁的訪問,既然是兩個元件,那總要定義一些約定俗成的通訊協議,而這就是WSGI,所以必須有WSGI。
那接下來,就引出另一個問題了:如果它們不分開,而將二者整合在一起,對外只有一個元件,是不是就沒有WSGI什麼事了?
答案,是的。
但是你也可以發現目前市場上有相當多的大大小小的web開發框架,如果每個框架都去自己實現web伺服器,那豈不是重複造輪子?
最好的情況應該是,由專業的團隊去開發專業的web伺服器,而開發出來的web伺服器需要具備框架通用性,Django可以用,Flask也可以用,開發者也可以自由選擇用哪個web 伺服器軟體,用哪個web 框架,靈活組合。
03. HTTP請求是如何到應用程式的?
當客戶端發出一個 HTTP 請求後,是如何轉到我們的應用程式處理並返回的呢?
關於這個過程,細節的點這裡沒法細講,只能講個大概。
我根據其架構組成的不同將這個過程的實現分為兩種:
1、兩級結構在這種結構裡,uWSGI作為伺服器,它用到了HTTP協議以及wsgi協議,flask應用作為application,實現了wsgi協議。當有客戶端發來請求,uWSGI接受請求,呼叫flask app得到相應,之後相應給客戶端。 這裡說一點,通常來說,Flask等web框架會自己附帶一個wsgi伺服器(這就是flask應用可以直接啟動的原因),但是這只是在開發階段用到的,在生產環境是不夠用的,所以用到了uwsgi這個效能高的wsgi伺服器。
2、三級結構這種結構裡,uWSGI作為中介軟體,它用到了uwsgi協議(與nginx通訊),wsgi協議(呼叫Flask app)。當有客戶端發來請求,nginx先做處理(靜態資源是nginx的強項),無法處理的請求(uWSGI),最後的相應也是nginx回覆給客戶端的。 多了一層反向代理有什麼好處?
提高web server效能(uWSGI處理靜態資源不如nginx;nginx會在收到一個完整的http請求後再轉發給wWSGI)
nginx可以做負載均衡(前提是有多個伺服器),保護了實際的web伺服器(客戶端是和nginx互動而不是uWSGI)
04. 實現一個簡單的 WSGI Server
在上面的架構圖裡,不知道你發現沒有,有個庫叫做wsgiref
,它是 Python 自帶的一個 wsgi 伺服器模組。
從其名字上就看出,它是用純Python編寫的WSGI伺服器的參考實現。所謂“參考實現”是指該實現完全符合WSGI標準,但是不考慮任何執行效率,僅供開發和測試使用。
有了 wsgiref 這個模組,你就可以很快速的啟動一個wsgi server。
from wsgiref.simple_server import make_server
# 這裡的 appclass 暫且不說,後面會講到
app = appclass()
server = make_server('', 64570, app)
server.serve_forever()
當你執行這段程式碼後,就會開啟一個 wsgi server,監聽0.0.0.0:64570
,並接收請求。
使用 lsof 命令可以查到確實開啟了這個埠
以上使用 wsgiref 寫了一個demo,讓你對wsgi有個初步的瞭解。其由於只適合在學習測試使用,在生產環境中應該另尋他道。
05. 實現“高併發”的 WSGI Server
上面我們說不能在生產中使用 wsgiref ,那在生產中應該使用什麼呢?選擇有挺多的,比如優秀的 uWSGI,Gunicorn等。但是今天我並不準備講這些,一是因為我不怎麼熟悉,二是因為我本人從事 OpenStack 的二次開發,對它比較熟悉。
所以下面,是我花了幾天時間閱讀 OpenStack 中的 Nova 元件程式碼的實現,剛好可以拿過來學習記錄一下,若有理解偏差,還望你批評指出。
在 nova 元件裡有不少服務,比如 nova-api,nova-compute,nova-conductor,nova-scheduler 等等。
其中,只有 nova-api 有對外開啟 http 介面。
要了解這個http 介面是如何實現的,從服務啟動入口開始看程式碼,肯定能找到一些線索。
從 Service 檔案可以得知 nova-api 的入口是nova.cmd.api:main()
開啟nova.cmd.api:main()
,一起看看是 OpenStack Nova 的程式碼。
在如下的黃框裡,可以看到在這裡使用了service.WSGIService 啟動了一個 server,就是我們所說的的 wsgi server
那這裡的 WSGI Server 是依靠什麼實現的呢?讓我們繼續深入原始碼。
wsgi.py 可以看到這裡使用了 eventlet 這個網路併發框架,它先開啟了一個綠色執行緒池,從配置裡可以看到這個伺服器可以接收的請求併發量是 1000 。
可是我們還沒有看到 WSGI Server 的身影,上面使用eventlet 開啟了執行緒池,那執行緒池裡的每個執行緒應該都是一個伺服器吧?它是如何接收請求的?
再繼續往下,可以發現,每個執行緒都是使用 eventlet.wsgi.server 開啟的 WSGI Server,還是使用的 eventlet。
由於原始碼比較多,我提取了主要的程式碼,精簡如下
# 建立綠色執行緒池
self._pool = eventlet.GreenPool(self.pool_size)
# 建立 socket:監聽的ip,埠
bind_addr = (host, port)
self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
dup_socket = self._socket.dup()
# 整理孵化協程所需的各項引數
wsgi_kwargs = {
'func': eventlet.wsgi.server,
'sock': dup_socket,
'site': self.app, # 這個就是 wsgi 的 application 函式
'protocol': self._protocol,
'custom_pool': self._pool,
'log': self._logger,
'log_format': CONF.wsgi.wsgi_log_format,
'debug': False,
'keepalive': CONF.wsgi.keep_alive,
'socket_timeout': self.client_socket_timeout
}
# 孵化協程
self._server = utils.spawn(**wsgi_kwargs)
就這樣,nova 開啟了一個可以接受1000個綠色協程併發的 WSGI Server。
06. 第一次路由:PasteDeploy
上面我們提到 WSGI Server 的建立要傳入一個 Application,用來處理接收到的請求,對於一個有多個 app 的專案。
比如,你有一個個人網站提供瞭如下幾個模組
/blog # 部落格 app
/wiki # wiki app
如何根據 請求的url 地址,將請求轉發到對應的application上呢?
答案是,使用 PasteDeploy 這個庫(在 OpenStack 中各元件被廣泛使用)。
PasteDeploy 到底是做什麼的呢?
根據官方文件的說明,翻譯如下
PasteDeploy 是用來尋找和配置WSGI應用和服務的系統。PasteDeploy給開發者提供了一個簡單的函式loadapp。通過這個函式,可以從一個配置檔案或者Python egg中載入一個WSGI應用。
使用PasteDeploy的其中一個重要意義在於,系統管理員可以安裝和管理WSGI應用,而無需掌握與Python和WSGI相關知識。
由於 PasteDeploy 原來是屬於 Paste 的,現在獨立出來了,但是安裝的時候還是會安裝到paste目錄(site-packages\paste\deploy)下。
我會先講下在 Nova 中,是如何藉助 PasteDeploy 實現對url的路由轉發。
還記得在上面建立WSGI Server的時候,傳入了一個 self.app 引數,這個app並不是一個固定的app,而是使用域名買賣平臺地圖 PasteDeploy 中提供的 loadapp 函式從 paste.ini 配置檔案中載入application。
具體可以,看下nova的實現。
通過列印的 DEBUG 內容得知 config_url 和 app name 的值
app: osapi_compute
config_url: /etc/nova/api-paste.inia
通過檢視/etc/nova/api-paste.ini
,在 composite 段裡找到了osapi_compute
這個app(這裡的app和wsgi app 是兩個概念,需要注意區分) ,可以看出 nova 目前有兩個版本的api,一個是 v2,一個是v2.1,目前我們在用的是 v2.1,從配置檔案中,可以得到其指定的 application 的路徑是nova.api.openstack.compute
這個模組下的 APIRouterV21 類 的factory方法,這是一個工廠函式,返回 APIRouterV21 例項。
[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v2: openstack_compute_api_v21_legacy_v2_compatible
/v2.1: openstack_compute_api_v21
[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory
這是 OpenStack 使用 PasteDeploy 實現的第一層的路由,如果你不感興趣,可以直接略過本節,進入下一節,下一節是 介紹 PasteDeploy 的使用,教你實現一個簡易的web server demo。推薦一定要看。
07. PasteDeploy 使用說明
到上一步,我已經得到了 application 的有用的線索。考慮到很多人是第一次接觸 PasteDeploy,所以這裡結合網上部落格做了下總結。對你入門會有幫助。
掌握 PasteDeploy ,你只要按照以下三個步驟逐個完成即可。
1、配置 PasteDeploy使用的ini檔案;
2、定義WSGI應用;
3、通過loadapp函式載入WSGI應用;
第一步:寫 paste.ini 檔案
在寫之前,咱得知道 ini 檔案的格式吧。
首先,像下面這樣一個段叫做section
。
[type:name]
key = value
...
其上的type,主要有如下幾種
composite
(組合):多個app的路由分發;
ini [composite:main] use = egg:Paste#urlmap / = home /blog = blog /wiki = wiki
- app(應用):指明 WSGI 應用的路徑;
ini [app:home] paste.app_factory = example:Home.factory
- pipeline(管道):給一個 app 繫結多個過濾器。將多個filter和最後一個WSGI應用串聯起來。
```ini [pipeline:main] pipeline = filter1 filter2 filter3 myapp
[filter:filter1] ...
[filter:filter2] ...
[app:myapp] ... ```
- filter(過濾器):以 app 做為唯一引數的函式,並返回一個“過濾”後的app。通過鍵值next可以指定需要將請求傳遞給誰。next指定的可以是一個普通的WSGI應用,也可以是另一個過濾器。雖然名稱上是過濾器,但是功能上不侷限於過濾功能,可以是其它功能,例如日誌功能,即將認為重要的請求資料記錄下來。
```ini [app-filter:filter_name] use = egg:... next = next_app
[app:next_app] ... ```
對 ini 檔案有了一定的瞭解後,就可以看懂下面這個 ini 配置檔案了
[composite:main]
use = egg:Paste#urlmap
/blog = blog
/wiki = wiki
[app:blog]
paste.app_factory = example:Blog.factory
[app:wiki]
paste.app_factory = example:Wiki.factory
第二步是定義一個符合 WSGI 規範的 applicaiton 物件。
符合 WSGI 規範的 application 物件,可以有多種形式,函式,方法,類,例項物件。這裡僅以例項物件為例(需要實現__call__
方法),做一個演示。
import os
from paste import deploy
from wsgiref.simple_server import make_server
class Blog(object):
def __init__(self):
print("Init Blog.")
def __call__(self, environ, start_response):
status_code = "200 OK"
response_headers = [("Content-Type", "text/plain")]
response_body = "This is Blog's response body.".encode('utf-8')
start_response(status_code, response_headers)
return [response_body]
@classmethod
def factory(cls, global_conf, **kwargs):
print("Blog factory.")
return Blog()
最後,第三步是使用 loadapp 函式載入 WSGI 應用。
loadapp 是 PasteDeploy 提供的一個函式,使用它可以很方便地從第一步的ini配置檔案里加載 app
loadapp 函式可以接收兩個實參:
- URI:"config:<配置檔案的全路徑>"
- name:WSGI應用的名稱
conf_path = os.path.abspath('paste.ini')
# 載入 app
applications = deploy.loadapp("config:{}".format(conf_path) , "main")
# 啟動 server, 監聽 localhost:22800
server = make_server("localhost", "22800", applications)
server.serve_forever()
applications 是URLMap 物件。
完善並整合第二步和第三步的內容,寫成一個 Python 檔案(wsgi_server.py)。內容如下
import os
from paste import deploy
from wsgiref.simple_server import make_server
class Blog(object):
def __init__(self):
print("Init Blog.")
def __call__(self, environ, start_response):
status_code = "200 OK"
response_headers = [("Content-Type", "text/plain")]
response_body = "This is Blog's response body.".encode('utf-8')
start_response(status_code, response_headers)
return [response_body]
@classmethod
def factory(cls, global_conf, **kwargs):
print("Blog factory.")
return Blog()
class Wiki(object):
def __init__(self):
print("Init Wiki.")
def __call__(self, environ, start_response):
status_code = "200 OK"
response_headers = [("Content-Type", "text/plain")]
response_body = "This is Wiki's response body.".