PEP 333 – Python Web Server Gateway Interface v1.0
- 作者:
- Phillip J. Eby <pje at telecommunity.com>
- 討論於:
- Web-SIG list
- 狀態:
- 最終 (Final)
- 類型:
- 資訊類
- 建立日期:
- 2003 年 12 月 07 日
- 公告歷史:
- 2003-12-07, 2004-08-08, 2004-08-20, 2004-08-27, 2010-09-27
- 被以下文件取代:
- 3333
目錄
前言
注意:關於支援 Python 3.x 並包含社群勘誤、附錄和說明的更新版本規範,請參閱 PEP 3333。
摘要
本文件規範了網頁伺服器與 Python 網頁應用程式或框架之間建議的標準介面,旨在促進網頁應用程式在各種網頁伺服器之間的移植性。
原理與目標
Python 目前擁有眾多網頁應用程式框架,例如 Zope、Quixote、Webware、SkunkWeb、PSO 以及 Twisted Web —— 僅舉幾例 [1]。這種多樣化的選擇對 Python 新手來說可能是個問題,因為一般而言,他們對網頁框架的選擇會限制其可用的網頁伺服器,反之亦然。
相比之下,儘管 Java 也有同樣多的網頁應用程式框架,但 Java 的「servlet」API 使得使用任何 Java 網頁框架編寫的應用程式,都能在任何支援 servlet API 的網頁伺服器中執行。
如果在 Python 的網頁伺服器中也能提供並廣泛使用這樣的 API —— 無論這些伺服器是用 Python 編寫的(如 Medusa)、嵌入了 Python(如 mod_python),還是透過閘道協議(如 CGI、FastCGI 等)呼叫 Python —— 都能將框架的選擇與網頁伺服器的選擇解耦,讓使用者能自由選擇適合的組合,同時讓框架和伺服器開發者能專注於各自偏好的專業領域。
因此,本 PEP 提出了網頁伺服器與網頁應用程式或框架之間的一個簡單且通用的介面:Python 網頁伺服器閘道介面(WSGI)。
但僅僅存在 WSGI 規範並不能解決 Python 網頁應用程式伺服器與框架的現狀。伺服器和框架的作者與維護者必須實際實作 WSGI 才能產生影響。
然而,由於目前沒有現有的伺服器或框架支援 WSGI,對於實作 WSGI 支援的作者來說,短期回報很少。因此,WSGI 必須易於實作,以便作者對該介面的初始投資可以保持在較低水準。
因此,介面的伺服器端和框架端實作的簡單性對於 WSGI 介面的實用性至關重要,並且是任何設計決策的首要標準。
但請注意,對框架作者而言的實作簡單性,並不等同於網頁應用程式作者的使用便利性。WSGI 為框架作者提供了一個絕對「簡潔」的介面,因為回應物件和 Cookie 處理等繁瑣功能只會干擾現有框架對這些問題的處理。再次強調,WSGI 的目標是促進現有伺服器與應用程式或框架之間的便捷互連,而不是建立一個新的網頁框架。
另請注意,此目標也排除了 WSGI 要求任何尚未在已發布的 Python 版本中提供的功能。因此,本規範不建議或要求新的標準函式庫模組,且 WSGI 中沒有任何內容要求 Python 版本高於 2.2.2。(然而,對於未來版本的 Python,在標準函式庫提供的網頁伺服器中包含對此介面的支援將是一個好主意。)
除了便於現有和未來的框架與伺服器實作外,建立請求預處理器、回應後處理器以及其他基於 WSGI 的「中介軟體」元件也應該是容易的。這些元件對於其包含的伺服器來說就像一個應用程式,而對於其包含的應用程式來說則像一個伺服器。
如果中介軟體能做到既簡單又強健,且 WSGI 在伺服器和框架中得到廣泛應用,那麼它將催生一種全新類型的 Python 網頁應用程式框架:由鬆散耦合的 WSGI 中介軟體元件組成的框架。事實上,現有的框架作者甚至可能選擇重構其框架現有的服務,改以此種方式提供,使其更像是與 WSGI 搭配使用的函式庫,而不是單體式(monolithic)框架。這將允許應用程式開發者為特定功能選擇「同類最佳」的元件,而不必受限於單一框架的所有優缺點。
當然,在撰寫本文時,那一天無疑還很遙遠。與此同時,WSGI 的短期目標是讓任何框架都能在任何伺服器上使用,這已經足夠了。
最後,必須提到的是,當前版本的 WSGI 並未規定任何特定的機制來「部署」與網頁伺服器或伺服器閘道配合使用的應用程式。目前,這必然由伺服器或閘道器自行定義。在有足夠數量的伺服器和框架實作了 WSGI 以提供各種部署需求的實作經驗後,或許可以建立另一個 PEP,描述 WSGI 伺服器和應用程式框架的部署標準。
規範概述
WSGI 介面有兩端:一端是「伺服器」或「閘道器」,另一端是「應用程式」或「框架」。伺服器端會呼叫一個由應用程式端提供的可呼叫物件。至於該物件如何提供,則取決於伺服器或閘道器。我們假設某些伺服器或閘道器會要求應用程式的部署者撰寫一個簡短的腳本,來建立伺服器或閘道器的執行個體,並將應用程式物件提供給它。其他伺服器和閘道器可能會使用配置檔案或其他機制,來指定應從何處匯入或以其他方式取得應用程式物件。
除了「純粹」的伺服器/閘道器和應用程式/框架外,還可以建立實作本規範兩端的「中介軟體」元件。此類元件對其包含的伺服器而言扮演應用程式的角色,對其包含的應用程式而言則扮演伺服器的角色,可用於提供擴充 API、內容轉換、導覽和其他實用功能。
在本規範中,我們將使用「可呼叫對象(a callable)」一詞,意指「函式、方法、類別,或具有 __call__ 方法的執行個體」。實作該可呼叫對象的伺服器、閘道器或應用程式可以根據其需求選擇合適的實作技術。相反地,呼叫該對象的伺服器、閘道器或應用程式,**絕不能**依賴於提供給它的可呼叫對象類型。可呼叫對象僅供呼叫,而非用於內省(introspection)。
應用程式/框架端
應用程式物件只是一個接受兩個參數的可呼叫物件。這裡的「物件」一詞不應被誤解為需要一個實際的物件執行個體:函式、方法、類別或具有 __call__ 方法的執行個體,都可以作為應用程式物件。應用程式物件必須能夠被呼叫多次,因為幾乎所有的伺服器/閘道器(CGI 除外)都會發出此類重複請求。
(注意:雖然我們將其稱為「應用程式」物件,但這不應被解讀為應用程式開發者將使用 WSGI 作為網頁程式開發 API!我們假設應用程式開發者將繼續使用現有的高階框架服務來開發其應用程式。WSGI 是一個提供給框架和伺服器開發者的工具,並非旨在直接支援應用程式開發者。)
以下是兩個應用程式物件範例;一個是函式,另一個是類別
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']
class AppClass:
"""Produce the same output, but using a class
(Note: 'AppClass' is the "application" here, so calling it
returns an instance of 'AppClass', which is then the iterable
return value of the "application callable" as required by
the spec.
If we wanted to use *instances* of 'AppClass' as application
objects instead, we would have to implement a '__call__'
method, which would be invoked to execute the application,
and we would need to create an instance for use by the
server or gateway.
"""
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
伺服器/閘道器端
伺服器或閘道器針對從 HTTP 客戶端接收到的、指向該應用程式的每個請求,呼叫一次應用程式可呼叫對象。為了說明,這裡有一個簡單的 CGI 閘道器,實作為一個接受應用程式物件的函式。請注意,這個簡單範例的錯誤處理能力有限,因為預設情況下,未擷取的異常(exception)將被傾倒到 sys.stderr 並由網頁伺服器記錄。
import os, sys
def run_with_cgi(application):
environ = dict(os.environ.items())
environ['wsgi.input'] = sys.stdin
environ['wsgi.errors'] = sys.stderr
environ['wsgi.version'] = (1, 0)
environ['wsgi.multithread'] = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once'] = True
if environ.get('HTTPS', 'off') in ('on', '1'):
environ['wsgi.url_scheme'] = 'https'
else:
environ['wsgi.url_scheme'] = 'http'
headers_set = []
headers_sent = []
def write(data):
if not headers_set:
raise AssertionError("write() before start_response()")
elif not headers_sent:
# Before the first output, send the stored headers
status, response_headers = headers_sent[:] = headers_set
sys.stdout.write('Status: %s\r\n' % status)
for header in response_headers:
sys.stdout.write('%s: %s\r\n' % header)
sys.stdout.write('\r\n')
sys.stdout.write(data)
sys.stdout.flush()
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
# Re-raise original exception if headers sent
raise exc_info[0], exc_info[1], exc_info[2]
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
return write
result = application(environ, start_response)
try:
for data in result:
if data: # don't send headers until body appears
write(data)
if not headers_sent:
write('') # send headers now if body was empty
finally:
if hasattr(result, 'close'):
result.close()
中介軟體:兼顧兩端的元件
請注意,單一物件在對某些應用程式時可以扮演伺服器的角色,而在對某些伺服器時也可以扮演應用程式的角色。此類「中介軟體」元件可以執行如下功能
- 在根據目標 URL 相應重寫
environ後,將請求路由到不同的應用程式物件。 - 允許在同一程序中並行執行多個應用程式或框架
- 透過在網路上轉發請求和回應來實現負載平衡和遠端處理
- 執行內容後處理,例如套用 XSL 樣式表
一般而言,中介軟體的存在對於介面的「伺服器/閘道器」和「應用程式/框架」兩端都是透明的,且不需要特殊支援。想要將中介軟體整合到應用程式中的使用者,只需像提供應用程式一樣將中介軟體元件提供給伺服器,並像配置伺服器一樣配置中介軟體元件來呼叫應用程式。當然,中介軟體封裝的「應用程式」實際上可能是另一個封裝了另一個應用程式的中介軟體元件,以此類推,形成所謂的「中介軟體堆疊(middleware stack)」。
在很大程度上,中介軟體必須符合 WSGI 伺服器端和應用程式端兩者的限制與要求。然而在某些情況下,對中介軟體的要求比對「純粹」伺服器或應用程式的要求更為嚴格,這些點將在規範中註明。
這是一個(開玩笑性質的)中介軟體元件範例,它使用 Joe Strout 的 piglatin.py 將 text/plain 回應轉換為兒童黑話(pig Latin)。(注意:一個「真正的」中介軟體元件可能會使用更強健的方法來檢查內容類型,並且還應該檢查內容編碼。此外,這個簡單範例忽略了單字可能跨越區塊邊界被切斷的可能性。)
from piglatin import piglatin
class LatinIter:
"""Transform iterated output to piglatin, if it's okay to do so
Note that the "okayness" can change until the application yields
its first non-empty string, so 'transform_ok' has to be a mutable
truth value.
"""
def __init__(self, result, transform_ok):
if hasattr(result, 'close'):
self.close = result.close
self._next = iter(result).next
self.transform_ok = transform_ok
def __iter__(self):
return self
def next(self):
if self.transform_ok:
return piglatin(self._next())
else:
return self._next()
class Latinator:
# by default, don't transform output
transform = False
def __init__(self, application):
self.application = application
def __call__(self, environ, start_response):
transform_ok = []
def start_latin(status, response_headers, exc_info=None):
# Reset ok flag, in case this is a repeat call
del transform_ok[:]
for name, value in response_headers:
if name.lower() == 'content-type' and value == 'text/plain':
transform_ok.append(True)
# Strip content-length if present, else it'll be wrong
response_headers = [(name, value)
for name, value in response_headers
if name.lower() != 'content-length'
]
break
write = start_response(status, response_headers, exc_info)
if transform_ok:
def write_latin(data):
write(piglatin(data))
return write_latin
else:
return write
return LatinIter(self.application(environ, start_latin), transform_ok)
# Run foo_app under a Latinator's control, using the example CGI gateway
from foo_app import foo_app
run_with_cgi(Latinator(foo_app))
規範詳情
應用程式物件必須接受兩個位置參數(positional arguments)。為了說明,我們將其命名為 environ 和 start_response,但並不要求必須使用這些名稱。伺服器或閘道器必須使用位置參數(而非關鍵字參數)來呼叫應用程式物件。(例如,如上所示呼叫 result = application(environ, start_response)。)
environ 參數是一個字典(dictionary)物件,包含 CGI 風格的環境變數。此物件必須是內建的 Python 字典(*不是*其子類、UserDict 或其他字典模擬),且允許應用程式以任何其希望的方式修改該字典。該字典還必須包含某些 WSGI 要求的變數(在後面的章節中說明),並可能包含伺服器特定的擴充變數,其命名遵循下述慣例。
start_response 參數是一個接受兩個必要位置參數和一個選用參數的可呼叫對象。為了說明,我們將這些參數命名為 status、response_headers 和 exc_info,但並不要求必須使用這些名稱,且應用程式必須使用位置參數呼叫 start_response 可呼叫對象(例如 start_response(status, response_headers))。
status 參數是一個格式為 "999 Message here" 的狀態字串,而 response_headers 是一個描述 HTTP 回應標頭的 (header_name, header_value) 元組(tuple)列表。選用的 exc_info 參數將在後面的 start_response() 可呼叫對象 和 錯誤處理 章節中說明。它僅在應用程式擷取到錯誤並試圖向瀏覽器顯示錯誤訊息時使用。
start_response 可呼叫對象必須返回一個 write(body_data) 可呼叫對象,後者接受一個位置參數:作為 HTTP 回應主體一部分寫入的字串。(注意:提供 write() 可呼叫對象僅是為了支援某些現有框架的指令式輸出 API;在可以避免的情況下,新應用程式或框架不應使用它。詳見 緩衝與串流 章節。)
當被伺服器呼叫時,應用程式物件必須傳回一個產生零個或多個字串的可迭代對象(iterable)。這可以透過多種方式實現,例如傳回字串列表,或者應用程式是一個產生字串的產生器(generator)函式,或者應用程式是一個其執行個體可迭代的類別。無論如何實現,應用程式物件必須始終傳回一個產生零個或多個字串的可迭代對象。
伺服器或閘道器必須以非緩衝的方式將產生的字串傳輸給客戶端,在完成每個字串的傳輸後再請求下一個字串。(換句話說,應用程式應該自行進行緩衝。關於應用程式輸出必須如何處理的更多資訊,請參見下面的 緩衝與串流 章節。)
伺服器或閘道器應將產生的字串視為二進位位元組序列:特別是它應確保換行符號不被更改。應用程式負責確保要寫入的字串格式適合客戶端。(伺服器或閘道器可以套用 HTTP 傳輸編碼,或為了實現位元組範圍傳輸等 HTTP 特性而執行其他轉換。詳見下文的 其他 HTTP 特性。)
如果對 len(iterable) 的呼叫成功,伺服器必須能夠依賴該結果的準確性。也就是說,如果應用程式傳回的可迭代對象提供了一個有效的 __len__() 方法,它必須傳回準確的結果。(關於這通常如何使用,請參見 處理 Content-Length 標頭 章節。)
如果應用程式傳回的可迭代對象具有 close() 方法,伺服器或閘道器必須在目前請求完成後呼叫該方法,無論請求是正常完成還是因錯誤提前終止(這是為了支援應用程式釋放資源)。此協定旨在補充 PEP 325 的產生器支援,以及其他具有 close() 方法的常見可迭代對象。
(注意:應用程式必須在可迭代對象產生其第一個主體字串之前呼叫 start_response() 可呼叫對象,以便伺服器可以在任何主體內容之前發送標頭。然而,此呼叫可以在可迭代對象的第一次迭代時執行,因此伺服器絕不能假設在開始對可迭代對象進行迭代之前,start_response() 已被呼叫過。)
最後,伺服器和閘道器絕不能直接使用應用程式傳回的可迭代對象的任何其他屬性,除非它是該伺服器或閘道器特定的類型執行個體,例如由 wsgi.file_wrapper 傳回的「檔案封裝器」(參見 選用的平台特定檔案處理)。在一般情況下,只有在此處指定或透過例如 PEP 234 迭代 API 存取的屬性才是可以接受的。
environ 變數
environ 字典被要求包含這些 CGI 環境變數,如通用閘道介面(Common Gateway Interface)規範 [2] 所定義。除非變數的值為空字串,否則下列變數必須存在,若值為空,則可以省略,除非下文另有說明。
REQUEST_METHOD- HTTP 請求方法,例如
"GET"或"POST"。這永遠不能是空字串,因此始終是必要的。 SCRIPT_NAME- 請求 URL 中對應於應用程式物件的「路徑」起始部分,以便應用程式知道其虛擬「位置」。如果應用程式對應於伺服器的「根目錄」,這可以是空字串。
PATH_INFO- 請求 URL 「路徑」的其餘部分,指定請求目標在應用程式內的虛擬「位置」。如果請求 URL 指向應用程式根目錄且沒有結尾斜槓,這可以是空字串。
QUERY_STRING- 請求 URL 中緊隨
"?"之後的部分(如果有)。可能為空或不存在。 CONTENT_TYPE- HTTP 請求中任何
Content-Type欄位的內容。可能為空或不存在。 CONTENT_LENGTH- HTTP 請求中任何
Content-Length欄位的內容。可能為空或不存在。 SERVER_NAME,SERVER_PORT- 當與
SCRIPT_NAME和PATH_INFO結合時,這些變數可用於完成 URL。但請注意,如果存在HTTP_HOST,則在重建請求 URL 時應優先於SERVER_NAME使用。詳見下文的 URL 重建 章節。SERVER_NAME和SERVER_PORT永遠不能是空字串,因此始終是必要的。 SERVER_PROTOCOL- 客戶端發送請求時使用的協議版本。通常會是
"HTTP/1.0"或"HTTP/1.1"之類的內容,應用程式可據此決定如何處理任何 HTTP 請求標頭。(此變數或許應該稱為REQUEST_PROTOCOL,因為它表示請求中使用的協議,且不一定是伺服器回應中將使用的協議。但是為了與 CGI 相容,我們必須保留現有名稱。) HTTP_變數- 對應於客戶端提供的 HTTP 請求標頭的變數(即名稱以
"HTTP_"開頭的變數)。這些變數的存在或缺失應與請求中相應 HTTP 標頭的存在或缺失相符。
伺服器或閘道器應該嘗試提供儘可能多適用的其他 CGI 變數。此外,如果正在使用 SSL,伺服器或閘道器還應該提供儘可能多適用的 Apache SSL 環境變數 [3],例如 HTTPS=on 和 SSL_PROTOCOL。但請注意,使用上述清單以外的任何 CGI 變數的應用程式,對於不支援相關擴充功能的網頁伺服器而言,必然是不可移植的。(例如,不發布檔案的網頁伺服器將無法提供有意義的 DOCUMENT_ROOT 或 PATH_TRANSLATED。)
符合 WSGI 標準的伺服器或閘道器應該記錄其提供的變數以及適用的定義。應用程式應該檢查其所需的任何變數是否存在,並在該變數缺失時有備案計劃。
注意:缺失的變數(例如未發生認證時的 REMOTE_USER)應從 environ 字典中排除。另請注意,CGI 定義的變數如果存在,必須是字串。如果 CGI 變數的值是 str 以外的任何類型,則違反本規範。
除了 CGI 定義的變數外,environ 字典還可以包含任意作業系統「環境變數」,且必須包含下列 WSGI 定義的變數
| 變數 | 值 |
|---|---|
wsgi.version |
元組 (1, 0),代表 WSGI 版本 1.0。 |
wsgi.url_scheme |
代表呼叫應用程式所在的 URL 「方案(scheme)」部分的字串。通常其值為 "http" 或 "https"。 |
wsgi.input |
可從中讀取 HTTP 請求主體的輸入串流(類檔案物件)。(伺服器或閘道器可以根據應用程式的請求按需執行讀取,也可以預先讀取客戶端請求主體並將其緩衝在記憶體或磁碟中,或者根據其偏好使用任何其他技術來提供此類輸入串流。) |
wsgi.errors |
可向其寫入錯誤輸出的輸出串流(類檔案物件),目的是為了在標準化且可能集中的位置記錄程式或其他錯誤。這應該是一個「文字模式」串流;即應用程式應使用 "\n" 作為換行符號,並假設它將被伺服器/閘道器轉換為正確的換行符號。對於許多伺服器而言, |
wsgi.multithread |
如果應用程式物件可能同時被同一程序中的另一個執行緒呼叫,則此值應為 true,否則應為 false。 |
wsgi.multiprocess |
如果等效的應用程式物件可能同時被另一個程序呼叫,則此值應為 true,否則應為 false。 |
wsgi.run_once |
如果伺服器或閘道器預期(但不保證!)應用程式在其包含程序的生命週期內僅被呼叫這一次,則此值應為 true。通常這僅對於基於 CGI(或類似機制)的閘道器為 true。 |
最後,environ 字典還可以包含伺服器定義的變數。這些變數的命名應僅使用小寫字母、數字、點和底線,並應加上定義該變數的伺服器或閘道器特有的名稱作為前綴。例如,mod_python 可能會定義名稱如 mod_python.some_variable 的變數。
輸入與錯誤串流
伺服器提供的輸入與錯誤串流必須支援下列方法
| 方法 | 串流 | 備註 |
|---|---|---|
read(size) |
input |
1 |
readline() |
input |
1, 2 |
readlines(hint) |
input |
1, 3 |
__iter__() |
input |
|
flush() |
errors |
4 |
write(str) |
errors |
|
writelines(seq) |
errors |
各方法的語義與 Python 函式庫參考手冊中的記錄一致,除了上述表格中所列的這些備註
- 伺服器不被要求讀取超過客戶端指定的
Content-Length,且如果應用程式試圖讀取超過該點,允許模擬文件結束(EOF)條件。應用程式不應嘗試讀取超過CONTENT_LENGTH變數指定的資料量。 readline()的選用參數 「size」不受支援,因為這對伺服器作者來說可能實作複雜,且在實踐中不常使用。- 請注意,
readlines()的hint參數對於呼叫者和實作者而言都是選用的。應用程式可以選擇不提供它,伺服器或閘道器也可以選擇忽略它。 - 由於
errors串流不能倒回,伺服器和閘道器可以選擇立即轉發寫入操作,而不進行緩衝。在這種情況下,flush()方法可能是一個無操作(no-op)。然而,可移植的應用程式不能假設輸出是非緩衝的或flush()是無操作的。如果需要確保輸出確實已被寫入,則必須呼叫flush()。(例如,為了儘量減少多個程序同時寫入同一個錯誤日誌時的資料混雜。)
符合本規範的所有伺服器必須支援上表列出的方法。符合本規範的應用程式絕不能使用 input 或 errors 物件的任何其他方法或屬性。特別是,應用程式絕不能嘗試關閉這些串流,即使它們具有 close() 方法。
start_response() 可呼叫對象
傳遞給應用程式物件的第二個參數是一個格式為 start_response(status, response_headers, exc_info=None) 的可呼叫對象。(與所有 WSGI 可呼叫對象一樣,參數必須按位置提供,而非關鍵字參數。)start_response 可呼叫對象用於開始 HTTP 回應,且必須返回一個 write(body_data) 可呼叫對象(參見下文的 緩衝與串流 章節)。
status 參數是一個 HTTP 「狀態」字串,例如 "200 OK" 或 "404 Not Found"。也就是說,它是一個由狀態碼(Status-Code)和原因短語(Reason-Phrase)組成的字串,按此順序排列並以單個空格分隔,前後沒有空格或其他字元。(詳見 RFC 2616 第 6.1.1 節。)該字串不得包含控制字元,且不得以回車符(CR)、換行符(LF)或其組合結尾。
response_headers 參數是一個 (header_name, header_value) 元組列表。它必須是一個 Python 列表;即 type(response_headers) is ListType,且伺服器可以按其希望的方式更改其內容。每個 header_name 必須是有效的 HTTP 標頭欄位名稱(如 RFC 2616 第 4.2 節定義),不帶結尾冒號或其他標點符號。
每個 header_value 不得包含 *任何* 控制字元,包括嵌入式或結尾的回車符或換行符。(這些要求是為了儘量減少伺服器、閘道器以及需要檢查或修改回應標頭的中間回應處理器所需執行的解析複雜度。)
一般而言,伺服器或閘道器負責確保將正確的標頭發送給客戶端:如果應用程式遺漏了 HTTP(或其他相關現行規範)要求的標頭,伺服器或閘道器必須補上。例如,HTTP 的 Date: 和 Server: 標頭通常由伺服器或閘道器提供。
(給伺服器/閘道器作者的提醒:HTTP 標頭名稱是不分大小寫的,因此在檢查應用程式提供的標頭時,請務必考慮到這一點!)
禁止應用程式和中介軟體使用 HTTP/1.1 的「逐跳(hop-by-hop)」特性或標頭、HTTP/1.0 中的任何等效特性,或任何會影響客戶端與網頁伺服器連線持久性的標頭。這些特性是實際網頁伺服器的專屬領域,伺服器或閘道器應將應用程式嘗試發送這些內容視為致命錯誤,如果將其提供給 start_response(),則應引發錯誤。(有關「逐跳」特性和標頭的更多詳情,請參見下文的 其他 HTTP 特性 章節。)
start_response 可呼叫對象絕不能實際傳輸回應標頭。相反地,它必須將其儲存起來,由伺服器或閘道器在應用程式回傳值產生第一個非空字串之後,或者在應用程式第一次呼叫 write() 可呼叫對象時,才進行傳輸。換句話說,在實際主體資料可用或應用程式傳回的可迭代對象耗盡之前,回應標頭不得發送。(規則中唯一可能的例外是,如果回應標頭明確包含長度為零的 Content-Length。)
延遲回應標頭傳輸是為了確保緩衝型和非同步應用程式在最後一刻前,能用錯誤輸出替換其原本預定的輸出。例如,如果在應用程式緩衝區內產生主體時發生錯誤,應用程式可能需要將回應狀態從「200 OK」更改為「500 Internal Error」。
如果提供了 exc_info 參數,它必須是一個 Python 的 sys.exc_info() 元組。僅當錯誤處理程式呼叫 start_response 時,應用程式才應提供此參數。如果提供了 exc_info,且尚未輸出任何 HTTP 標頭,則 start_response 應將當前儲存的 HTTP 回應標頭替換為新提供的標頭,從而允許應用程式在發生錯誤時「改變主意」。
但是,如果提供了 exc_info,但 HTTP 標頭已經發送,則 start_response 必須引發一個錯誤,且應該重新引發 exc_info 元組。即:
raise exc_info[0], exc_info[1], exc_info[2]
這將重新引發應用程式擷取到的異常,且原則上應中止應用程式。(一旦 HTTP 標頭已發送,應用程式嘗試向瀏覽器輸出錯誤內容就不再安全。)如果應用程式在呼叫 start_response 時帶有 exc_info,則絕不能擷取由 start_response 引發的任何異常。相反,它應讓此類異常傳回伺服器或閘道器。詳見下文的 錯誤處理。
若且唯若提供 exc_info 參數時,應用程式可以多次呼叫 start_response。更精確地說,如果在目前應用程式呼叫中已經呼叫過 start_response,但在不帶 exc_info 參數的情況下再次呼叫它,則是致命錯誤。(有關正確邏輯的說明,請參見上面的 CGI 閘道器範例。)
注意:實作 start_response 的伺服器、閘道器或中介軟體應該確保在函數執行期間之外不保留對 exc_info 參數的引用,以避免透過涉及的追蹤記錄(traceback)和框架(frame)建立循環引用。最簡單的方法如下:
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
# do stuff w/exc_info here
finally:
exc_info = None # Avoid circular ref.
範例 CGI 閘道器提供了這種技術的另一個說明。
處理 Content-Length 標頭
如果應用程式未提供 Content-Length 標頭,伺服器或閘道器可以選擇幾種處理方式。最簡單的一種是在回應完成時關閉客戶端連線。
然而,在某些情況下,伺服器或閘道器或許能夠產生 Content-Length 標頭,或至少避免需要關閉客戶端連線。如果應用程式 *沒有* 呼叫 write() 可呼叫對象,且傳回一個 len() 為 1 的可迭代對象,那麼伺服器可以透過獲取可迭代對象產生的第一個字串的長度來自動確定 Content-Length。
而且,如果伺服器和客戶端都支援 HTTP/1.1 「分塊編碼(chunked encoding)」,那麼伺服器可以使用分塊編碼為每個 write() 呼叫或可迭代對象產生的字串發送一個區塊,從而為每個區塊產生一個 Content-Length 標頭。這允許伺服器保持客戶端連線(如果它願意)。請注意,伺服器在執行此操作時必須完全符合 RFC 2616,否則必須退而使用其他策略來處理 Content-Length 的缺失。
(注意:應用程式和中介軟體絕不能對其輸出套用任何類型的 Transfer-Encoding,例如分塊或 gzip;作為「逐跳」操作,這些編碼是實際網頁伺服器/閘道器的權責。詳見下文的 其他 HTTP 特性。)
緩衝與串流
一般而言,應用程式透過緩衝其(適度大小的)輸出並一次性發送,將獲得最佳輸送量。這是現有框架(如 Zope)中的常見做法:輸出被緩衝在 StringIO 或類似物件中,然後連同回應標頭一起一次性傳輸。
在 WSGI 中相應的做法是,應用程式僅傳回一個包含回應主體單一字串的單元素可迭代對象(如列表)。這是絕大多數應用程式函式的推薦做法,因為它們呈現的 HTML 頁面文字可以輕易裝入記憶體。
然而,對於大型檔案,或對於 HTTP 串流的特殊用途(例如多部分「伺服器推播」),應用程式可能需要以較小的區塊提供輸出(例如,為了避免將大型檔案載入記憶體)。有時回應的一部分可能耗時較長才能產生,但先行發送回應的前半部分會很有用。
在這些情況下,應用程式通常會傳回一個疊代器(iterator)(通常是產生器疊代器),以逐塊的方式產生輸出。這些區塊的切分可以與多部分邊界一致(用於「伺服器推播」),或就在耗時任務(例如讀取磁碟檔案的另一個區塊)之前。
WSGI 伺服器、閘道器和中介軟體絕不能延遲任何區塊的傳輸;它們必須要麼將區塊完整傳輸給客戶端,要麼保證即使應用程式正在產生下一個區塊,傳輸也會繼續。伺服器/閘道器或中介軟體可以透過以下三種方式之一提供此保證:
- 在將控制權交還給應用程式之前,將整個區塊發送給作業系統(並請求重新整理任何作業系統緩衝區),或者
- 使用不同的執行緒,確保在應用程式產生下一個區塊時,該區塊能繼續傳輸。
- (僅限中介軟體)將整個區塊發送給其父閘道器/伺服器
透過提供此保證,WSGI 允許應用程式確保傳輸不會在其輸出資料的任意點停滯。這對於例如多部分「伺服器推播」串流的正確運作至關重要,其中多部分邊界之間的資料應完整傳輸給客戶端。
中介軟體對區塊邊界的處理
為了更好地支援非同步應用程式和伺服器,中介軟體元件絕不能在等待應用程式可迭代對象的多個值時阻塞迭代。如果中介軟體在產生任何輸出之前需要從應用程式累積更多資料,它必須產生一個空字串。
換句話說,中介軟體元件在其底層應用程式產生一個值的每一次,都必須至少產生一個值。如果中介軟體無法產生任何其他值,它必須產生一個空字串。
這一要求確保了非同步應用程式和伺服器可以協作減少同時執行給定數量的應用程式執行個體所需的執行緒數量。
另請注意,這一要求意味著中介軟體在底層應用程式返回可迭代對象時,也必須立即返回一個可迭代對象。此外,嚴禁中介軟體使用 write() 可呼叫對象來傳輸底層應用程式產生的資料。中介軟體只能使用其父伺服器的 write() 可呼叫對象來傳輸底層應用程式透過中介軟體提供的 write() 可呼叫對象發送的資料。
write() 可呼叫對象
某些現有的應用程式框架 API 以不同於 WSGI 的方式支援非緩衝輸出。具體而言,它們提供某種「寫入(write)」函式或方法來寫入非緩衝的資料塊,或者它們提供緩衝的「寫入」函式和一個「重新整理(flush)」機制來重新整理緩衝區。
不幸的是,除非使用執行緒或其他特殊機制,否則此類 API 無法根據 WSGI 的「可迭代」應用程式回傳值來實作。
因此,為了允許這些框架繼續使用指令式 API,WSGI 包含了一個特殊的 write() 可呼叫對象,由 start_response 可呼叫對象傳回。
如果可能,新的 WSGI 應用程式和框架不應使用 write() 可呼叫對象。write() 可呼叫對象純粹是為了支援指令式串流 API 的權宜之計(hack)。一般而言,應用程式應透過其傳回的可迭代對象來產生輸出,因為這使得網頁伺服器能夠在同一個 Python 執行緒中交錯執行其他任務,從而可能為整個伺服器提供更好的輸送量。
write() 可呼叫對象由 start_response() 可呼叫對象傳回,它接受單個參數:作為 HTTP 回應主體一部分寫入的字串,其處理方式與輸出可迭代對象產生的字串完全相同。換句話說,在 write() 傳回之前,它必須保證傳入的字串要麼已完整發送給客戶端,要麼已緩衝以便在應用程式繼續執行時傳輸。
應用程式必須傳回一個可迭代物件,即使它使用 write() 來產生全部或部分回應主體。傳回的可迭代對象可以是空的(即不產生非空字串),但如果它 *確實* 產生了非空字串,則該輸出必須由伺服器或閘道器正常處理(即,必須立即發送或排隊)。應用程式絕不能在其回傳的可迭代對象內部呼叫 write(),因此,可迭代對象產生的任何字串,都會在傳遞給 write() 的所有字串發送給客戶端之後才傳輸。
Unicode 問題
HTTP 不直接支援 Unicode,本介面也不支援。所有的編碼/解碼必須由應用程式處理;所有與伺服器往來的字串必須是標準的 Python 位元組字串(byte strings),而非 Unicode 物件。在需要字串物件的地方使用 Unicode 物件的結果是未定義的。
另請注意,作為狀態或回應標頭傳遞給 start_response() 的字串,在編碼方面必須遵循 RFC 2616。也就是說,它們必須是 ISO-8859-1 字元,或使用 RFC 2047 MIME 編碼。
在 str 或 StringType 類型實際上是基於 Unicode 的 Python 平台(例如 Jython、IronPython、Python 3000 等)上,本規範中提到的所有「字串」必須僅包含 ISO-8859-1 編碼可表示的碼點(\u0000 到 \u00FF,含)。應用程式提供包含任何其他 Unicode 字元或碼點的字串是致命錯誤。同樣,伺服器和閘道器絕不能向應用程式提供包含任何其他 Unicode 字元的字串。
再次強調,本規範中提到的所有字串必須為 str 或 StringType 類型,且絕不能為 unicode 或 UnicodeType 類型。而且,即使給定平台允許 str/StringType 物件每個字元超過 8 位元,對於本規範中稱為「字串」的任何值,也只能使用低 8 位元。
錯誤處理
一般而言,應用程式應該嘗試擷取其內部的錯誤,並在瀏覽器中顯示有用的訊息。(由應用程式決定在這種情況下「有用」意味著什麼。)
然而,要顯示這樣的訊息,應用程式絕對不能已經向瀏覽器發送任何資料,否則會面臨破壞回應的風險。因此,WSGI 提供了一種機制,既允許應用程式發送其錯誤訊息,又能在必要時自動中止:start_response 的 exc_info 參數。以下是其用法範例:
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 的呼叫將正常返回,應用程式將返回一個錯誤主體發送給瀏覽器。但是,如果已經有任何輸出發送到了瀏覽器,start_response 將重新引發提供的異常。此異常不應被應用程式擷取,因此應用程式將中止。伺服器或閘道器接著可以擷取此(致命)異常並中止回應。
伺服器應該擷取並記錄任何導致應用程式或其回傳值迭代中止的異常。如果在發生應用程式錯誤時,部分回應已經寫入瀏覽器,伺服器或閘道器可以嘗試向輸出添加錯誤訊息,前提是已發送的標頭指示了伺服器知道如何乾淨地修改的 text/* 內容類型。
某些中介軟體可能希望提供額外的異常處理服務,或攔截並替換應用程式錯誤訊息。在這種情況下,中介軟體可以選擇不重新引發提供給 start_response 的 exc_info,而是引發一個中介軟體特有的異常,或者在儲存提供的參數後直接返回而不引發異常。這將導致應用程式傳回其錯誤主體可迭代對象(或呼叫 write()),從而允許中介軟體擷取並修改錯誤輸出。只要應用程式作者遵循以下原則,這些技術就會奏效:
- 在開始錯誤回應時始終提供
exc_info - 在提供
exc_info時,絕不擷取由start_response引發的錯誤
HTTP 1.1 Expect/Continue
實作 HTTP 1.1 的伺服器和閘道器必須為 HTTP 1.1 的「expect/continue」機制提供透明支援。這可以透過多種方式實現:
- 對包含
Expect: 100-continue標頭的請求,立即回應「100 Continue」,並正常進行。 - 正常處理請求,但為應用程式提供一個
wsgi.input串流,當應用程式第一次嘗試從輸入串流讀取時,該串流會發送「100 Continue」回應。讀取請求隨後必須保持阻塞狀態,直到客戶端回應。 - 等待直到客戶端判定伺服器不支援 expect/continue,並自行發送請求主體。(這是次優的做法,不推薦使用。)
請注意,這些行為限制不適用於 HTTP 1.0 請求,也不適用於不指向應用程式物件的請求。有關 HTTP 1.1 Expect/Continue 的更多資訊,請參見 RFC 2616 第 8.2.3 和 10.1.1 節。
其他 HTTP 特性
一般而言,伺服器和閘道器應「保持中立」,並允許應用程式完全控制其輸出。它們應該只進行不會改變應用程式回應有效語義的更改。應用程式開發者始終可以添加中介軟體元件來提供額外功能,因此伺服器/閘道器開發者在實作時應保持保守。在某種意義上,伺服器應將自己視為 HTTP 「閘道器伺服器」,而應用程式則是 HTTP 「原始伺服器(origin server)」。(有關這些術語的定義,請參見 RFC 2616 第 1.3 節。)
然而,由於 WSGI 伺服器和應用程式不透過 HTTP 通訊,RFC 2616 所稱的「逐跳(hop-by-hop)」標頭不適用於 WSGI 內部通訊。WSGI 應用程式絕不能產生任何 「逐跳標頭」,不得嘗試使用需要產生此類標頭的 HTTP 特性,也不得依賴 environ 字典中任何傳入的「逐跳標頭」。WSGI 伺服器必須自行處理任何支援的傳入「逐跳標頭」,例如解碼任何傳入的 Transfer-Encoding(包括適用的分塊編碼)。
將這些原則套用於各種 HTTP 特性,應該可以清楚看出伺服器可以透過 If-None-Match 和 If-Modified-Since 請求標頭以及 Last-Modified 和 ETag 回應標頭來處理快取驗證。但是,伺服器並非必須這樣做,如果應用程式想要支援該特性,它應該自行執行快取驗證,因為伺服器/閘道器不被要求執行此類驗證。
同樣,伺服器可以對應用程式的回應進行重新編碼或傳輸編碼,但應用程式應該自行使用合適的內容編碼,且絕不能套用傳輸編碼。如果客戶端請求,且應用程式本身不支援位元組範圍,伺服器可以傳輸應用程式回應的位元組範圍。同樣,如果需要,應用程式應該自行執行此功能。
請注意,對應用程式的這些限制並不一定意味著每個應用程式都必須重新實作每個 HTTP 特性;許多 HTTP 特性可以由中介軟體元件部分或全部實作,從而使伺服器和應用程式作者免於反覆實作相同的功能。
執行緒支援
執行緒支援(或缺失)也取決於伺服器。能夠並行執行多個請求的伺服器,也應該提供以單執行緒方式執行應用程式的選項,以便非執行緒安全的應用程式或框架仍可與該伺服器配合使用。
實作/應用說明
伺服器擴充 API
某些伺服器作者可能希望公開更進階的 API,供應用程式或框架作者用於特殊用途。例如,基於 mod_python 的閘道器可能希望將部分 Apache API 作為 WSGI 擴充功能公開。
在最簡單的情況下,這只需要定義一個 environ 變數,例如 mod_python.some_api。但是,在許多情況下,中介軟體的可能存在會使這變得很困難。例如,一個提供存取與 environ 變數中相同 HTTP 標頭的 API,如果 environ 已被中介軟體修改,則可能會傳回不同的資料。
一般而言,任何複製、取代或繞過 WSGI 部分功能的擴充 API 都存在與中介軟體元件不相容的風險。伺服器/閘道器開發者 *不應* 假設無人會使用中介軟體,因為某些框架開發者專門打算將其框架組織或重組為幾乎完全由各類中介軟體組成。
因此,為了提供最大相容性,提供取代某些 WSGI 功能之擴充 API 的伺服器和閘道器,必須將這些 API 設計成需要透過它們所取代的 API 部分來呼叫。例如,存取 HTTP 請求標頭的擴充 API 必須要求應用程式傳入其當前的 environ,以便伺服器/閘道器可以驗證透過該 API 存取的 HTTP 標頭未被中介軟體修改。如果擴充 API 不能保證它始終與 environ 中關於 HTTP 標頭的內容一致,則它必須拒絕向應用程式提供服務,例如透過引發錯誤、傳回 None 而非標頭集合,或任何適合該 API 的方式。
同樣,如果擴充 API 提供了一種寫入回應資料或標頭的替代方法,它應該要求在應用程式獲得擴充服務之前傳入 start_response 可呼叫對象。如果傳入的物件不是伺服器/閘道器最初提供給應用程式的那個,它就不能保證正確操作,且必須拒絕向應用程式提供擴充服務。
這些準則也適用於將解析後的 Cookie、表單變數、對話(session)等資訊添加到 environ 的中介軟體。具體而言,此類中介軟體應將這些特性提供為操作 environ 的函式,而非僅僅將值填入 environ。這有助於確保資訊是在任何中介軟體執行過 URL 重寫或其他 environ 修改 *之後*,才從 environ 計算出來的。
伺服器/閘道器和中介軟體開發者遵循這些「安全擴充」規則非常重要,以避免未來中介軟體開發者被迫從 environ 中刪除所有擴充 API,以確保其調節功能不會被使用這些擴充功能的應用程式繞過!
應用程式配置
本規範未定義伺服器如何選擇或取得要呼叫的應用程式。這些以及其他配置選項是高度伺服器特定的事項。預期伺服器/閘道器作者將記錄如何配置伺服器以執行特定的應用程式物件,以及帶有哪些選項(例如執行緒選項)。
另一方面,框架作者應記錄如何建立一個封裝了其框架功能的應用程式物件。選擇了伺服器和應用程式框架的使用者必須將兩者連接在一起。然而,由於現在框架和伺服器都有一個共同的介面,這應該僅僅是機械化的操作,而不是為每對新的伺服器/框架進行重大的工程投入。
最後,某些應用程式、框架和中介軟體可能希望使用 environ 字典來接收簡單的字串配置選項。伺服器和閘道器應 (should) 支援此功能,允許應用程式部署者指定要放入 environ 中的名稱-值對。在最簡單的情況下,此支援可以僅包括將所有作業系統提供的環境變數從 os.environ 複製到 environ 字典中,因為原則上部署者可以在伺服器外部配置這些變數,或者在 CGI 的情況下,可以透過伺服器的設定檔來設置。
應用程式應 (should) 嘗試將此類必要變數保持在最低限度,因為並非所有伺服器都能輕易支援其配置。當然,即使在最壞的情況下,部署應用程式的人員也可以建立一個腳本來提供必要的配置值。
from the_app import application
def new_app(environ, start_response):
environ['the_app.configval1'] = 'something'
return application(environ, start_response)
但是,大多數現有的應用程式和框架可能只需要從 environ 獲取單個配置值,以指示其應用程式或框架特定設定檔的位置。(當然,應用程式應該快取此類配置,以避免每次呼叫時重新讀取。)
URL 重建
如果應用程式希望重建請求的完整 URL,可以使用以下由 Ian Bicking 提供的演算法:
from urllib import quote
url = environ['wsgi.url_scheme']+'://'
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
請注意,重建後的 URL 可能與用戶端請求的 URI 不完全相同。例如,伺服器重寫規則 (rewrite rules) 可能已修改用戶端最初請求的 URL,使其變為規範形式 (canonical form)。
支援舊版本 (<2.2) 的 Python
某些伺服器、閘道器或應用程式可能希望支援較舊(低於 2.2)版本的 Python。如果目標平台是 Jython,這一點尤為重要,因為在撰寫本文時,尚未提供生產就緒版本的 Jython 2.2。
對於伺服器和閘道器來說,這相對簡單:針對 2.2 以前版本 Python 的伺服器和閘道器必須僅限於使用標準的 「for」 迴圈來遍歷應用程式返回的任何可迭代物件。這是確保原始碼層級相容於 2.2 之前的迭代器協定(下文將進一步討論)和「當今」的迭代器協定(參見 PEP 234)的唯一方法。
(請注意,此技術必然僅適用於以 Python 編寫的伺服器、閘道器或中介軟體。如何從其他語言正確使用迭代器協定不在本 PEP 的討論範圍之內。)
對於應用程式來說,支援 2.2 以前版本的 Python 會稍微複雜一些:
- 您不能返回檔案物件並期望它像可迭代物件一樣運作,因為在 Python 2.2 之前,檔案是不可迭代的。(一般來說,您也不應該這樣做,因為在大多數情況下性能會非常差!)請使用
wsgi.file_wrapper或應用程式特定的檔案包裝類別。(關於wsgi.file_wrapper的更多資訊,以及可用於將檔案包裝為可迭代物件的範例類別,請參見選用性平台特定檔案處理。) - 如果您返回自定義的可迭代物件,它必須 (must) 實作 2.2 之前的迭代器協定。也就是說,提供一個接受整數鍵值的
__getitem__方法,並在耗盡時引發IndexError。(請注意,內建序列類型也是可以接受的,因為它們也實作了此協定。)
最後,希望支援 2.2 以前版本 Python,且需要遍歷應用程式返回值或本身返回可迭代物件(或兩者兼有)的中介軟體,必須遵循上述相應建議。
(注意:不言而喻,為了支援 2.2 之前的 Python 版本,任何伺服器、閘道器、應用程式或中介軟體都必須僅使用目標版本中可用的語言特性,例如使用 1 和 0 代替 True 和 False 等。)
選用的平台特定檔案處理
某些作業環境提供特殊的、高效能的檔案傳輸功能,例如 Unix 的 sendfile() 系統呼叫。伺服器和閘道器可以 (may) 透過 environ 中選用的 wsgi.file_wrapper 鍵來公開此功能。應用程式可以 (may) 使用此「檔案包裝器」將檔案或類檔案物件轉換為其後返回的可迭代物件,例如:
if 'wsgi.file_wrapper' in environ:
return environ['wsgi.file_wrapper'](filelike, block_size)
else:
return iter(lambda: filelike.read(block_size), '')
如果伺服器或閘道器提供 wsgi.file_wrapper,它必須是一個接受一個必要位置參數和一個選用位置參數的可呼叫物件。第一個參數是要發送的類檔案物件,第二個參數是選用的區塊大小「建議」(伺服器/閘道器不一定要使用)。該可呼叫物件必須 (must) 返回一個可迭代物件,並且在伺服器/閘道器實際收到該可迭代物件作為應用程式返回值之前,不得 (must not) 進行任何數據傳輸。(否則會阻止中介軟體解釋或覆蓋回應數據。)
為了被視為「類檔案」,應用程式提供的物件必須具有一個接受選用大小參數的 read() 方法。它可以 (may) 具有 close() 方法,如果有,則 wsgi.file_wrapper 返回的可迭代物件必須 (must) 具有一個呼叫原始類檔案物件之 close() 方法的 close() 方法。如果該「類檔案」物件具有名稱與 Python 內建檔案物件相匹配的任何其他方法或屬性(例如 fileno()),則 wsgi.file_wrapper 可以 (may) 假設這些方法或屬性具有與內建檔案物件相同的語義。
任何平台特定檔案處理的實際實作必須在應用程式返回之後,且伺服器或閘道器檢查是否返回了包裝器物件時發生。(同樣地,由於存在中介軟體、錯誤處理器等,無法保證建立的任何包裝器實際上都會被使用。)
除了 close() 的處理之外,從應用程式返回檔案包裝器的語義應與應用程式返回 iter(filelike.read, '') 相同。換句話說,傳輸應從傳輸開始時「檔案」內的當前位置開始,並持續到結束。
當然,平台特定的檔案傳輸 API 通常不接受任意的「類檔案」物件。因此,wsgi.file_wrapper 必須對提供的物件進行內省 (introspect),以查找諸如 fileno()(類 Unix 作業系統)或 java.nio.FileChannel(Jython 環境下)之類的東西,以確定該類檔案物件是否適用於其支援的平台特定 API。
請注意,即使該物件不適用於平台 API,wsgi.file_wrapper 仍必須 (must) 返回一個包裝了 read() 和 close() 的可迭代物件,以便使用檔案包裝器的應用程式可以跨平台移植。這是一個簡單的跨平台檔案包裝類別,同樣適用於舊版(2.2 之前)和新版 Python:
class FileWrapper:
def __init__(self, filelike, blksize=8192):
self.filelike = filelike
self.blksize = blksize
if hasattr(filelike, 'close'):
self.close = filelike.close
def __getitem__(self, key):
data = self.filelike.read(self.blksize)
if data:
return data
raise IndexError
這裡是一個伺服器/閘道器使用它來提供存取平台特定 API 的片段:
environ['wsgi.file_wrapper'] = FileWrapper
result = application(environ, start_response)
try:
if isinstance(result, FileWrapper):
# check if result.filelike is usable w/platform-specific
# API, and if so, use that API to transmit the result.
# If not, fall through to normal iterable handling
# loop below.
for data in result:
# etc.
finally:
if hasattr(result, 'close'):
result.close()
常見問題解答
- 為什麼
environ必須是字典?使用子類別有什麼問題?要求使用字典的理由是為了最大限度地提高伺服器之間的移植性。另一種方法是定義字典方法的一個子集作為標準且可移植的介面。然而,在實踐中,大多數伺服器可能會發現字典足以滿足其需求,因此框架作者將期望可以使用整套字典功能,因為它們通常都會存在。但是,如果某些伺服器選擇不使用字典,那麼儘管該伺服器「符合」規範,仍會出現互操作性問題。因此,強制使用字典簡化了規範並保證了互操作性。
請注意,這並不妨礙伺服器或框架開發人員在
environ字典內部將專門的服務作為自定義變數提供。這是提供任何此類加值服務的推薦方法。 - 為什麼既可以呼叫
write()又可以 yield 字串或返回可迭代物件?我們難道不應該只選一種方式嗎?如果我們只支援迭代方式,那麼目前假設「推播 (push)」可用性的框架就會受損。但是,如果我們只支援透過
write()進行推播,那麼對於傳輸大型檔案等情況,伺服器性能會受損(如果工作執行緒在所有輸出發送完畢之前無法開始處理新請求)。因此,這種折衷方案允許應用框架根據需要支援這兩種方式,但對伺服器實作者而言,負擔僅比純推播方式多一點。 close()是做什麼用的?當應用程式物件執行期間進行寫入時,應用程式可以使用 try/finally 區塊來確保資源被釋放。但是,如果應用程式返回一個可迭代物件,則所使用的任何資源直到該可迭代物件被垃圾回收時才會釋放。
close()慣用法允許應用程式在請求結束時釋放關鍵資源,並且它與 PEP 325 提議的產生器 try/finally 支援向前相容。- 為什麼這個介面如此低階?我想要功能 X!(例如:cookie、會話、持久化……)
這不是「又一個 Python 網頁框架」。這只是框架與網頁伺服器之間的一種溝通方式,反之亦然。如果你想要這些功能,你需要選擇一個提供你所需功能的網頁框架。如果該框架允許你建立 WSGI 應用程式,你應該能夠在大多數支援 WSGI 的伺服器中運行它。此外,某些 WSGI 伺服器可能會透過其
environ字典中提供的物件提供額外服務;詳情請參閱適用的伺服器文件。(當然,使用此類擴充功能的應用程式將無法移植到其他基於 WSGI 的伺服器。) - 為什麼使用 CGI 變數而不是好用的 HTTP 標頭?為什麼要把它們與 WSGI 定義的變數混在一起?
許多現有的網頁框架是大量建立在 CGI 規範之上的,且現有的網頁伺服器知道如何生成 CGI 變數。相比之下,表示傳入 HTTP 資訊的其他方式則顯得零散且缺乏市場佔有率。因此,使用 CGI 「標準」似乎是利用現有實作的好方法。至於將它們與 WSGI 變數混在一起,將它們分開只需要傳遞兩個字典參數,卻沒有帶來真正的實質好處。
- 狀態字串 (status string) 呢?我們不能只使用數字,傳入
200而不是"200 OK"嗎?這樣做會使伺服器或閘道器變得複雜,因為需要它們擁有一個數字狀態和對應訊息的對照表。相比之下,應用程式或框架作者很容易輸入與其正在使用的特定回應代碼相對應的額外文字,且現有的框架通常已經擁有包含所需訊息的對照表。因此,權衡之下,讓應用程式/框架負責似乎比讓伺服器或閘道器負責更好。
- 為什麼不保證
wsgi.run_once只執行應用程式一次?因為這僅僅是給應用程式的一個建議,告訴它應該「為不頻繁的執行做好準備」。這是針對在快取、會話等方面具有多種操作模式的應用框架設計的。在「多次執行」模式下,此類框架可能會預先載入快取,並且可能不會在每次請求後都將日誌或會話數據寫入磁碟。在「單次執行」模式下,此類框架會避免預先載入,並在每次請求後刷新所有必要的寫入。
然而,為了測試應用程式或框架以驗證在後者模式下的正確執行,可能有必要(或至少是權宜之計)多次呼叫它。因此,應用程式不應僅僅因為在呼叫時將
wsgi.run_once設置為True就假設它絕對不會再次執行。 - 功能 X(字典、可呼叫物件等)在應用程式代碼中使用起來很醜陋;我們為什麼不使用物件呢?
WSGI 的所有這些實作選擇都是專門為了讓功能彼此解耦 (decouple);將這些功能重新組合到封裝的物件中會使得編寫伺服器或閘道器變得困難一些,而編寫僅替換或修改整體功能一小部分的中介軟體則會困難一個數量級。
實質上,中介軟體希望擁有一個「責任鏈 (Chain of Responsibility)」模式,藉此它可以充當某些功能的「處理器」,同時允許其他功能保持不變。如果介面要保持可擴充性,這對於普通的 Python 物件來說是很難實現的。例如,必須使用
__getattr__或__getattribute__覆蓋 (overrides),以確保擴充功能(例如未來 WSGI 版本定義的屬性)能夠傳遞下去。眾所周知,這種類型的代碼很難做到 100% 正確,而且很少有人願意自己編寫。因此,他們會複製別人的實作,但在被複製的人修正了另一個邊緣案例 (corner case) 時,卻未能及時更新。
此外,這些必要的樣板代碼純粹是額外開銷,是中介軟體開發人員為了支援應用框架開發人員稍微漂亮一點的 API 而支付的代價。但是,應用框架開發人員通常只會更新一個框架來支援 WSGI,而且僅佔其整個框架中非常有限的一部分。這很可能是他們第一次(也可能是唯一的)WSGI 實作,因此他們很可能在編寫時隨手參考此規範。因此,花費精力用物件屬性之類的東西使 API 變得「更漂亮」,對於這群受眾來說可能是浪費。
我們鼓勵那些希望在直接 Web 應用程式編程(與 Web 框架開發相對)中使用更漂亮(或以其他方式改進)的 WSGI 介面的人,開發包裝 WSGI 的 API 或框架,以便應用程式開發人員方便使用。透過這種方式,WSGI 對於伺服器和中介軟體作者來說可以保持便捷的低階特性,而對於應用程式開發人員來說也不會顯得「醜陋」。
建議中/討論中
這些項目目前正在 Web-SIG 和其他地方討論,或者在 PEP 作者的「待辦事項」清單上:
wsgi.input是否應該是迭代器而不是檔案?這將有助於非同步應用程式和分塊編碼 (chunked-encoding) 的輸入流。- 目前正在討論選用性擴充功能,用於暫停應用程式輸出的迭代,直到有輸入可用或發生回呼 (callback)。
- 增加關於同步 vs. 非同步應用程式與伺服器的章節,相關的執行緒模型,以及這些領域的問題/設計目標。
致謝
感謝 Web-SIG 郵件列表上的許多成員,他們深思熟慮的反饋使這份修訂草案成為可能。特別感謝:
- Gregory 「Grisha」 Trubetskoy,
mod_python的作者,他對第一稿提出了嚴厲批評,認為它與「平淡無奇的 CGI」相比沒有任何優勢,從而促使我尋找更好的方法。 - Ian Bicking,他協助督促我正確指定多執行緒和多程序選項,並不斷要求我提供一種讓伺服器向應用程式提供自定義擴充數據的機制。
- Tony Lownds,他提出了
start_response函數的概念,該函數接收狀態和標頭並返回一個write函數。他的投入也引導了異常處理機制的設計,特別是在允許中介軟體覆蓋應用程式錯誤訊息的領域。 - Alan Kennedy,他在規範定稿前就勇於嘗試在 Jython 上實作 WSGI,這有助於塑造「支援舊版本 Python」章節以及選用的
wsgi.file_wrapper功能。 - Mark Nottingham,他針對 HTTP RFC 合規性問題廣泛地審閱了規範,特別是關於 HTTP/1.1 的特性,在他指出之前我甚至不知道它們的存在。
參考文獻
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-0333.rst
最後修改日期:2025-02-01 08:59:27 GMT