Python Web 開發中,服務端程序能夠分爲兩個部分,一是服務器程序,二是應用程序。前者負責把客戶端請求接收,整理,後者負責具體的邏輯處理。爲了方便應用程序的開 發,咱們把經常使用的功能封裝起來,成爲各類Web開發框架,例如 Django, Flask, Tornado。不一樣的框架有不一樣的開發方式,可是不管如何,開發出的應用程序都要和服務器程序配合,才能爲用戶提供服務。這樣,服務器程序就須要爲不一樣 的框架提供不一樣的支持。這樣混亂的局面不管對於服務器仍是框架,都是很差的。對服務器來講,須要支持各類不一樣框架,對框架來講,只有支持它的服務器才能被 開發出的應用使用。html
這時候,標準化就變得尤其重要。咱們能夠設立一個標準,只要服務器程序支持這個標準,框架也支持這個標準,那麼他們就能夠配合使用。一旦標準肯定,雙方各自實現。這樣,服務器能夠支持更多支持標準的框架,框架也可使用更多支持標準的服務器。python
Python Web開發中,這個標準就是 The Web Server Gateway Interface, 即 WSGI. 這個標準在PEP 333中描述,後來,爲了支持 Python 3.x, 而且修正一些問題,新的版本在PEP 3333中描述。git
WSGI 是服務器程序與應用程序的一個約定,它規定了雙方各自須要實現什麼接口,提供什麼功能,以便兩者可以配合使用。github
WSGI 不能規定的太複雜,不然對已有的服務器來講,實現起來會困難,不利於WSGI的普及。同時WSGI也不能規定的太多,例如cookie處理就沒有在 WSGI中規定,這是爲了給框架最大的靈活性。要知道WSGI最終的目的是爲了方便服務器與應用程序配合使用,而不是成爲一個Web框架的標準。web
另外一方面,WSGI須要使得middleware(是中間件麼?)易於實現。middleware處於服務器程序與應用程序之間,對服務器程序來講,它相 當於應用程序,對應用程序來講,它至關於服務器程序。這樣,對用戶請求的處理,能夠變成多個 middleware 疊加在一塊兒,每一個middleware實現不一樣的功能。請求從服務器來的時候,依次經過middleware,響應從應用程序返回的時候,反向經過層層 middleware。咱們能夠方便地添加,替換middleware,以便對用戶請求做出不一樣的處理。ubuntu
WSGI主要是對應用程序與服務器端的一些規定,因此,它的主要內容就分爲兩個部分。瀏覽器
WSGI規定:服務器
1. 應用程序須要是一個可調用的對象
在Python中:cookie
能夠是函數數據結構
能夠是一個實例,它的類實現了__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對象,一個是函數。它們到底有什麼用呢?這都不是咱們如今應該關心的,如今只須要知道,服務器程序大概作了什麼事情就行了,後面,咱們會深刻討論這些細節。
另外,有些功能可能介於服務器程序和應用程序之間,例如,服務器拿到了客戶端請求的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規定 必須知足 的。
應用程序是可調用對象
可調用對象有兩個位置參數
所謂位置參數就是調用的時候,依靠位置來肯定參數的語義,而不是參數名,也就是說服務 器調用應用程序時,應該是這樣:
application(env, start_response)
而不是這樣:
application(start_response=start_response, environ=env)
因此,參數名實際上是能夠隨便起的,只不過爲了表義清楚,咱們起了environ
和 start_response
。
第一個參數environ是Python內置的dict對象,應用程序能夠對這個參數任意修改。
environ參數必須包含 WSGI 須要的一些變量(詳見後文)
也能夠包含一些擴展參數,命名規範見後文
start_response參數是一個可調用對象。接受兩個位置參數,一個可選參數。 例如:start_response(status, response_headers, exc_info=None)
status參數是狀態碼,例如 200 OK
。
response_headers參數是一個列表,列表項的形式爲(header_name, header_value)。
exc_info參數在錯誤處理的時候使用。
status和response_headers的具體內容能夠參考 HTTP 協議 Response部分。
start_response必須返回一個可調用對象: write(body_data)
應用程序必須返回一個可迭代對象。
應用程序不該假設返回的可迭代對象被遍歷至終止,由於遍歷過程可能出現錯誤。
應用程序必須在第一次返回可迭代數據以前調用 start_response 方法。
這是由於可迭代數據是 返回數據的 body
部分,在它返回以前,須要使用 start_response
返回 response_headers 數據。
服務器必須將可迭代對象的內容傳遞給客戶端,可迭代對象會產生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 變量須要包含 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的形式。
服務器程序提供的輸入流及錯誤流必須包含如下方法:
read(size)
readline()
readlines(hint)
iter()
flush()
write()
writelines(seq)
應用程序使用輸入流對象及錯誤流對象時,只能使用這些方法,禁止使用其它方法,特別是, 禁止應用程序關閉這些流。
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,那麼服務器程序傳遞的數據大小不該該超過 Content-Length,當發送了足夠的數據後,應該中止迭代,或者 raise 一個 error。固然,若是應用程序返回的數據大小沒有它指定的Content-Length那麼多,那麼服務器程序應該關閉鏈接,使用Log記錄,或者報告 錯誤。
若是應用程序不支持Content-Length,那麼服務器程序應該選擇一種方法處理這種狀況。最簡單的方法就是當響應完成後,關閉與客戶端的鏈接。
通常狀況下,應用程序會把須要返回的數據放在緩衝區裏,而後一次性發送出去。以前說的應用程序會返回一個可迭代對象,多數狀況下,這個可迭代對象,都只有 一個元素,這個元素包含了HTML內容。可是在有些狀況下,數據太大了,沒法一次性在內存中存儲這些數據,因此就須要作成一個可迭代對象,每次迭代只發送 一塊數據。
禁止服務器程序延遲任何一塊數據的傳送,要麼把一塊數據徹底傳遞給客戶端,要麼保證在產生下一塊數據時,繼續傳遞這一塊數據。
若是 middleware調用的應用程序產生了數據,那麼middleware至少要產生一個數據,即便它想等數據積累到必定程度再返回,它也須要產生一個空 的bytestring。 注意,這也意味着只要middleware調用的應用程序產生了一個可迭代對象,middleware也必須返回一個可迭代對象。 同時,禁止middleware使用可調用對象write傳遞數據,write是middleware調用的應用程序使用的。
一些已經存在的應用程序框架使用了write函數或方法傳遞數據,而且沒有使用緩衝區。不幸的是,根據WSGI中的要求,應用程序須要返回可迭代對象,這 樣就沒法實現這些API,爲了容許這些API 繼續使用,WSGI要求 start_response 返回一個 write 可調用對象,這樣應用程序就能使用這個 write 了。
可是,若是能避免使用這個 write,最好避免使用,這是爲兼容之前的應用程序而設計的。這個write的參數是HTTP response body的一部分,這意味着在write()返回前,必須保證傳給它的數據已經徹底被傳送了,或者已經放在緩衝區了。
應用程序必須返回一個可迭代對象,即便它使用write產生HTTP response body。
這裏能夠發現,有兩中傳遞數據的方式,一種是直接使用write傳遞,一種是應用程序返回可迭代對象後,再將這個可迭代對象傳遞,若是同時使用這兩種方式,前者的數據必須在後者以前傳遞。
HTTP 不支持 Unicode, 全部編碼/解碼都必須由應用程序完成,全部傳遞給或者來自server的字符串都必須是 str
或者bytes
類型,而不是unicode
。
注意傳遞給start_response的數據,其編碼都必須遵循 RFC 2616, 即便用 ISO-8859-1 或者 RFC 2047 MIME 編碼。
WSGI 中聽說的 bytestrings
, 在Python3中指 bytes
,在之前的Python版本中,指 str
。
應用程序應該捕獲它們本身的錯誤,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,那麼它必須提供對 HTTP 1.1 expect/continue
機制的支持。
在 PEP 3333 中,還包含了其它內容,例如:
HTTP 特性
線程支持
實現時須要注意的地方:包括,擴展API,應用程序配置,URL重建等
這裏就不做過多介紹了。
這篇文章主要是我閱讀 PEP 3333 後的理解和記錄,有些地方可能沒有理解正確或者沒有寫全,下面提供一些資源供擴展閱讀。
PEP 3333
不解釋
WSGI org
看起來好像官方網站的樣子,覆蓋了關於WSGI的方方面面,包含學習資源,支持WSGI的框架列表,服務器列表,應用程序列表,middleware和庫等等。
wsgiref
WSGI的參考實現,閱讀源代碼後有利於對WSGI的理解。我在GitHub上有本身閱讀後的註釋版本,而且做了一些圖,有須要能夠看這裏:wsgiref 源代碼閱讀
另外,還有一些文章介紹了一些基本概念和一些有用的實例,很是不錯。