WSGI 簡介
背景
Python Web 開發中,服務端程式可以分為兩個部分,一是伺服器程式,二是應用程式。前者負責把用戶端請求接收,整理,後者負責具體的邏輯處理。為了友善應用程式的開發,我們把常用的功能封裝起來,成為各種Web開發架構,例如 Django, Flask, Tornado。不同的架構有不同的開發方式,但是無論如何,開發出的應用程式都要和伺服器程式配合,才能為使用者提供服務。這樣,伺服器程式就需要為不同的架構提供不同的支援。這樣混亂的局面無論對于伺服器還是架構,都是不好的。對伺服器來說,需要支援各種不同架構,對架構來說,隻有支援它的伺服器才能被開發出的應用使用。
這時候,标準化就變得尤為重要。我們可以設立一個标準,隻要伺服器程式支援這個标準,架構也支援這個标準,那麼他們就可以配合使用。一旦标準确定,雙方各自實作。這樣,伺服器可以支援更多支援标準的架構,架構也可以使用更多支援标準的伺服器。
Python Web開發中,這個标準就是 The Web Server Gateway Interface, 即 WSGI. 這個标準在PEP 333中描述,後來,為了支援 Python 3.x, 并且修正一些問題,新的版本在PEP 3333中描述。
WSGI 是什麼
WSGI 是伺服器程式與應用程式的一個約定,它規定了雙方各自需要實作什麼接口,提供什麼功能,以便二者能夠配合使用。
WSGI 不能規定的太複雜,否則對已有的伺服器來說,實作起來會困難,不利于WSGI的普及。同時WSGI也不能規定的太多,例如cookie處理就沒有在WSGI中規定,這是為了給架構最大的靈活性。要知道WSGI最終的目的是為了友善伺服器與應用程式配合使用,而不是成為一個Web架構的标準。
另一方面,WSGI需要使得middleware(是中間件麼?)易于實作。middleware處于伺服器程式與應用程式之間,對伺服器程式來說,它相當于應用程式,對應用程式來說,它相當于伺服器程式。這樣,對使用者請求的處理,可以變成多個 middleware 疊加在一起,每個middleware實作不同的功能。請求從伺服器來的時候,依次通過middleware,響應從應用程式傳回的時候,反向通過層層middleware。我們可以友善地添加,替換middleware,以便對使用者請求作出不同的處理。
WSGI 内容概要
WSGI主要是對應用程式與伺服器端的一些規定,是以,它的主要内容就分為兩個部分。
應用程式
WSGI規定:
1. 應用程式需要是一個可調用的對象
在Python中:
- 可以是函數
- 可以是一個執行個體,它的類實作了
方法__call__
- 可以是一個類,這時候,用這個類生成執行個體的過程就相當于調用這個類
同時,WSGI規定:
2. 可調用對象接收兩個參數
這樣,如果這個對象是函數的話,它看起來要是這個樣子:
# callable function
def application(environ, start_response):
pass
如果這個對象是一個類的話,它看起來是這個樣子:
# callable class
class Application:
def __init__(self, environ, start_response):
pass
如果這個對象是一個類的執行個體,那麼,這個類看起來是這個樣子:
# callable object
class ApplicationObj:
def __call__(self, environ, start_response):
pass
最後,WSGI還規定:
3.可調用對象要傳回一個值,這個值是可疊代的。
這樣的話,前面的三個例子就變成:
HELLO_WORLD = b"Hello world!\n"
# callable function
def application(environ, start_response):
return [HELLO_WORLD]
# callable class
class Application:
def __init__(self, environ, start_response):
pass
def __iter__(self):
yield HELLO_WORLD
# callable object
class ApplicationObj:
def __call__(self, environ, start_response):
return [HELLO_WORLD]
你可能會說,不是啊,我們平時寫的web程式不是這樣啊。 比如如果使用web.py架構的話,一個典型的應用可能是這樣的:
class hello:
def GET(self):
return 'Hello, world!'
這是由于架構已經把WSGI中規定的一些東西封裝起來了,我們平時用架構時,看不到這些東西,隻需要直接實作我們的邏輯,再傳回一個值就好了。其它的東西架構幫我們做好了。這也是架構的價值所在,把常用的東西封裝起來,讓使用者隻需要關注最重要的東西。
當然,WSGI關于應用程式的規定不隻這些,但是現在,我們隻需要知道這些就足夠了。下面,再介紹伺服器程式。
伺服器程式
伺服器程式會在每次用戶端的請求傳來時,調用我們寫好的應用程式,并将處理好的結果傳回給用戶端。
WSGI規定:
4.伺服器程式需要調用應用程式
伺服器程式看起來大概是這個樣子的:
def run(application):
environ = {}
def start_response(status, response_headers, exc_info=None):
pass
result = application(environ, start_response)
def write(data):
pass
for data in result:
write(data)
這裡可以看出伺服器程式是如何與應用程式配合完成使用者請求的。
WSGI規定了應用程式需要一個可調用對象,有兩個參數,傳回一個可疊代對象。在伺服器 程式中,針對這幾個規定,做了以下幾件事:
- 把應用程式需要的兩個參數設定好
- 調用應用程式
- 疊代通路應用程式的傳回結果,并将其傳回用戶端
你可以從中發現,應用程式需要的兩個參數,一個是一個dict對象,一個是函數。它們到底有什麼用呢?這都不是我們現在應該關心的,現在隻需要知道,伺服器程式大概做了什麼事情就好了,後面,我們會深入讨論這些細節。
middleware
另外,有些功能可能介于伺服器程式和應用程式之間,例如,伺服器拿到了用戶端請求的URL, 不同的URL需要交由不同的函數處理,這個功能叫做 URL Routing,這個功能就可以放在二者中間實作,這個中間層就是 middleware。
middleware對伺服器程式和應用是透明的,也就是說,伺服器程式以為它就是應用程式,而應用程式以為它就是伺服器。這就告訴我們,middleware需要把自己僞裝成一個伺服器,接受應用程式,調用它,同時middleware還需要把自己僞裝成一個應用程式,傳給伺服器程式。
其實無論是伺服器程式,middleware 還是應用程式,都在服務端,為用戶端提供服務,之是以把他們抽象成不同層,就是為了控制複雜度,使得每一次都不太複雜,各司其職。
下面,我們看看middleware大概是什麼樣子的。
# URL Routing middleware
def urlrouting(url_app_mapping):
def midware_app(environ, start_response):
url = environ['PATH_INFO']
app = url_app_mapping[url]
result = app(environ, start_response)
return result
return midware_app
函數
midware_app
就是一個簡單的middleware:對伺服器而言,它是一個應用程式,是一個可調用對象, 有兩個參數,傳回一個可調用對象。對應用程式而言,它是一個伺服器,為應用程式提供了參數,并且調用了應用程式。
另外,這裡的
urlrouting
函數,相當于一個函數生成器,你給它不同的 url-app 映射關系,它會生成相應的具有 url routing功能的 middleware。
如果你僅僅想簡單了解一下WSGI是什麼,相信到這裡,你差不多明白了,下面會介紹WSGI的細節,這些細節來自 PEP3333, 如果沒有興趣,到這裡 可以停止了。
WSGI詳解
注意:以 點 開始的解釋是WSGI規定 必須滿足 的。
應用程式
- 應用程式是可調用對象
-
可調用對象有兩個位置參數
所謂位置參數就是調用的時候,依靠位置來确定參數的語義,而不是參數名,也就是說服務 器調用應用程式時,應該是這樣:
application(env, start_response)
而不是這樣:
application(start_response=start_response, environ=env)
是以,參數名其實是可以随便起的,隻不過為了表義清楚,我們起了
environ
和
start_response
。
- 第一個參數environ是Python内置的dict對象,應用程式可以對這個參數任意修改。
-
environ參數必須包含 WSGI 需要的一些變量(詳見後文)
也可以包含一些擴充參數,命名規範見後文
- start_response參數是一個可調用對象。接受兩個位置參數,一個可選參數。 例如:
status參數是狀态碼,例如start_response(status, response_headers, exc_info=None)
。200 OK
response_headers參數是一個清單,清單項的形式為(header_name, header_value)。
exc_info參數在錯誤處理的時候使用。
status和response_headers的具體内容可以參考 HTTP 協定 Response部分。
- start_response必須傳回一個可調用對象:
write(body_data)
- 應用程式必須傳回一個可疊代對象。
- 應用程式不應假設傳回的可疊代對象被周遊至終止,因為周遊過程可能出現錯誤。
-
應用程式必須在第一次傳回可疊代資料之前調用 start_response 方法。
這是因為可疊代資料是 傳回資料的
部分,在它傳回之前,需要使用body
傳回 response_headers 資料。start_response
伺服器程式
- 伺服器必須将可疊代對象的内容傳遞給用戶端,可疊代對象會産生bytestrings,必須完全完成每個bytestring後才能請求下一個。
- 假設result 為應用程式的傳回的可疊代對象。如果len(result) 調用成功,那麼result必須是可累積的。
- 如果result有
方法,那麼每次完成對請求的處理時,必須調用它,無論這次請求正常完成,還是遇到了錯誤。close
- 伺服器程式禁止使用可疊代對象的其它屬性,除非這個可疊代對象是一個特殊類的執行個體,這個類會被
定義。wsgi.file_wrapper
根據上述内容,我們的伺服器程式看起來會是這個樣子:
def run(application):
environ = {}
# set environ
def write(data):
pass
def start_response(status, response_headers, exc_info=None):
return write
try:
result = application(environ, start_response)
finally:
if hasattr(result, 'close'):
result.close()
if hasattr(result, '__len__'):
# result must be accumulated
pass
for data in result:
write(data)
應用程式看起來是這個樣子:
HELLO_WORLD = b"Hello world!\n"
# callable function
def application(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]
下面我們再詳細介紹之前提到的一些資料結構
environ 變量
environ 變量需要包含 CGI 環境變量,它們在The Common Gateway Interface Specification 中定義,下面列出的變量必須包含在 enciron變量中:
-
REQUEST_METHOD
HTTP 請求方法,例如 "GET", "POST"
-
SCRIPT_NAME
URL 路徑的起始部分對應的應用程式對象,如果應用程式對象對應伺服器的根,那麼這個值可以為空字元串
-
PATH_INFO
URL 路徑除了起始部分後的剩餘部分,用于找到相應的應用程式對象,如果請求的路徑就是根路徑,這個值為空字元串
-
QUERY_STRING
URL路徑中
後面的部分?
-
CONTENT_TYPE
HTTP 請求中的
部分Content-Type
-
CONTENT_LENGTH
HTTP 請求中的
部分Content-Lengh
-
SERVER_NAME, SERVER_PORT
與 SCRIPT_NAME,PATH_INFO 共同構成完整的 URL,它們永遠不會為空。但是,如果 HTTP_HOST 存在的話,當建構 URL 時, HTTP_HOST優先于SERVER_NAME。
-
SERVER_PROTOCOL
用戶端使用的協定,例如 "HTTP/1.0", "HTTP/1.1", 它決定了如何處理 HTTP 請求的頭部。這個名字其實應該叫
REQUEST_PROTOCOL
,因為它表示的是用戶端請求的協定,而不是服務端響應的協定。但是為了和CGI相容,我們隻好叫這個名字了。 *HTTP_ Variables
這個是一個系列的變量名,都以
開頭,對應用戶端支援的HTTP請求的頭部資訊。HTTP
WSGI 有一個參考實作,叫 wsgiref,裡面有一個示例,我們這裡引用這個示例的結果,展現一下這些變量,以便有一個直覺的體會,這個示例通路的 URL 為
http://localhost:8000/xyz?abc
上面提到的變量值為:
REQUEST_METHOD = 'GET'
SCRIPT_NAME = ''
PATH_INFO = '/xyz'
QUERY_STRING = 'abc'
CONTENT_TYPE = 'text/plain'
CONTENT_LENGTH = ''
SERVER_NAME = 'minix-ubuntu-desktop'
SERVER_PORT = '8000'
SERVER_PROTOCOL = 'HTTP/1.1'
HTTP_ACCEPT = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
HTTP_ACCEPT_ENCODING = 'gzip,deflate,sdch'
HTTP_ACCEPT_LANGUAGE = 'en-US,en;q=0.8,zh;q=0.6,zh-CN;q=0.4,zh-TW;q=0.2'
HTTP_CONNECTION = 'keep-alive'
HTTP_HOST = 'localhost:8000'
HTTP_USER_AGENT = 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36'
另外,伺服器還應該(非必須)提供盡可能多的CGI變量,如果支援SSL的話,還應該提供Apache SSL 環境變量。
伺服器程式應該在文檔中對它提供的變量進行說明,應用程式應該檢查它需要的變量是否存在。
除了 CGI 定義的變量外,伺服器程式還可以包含和作業系統相關的環境變量,但這并非必須。
但是,下面列出的這些 WSGI 相關的變量必須要包含:
-
wsgi.version
值的形式為 (1, 0) 表示 WSGI 版本 1.0
-
wsgi.url_scheme
表示 url 的模式,例如 "https" 還是 "http"
-
wsgi.input
輸入流,HTTP請求的 body 部分可以從這裡讀取
-
wsgi.erros
輸出流,如果出現錯誤,可以寫往這裡
-
wsgi.multithread
如果應用程式對象可以被同一程序中的另一線程同時調用,這個值為True
-
wsgi.multiprocess
如果應用程式對象可以同時被另一個程序調用,這個值為True
-
wsgi.run_once
如果伺服器希望應用程式對象在包含它的程序中隻被調用一次,那麼這個值為True
這些值在 wsgiref示例中的值為:
wsgi.errors = <open file '<stderr>', mode 'w' at 0xb735f0d0>
wsgi.file_wrapper = <class wsgiref.util.FileWrapper at 0xb70525fc>
wsgi.input = <socket._fileobject object at 0xb7050e6c>
wsgi.multiprocess = False
wsgi.multithread = True
wsgi.run_once = False
wsgi.url_scheme = 'http'
wsgi.version = (1, 0)
另外,environ中還可以包含伺服器自己定義的一些變量,這些變量應該隻包含
- 小寫字母
- 數字
- 點
- 下劃線
- 獨立的字首
例如,mod_python定義的變量名應該為mod_python.var_name的形式。
輸入流及錯誤流(Input and Error Streams)
伺服器程式提供的輸入流及錯誤流必須包含以下方法:
- read(size)
- readline()
- readlines(hint)
- iter()
- flush()
- write()
- writelines(seq)
應用程式使用輸入流對象及錯誤流對象時,隻能使用這些方法,禁止使用其它方法,特别是, 禁止應用程式關閉這些流。
start_response()
start_response是HTTP響應的開始,它的形式為:
start_response(status, response_headers, exc_info=None)
傳回一個可調用對象,這個可調用對象形式為:
write(body_data)
status 表示 HTTP 狀态碼,例如 "200 OK", "404 Not Found",它們在 RFC 2616中定義,status禁止包含控制字元。
response_headers 是一個清單,清單項是一個二進制組: (header_name, heaer_value) , 每個 header_name 都必須是 RFC 2616 4.2 節中定義的HTTP 頭部名。header_value 禁止包含控制字元。
另外,伺服器程式必須保證正确的headers 被傳回給用戶端,如果應用程式沒有傳回headers,伺服器必須添加它。
應用程式和middleware禁止使用 HTTP/1.1 中的 "hop-by-hop"特性,以及其它可能影響用戶端與伺服器永久連接配接的特性。
start_response 被調用時,伺服器應該檢查 headers 中的錯誤,另外,禁止 start_response直接将 response_headers傳遞給用戶端,它必須把它們存儲起來,一直到應用程式第一次疊代傳回一個非空資料後,才能将response_headers傳遞給用戶端。這其實是在說,HTTP響應body部分必須有資料,不能隻傳回一個header。
start_response的第三個參數是一個可選參數,exc_info,它必須和Python的 sys.exc_info()傳回的資料有相同類型。當處理請求的過程遇到錯誤時,這個參數會被設定,同時調用 start_response。如果提供了exc_info,但是HTTP headers 還沒有輸出,那麼 start_response需要将目前存儲的 HTTP response headers替換成一個新值。但是,如果提供了exc_info,同時 HTTP headers已經輸出了,那麼 start_response 必須 raise 一個 error。禁止應用程式處理 start_response raise出的 exceptions,應該交給伺服器程式處理。
當且僅當提供 exc_info參數時,start_response才可以被調用多于一次。換句話說,要是沒提供這個參數,start_response在目前應用程式中調用後,禁止再調用。
為了避免循環引用,start_response實作時需要保證 exc_info在函數調用後不再包含引用。 也就是說start_response用完 exc_info後,需要保證執行一句
exc_info = None
這可以通過 try/finally實作。
處理 Content-Length Header
如果應用程式支援 Content-Length,那麼伺服器程式傳遞的資料大小不應該超過 Content-Length,當發送了足夠的資料後,應該停止疊代,或者 raise 一個 error。當然,如果應用程式傳回的資料大小沒有它指定的Content-Length那麼多,那麼伺服器程式應該關閉連接配接,使用Log記錄,或者報告錯誤。
如果應用程式不支援Content-Length,那麼伺服器程式應該選擇一種方法處理這種情況。最簡單的方法就是當響應完成後,關閉與用戶端的連接配接。
緩沖與流(Buffering and Streaming)
一般情況下,應用程式會把需要傳回的資料放在緩沖區裡,然後一次性發送出去。之前說的應用程式會傳回一個可疊代對象,多數情況下,這個可疊代對象,都隻有一個元素,這個元素包含了HTML内容。但是在有些情況下,資料太大了,無法一次性在記憶體中存儲這些資料,是以就需要做成一個可疊代對象,每次疊代隻發送一塊資料。
禁止伺服器程式延遲任何一塊資料的傳送,要麼把一塊資料完全傳遞給用戶端,要麼保證在産生下一塊資料時,繼續傳遞這一塊資料。
middleware 處理資料
如果 middleware調用的應用程式産生了資料,那麼middleware至少要産生一個資料,即使它想等資料積累到一定程度再傳回,它也需要産生一個空的bytestring。 注意,這也意味着隻要middleware調用的應用程式産生了一個可疊代對象,middleware也必須傳回一個可疊代對象。 同時,禁止middleware使用可調用對象write傳遞資料,write是middleware調用的應用程式使用的。
write 可調用對象
一些已經存在的應用程式架構使用了write函數或方法傳遞資料,并且沒有使用緩沖區。不幸的是,根據WSGI中的要求,應用程式需要傳回可疊代對象,這樣就無法實作這些API,為了允許這些API 繼續使用,WSGI要求 start_response 傳回一個 write 可調用對象,這樣應用程式就能使用這個 write 了。
但是,如果能避免使用這個 write,最好避免使用,這是為相容以前的應用程式而設計的。這個write的參數是HTTP response body的一部分,這意味着在write()傳回前,必須保證傳給它的資料已經完全被傳送了,或者已經放在緩沖區了。
應用程式必須傳回一個可疊代對象,即使它使用write産生HTTP response body。
這裡可以發現,有兩中傳遞資料的方式,一種是直接使用write傳遞,一種是應用程式傳回可疊代對象後,再将這個可疊代對象傳遞,如果同時使用這兩種方式,前者的資料必須在後者之前傳遞。
Unicode
HTTP 不支援 Unicode, 所有編碼/解碼都必須由應用程式完成,所有傳遞給或者來自server的字元串都必須是
str
或者
bytes
類型,而不是
unicode
。
注意傳遞給start_response的資料,其編碼都必須遵循 RFC 2616, 即使用 ISO-8859-1 或者 RFC 2047 MIME 編碼。
WSGI 中據說的
bytestrings
, 在Python3中指
bytes
,在以前的Python版本中,指
str
。
錯誤處理(Error Handling)
應用程式應該捕獲它們自己的錯誤,internal erros, 并且将相關錯誤資訊傳回給浏覽器。 WSGI 提供了一種錯誤處理的方式,這就是之前提到的 exc_info參數。下面是 PEP 3333中提供的一段示例:
try:
# regular application code here
status = "200 Froody"
response_headers = [("content-type", "text/plain")]
start_response(status, response_headers)
return ["normal body goes here"]
except:
# XXX should trap runtime issues like MemoryError, KeyboardInterrupt
# in a separate handler before this bare 'except:'...
status = "500 Oops"
response_headers = [("content-type", "text/plain")]
start_response(status, response_headers, sys.exc_info())
return ["error body goes here"]
當出現異常時,start_response的exc_info參數被設定成 sys.exc_info(),這個函數會傳回目前的異常。
HTTP 1.1 Expect/Continue
如果伺服器程式要實作 HTTP 1.1,那麼它必須提供對 HTTP 1.1
expect/continue
機制的支援。
其它内容
在 PEP 3333 中,還包含了其它内容,例如:
- HTTP 特性
- 線程支援
- 實作時需要注意的地方:包括,擴充API,應用程式配置,URL重建等
這裡就不作過多介紹了。
擴充閱讀
這篇文章主要是我閱讀 PEP 3333 後的了解和記錄,有些地方可能沒有了解正确或者沒有寫全,下面提供一些資源供擴充閱讀。
-
PEP 3333
不解釋
-
WSGI org
看起來好像官方網站的樣子,覆寫了關于WSGI的方方面面,包含學習資源,支援WSGI的架構清單,伺服器清單,應用程式清單,middleware和庫等等。
-
wsgiref
WSGI的參考實作,閱讀源代碼後有利于對WSGI的了解。我在GitHub上有自己閱讀後的注釋版本,并且作了一些圖,有需要可以看這裡:wsgiref 源代碼閱讀
另外,還有一些文章介紹了一些基本概念和一些有用的執行個體,非常不錯。
- Wsgi研究
- wsgi初探