天天看點

WSGI 簡介 WSGI 簡介

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參數是一個可調用對象。接受兩個位置參數,一個可選參數。 例如:

    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 變量

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初探