WSGI:Python 的服務器-應用程序接口
已發表: 2022-03-111993 年,網絡仍處於起步階段,擁有大約 1400 萬用戶和 100 個網站。 頁面是靜態的,但已經需要生成動態內容,例如最新的新聞和數據。 對此,Rob McCool 和其他貢獻者在國家超級計算應用中心 (NCSA) HTTPd Web 服務器(Apache 的前身)中實現了通用網關接口 (CGI)。 這是第一個可以提供由單獨應用程序生成的內容的 Web 服務器。
此後,互聯網上的用戶數量呈爆炸式增長,動態網站無處不在。 當第一次學習一門新語言甚至第一次學習編碼時,開發人員很快就會想知道如何將他們的代碼連接到網絡中。
Web 上的 Python 和 WSGI 的興起
自 CGI 創建以來,發生了很大變化。 CGI 方法變得不切實際,因為它需要在每個請求時創建一個新進程,從而浪費內存和 CPU。 出現了一些其他低級方法,如 FastCGI](http://www.fastcgi.com/) (1996) 和 mod_python (2000),在 Python Web 框架和 Web 服務器之間提供不同的接口。 隨著不同方法的激增,開發人員對框架的選擇最終限制了 Web 服務器的選擇,反之亦然。
為了解決這個問題,2003 年 Phillip J. Eby 提出了 PEP-0333,即 Python Web 服務器網關接口 (WSGI)。 這個想法是在 Python 應用程序和 Web 服務器之間提供一個高級的通用接口。
2003 年,PEP-3333 更新了 WSGI 接口以添加 Python 3 支持。 如今,幾乎所有 Python 框架都使用 WSGI 作為與 Web 服務器進行通信的一種手段,如果不是唯一手段的話。 這就是 Django、Flask 和許多其他流行框架的做法。
本文旨在讓讀者了解 WSGI 的工作原理,並允許讀者構建一個簡單的 WSGI 應用程序或服務器。 不過,這並不意味著詳盡無遺,打算實現生產就緒服務器或應用程序的開發人員應該更徹底地研究 WSGI 規範。
Python WSGI 接口
WSGI 指定了服務器和應用程序必須遵守的簡單規則。 讓我們從回顧這個整體模式開始。
應用程序接口
在 Python 3.5 中,應用程序接口如下所示:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
在 Python 2.7 中,這個接口不會有太大的不同。 唯一的變化是主體由一個str
對象表示,而不是一個bytes
。
雖然我們在這種情況下使用了一個函數,但任何可調用的都可以。 此處應用程序對象的規則是:
- 必須是帶有
environ
和start_response
參數的可調用對象。 - 必須在發送正文之前調用
start_response
回調。 - 必須返回帶有文檔正文部分的可迭代對象。
滿足這些規則並產生相同效果的對象的另一個示例是:
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body
服務器接口
一個 WSGI 服務器可以像這樣與這個應用程序交互::
def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()
您可能已經註意到, start_response
可調用對象返回了一個write
可調用對象,應用程序可以使用該可調用對象將數據發送回客戶端,但我們的應用程序代碼示例並未使用該可調用對象。 這個write
接口已被棄用,我們現在可以忽略它。 稍後將在文章中簡要討論。
服務器職責的另一個特點是調用響應迭代器上的可選close
方法(如果存在)。 正如 Graham Dumpleton 在此處的文章中所指出的,它是 WSGI 的一個經常被忽視的特性。 調用此方法(如果存在)允許應用程序釋放它可能仍持有的任何資源。
應用程序可調用的environ
參數
environ
參數應該是一個字典對象。 它用於將請求和服務器信息傳遞給應用程序,與 CGI 的方式非常相似。 事實上,所有 CGI 環境變量在 WSGI 中都是有效的,服務器應該將所有適用於應用程序的變量傳遞給應用程序。
雖然可以傳遞許多可選鍵,但有幾個是強制性的。 以下面的GET
請求為例:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
這些是服務器必須提供的鍵,以及它們將採用的值:
鑰匙 | 價值 | 註釋 |
---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | 服務器設置依賴 |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | 服務器設置依賴 |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | 客戶端提供的 HTTP 標頭 | |
wsgi.version | (1, 0) | 帶有 WSGI 版本的元組 |
wsgi.url_scheme | "http" | |
wsgi.input | 類文件對象 | |
wsgi.errors | 類文件對象 | |
wsgi.multithread | False | 如果服務器是多線程的,則為True |
wsgi.multiprocess | False | 如果服務器運行多個進程,則為True |
wsgi.run_once | False | 如果服務器希望此腳本只運行一次,則為True (例如:在 CGI 環境中) |
此規則的例外是,如果這些鍵之一為空(如上表中的CONTENT_TYPE
),則可以從字典中省略它們,並假定它們對應於空字符串。
wsgi.input
和wsgi.errors
大多數environ
鍵都很簡單,但其中兩個需要進一步說明: wsgi.input
,它必須包含一個帶有來自客戶端的請求正文的流,以及wsgi.errors
,應用程序報告它遇到的任何錯誤。 從應用程序發送到wsgi.errors
的錯誤通常會發送到服務器錯誤日誌。
這兩個鍵必須包含類似文件的對象; 也就是說,提供接口以作為流讀取或寫入的對象,就像我們在 Python 中打開文件或套接字時獲得的對像一樣。 起初這可能看起來很棘手,但幸運的是,Python 為我們提供了很好的工具來處理這個問題。

首先,我們在談論什麼樣的流? 根據 WSGI 定義, wsgi.input
和wsgi.errors
必須處理 Python 3 中的bytes
對象和 Python 2 中的str
對象。無論哪種情況,如果我們想使用內存緩衝區通過 WSGI 傳遞或獲取數據接口,我們可以使用類io.BytesIO
。
例如,如果我們正在編寫一個 WSGI 服務器,我們可以像這樣向應用程序提供請求正文:
- 對於 Python 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
- 對於 Python 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
在應用程序方面,如果我們想將接收到的流輸入轉換為字符串,我們需要編寫如下內容:
- 對於 Python 2.7
readstr = environ['wsgi.input'].read() # returns str object
- 對於 Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
wsgi.errors
流應該用於向服務器報告應用程序錯誤,並且行應該以\n
結尾。 Web 服務器應注意根據系統轉換為不同的行尾。
Application Callable 的start_response
參數
start_response
參數必須是一個可調用的,帶有兩個必需參數,即status
和headers
,以及一個可選參數exc_info
。 在將正文的任何部分發送回 Web 服務器之前,應用程序必須調用它。
在本文開頭的第一個應用示例中,我們將響應的主體作為列表返回,因此我們無法控制何時迭代列表。 因此,我們必須在返回列表之前調用start_response
。
在第二個中,我們在產生響應正文的第一個(並且在本例中是唯一一個)片段之前調用了start_response
。 無論哪種方式在 WSGI 規範中都是有效的。
從 Web 服務器端,調用start_response
實際上不應該將標頭髮送給客戶端,而是將其延遲到響應正文中至少有一個非空字節串要發送回客戶端。 這種架構允許在應用程序執行的最後一刻之前正確報告錯誤。
start_response
的status
參數
傳遞給start_response
回調的status
參數必須是一個由 HTTP 狀態代碼和描述組成的字符串,由一個空格分隔。 有效示例是: '200 OK'
或'404 Not Found'
。
start_response
的headers
參數
傳遞給start_response
回調的headers
參數必須是一個 Python tuple
list
,每個元組由(header_name, header_value)
組成。 每個標頭的名稱和值都必須是字符串(無論 Python 版本如何)。 這是一個罕見的類型很重要的例子,因為這確實是 WSGI 規範所要求的。
下面是一個header
參數可能是什麼樣子的有效示例:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
HTTP 標頭不區分大小寫,如果我們正在編寫符合 WSGI 的 Web 服務器,則在檢查這些標頭時需要注意這一點。 此外,應用程序提供的標頭列表不應該是詳盡的。 服務器有責任確保在將響應發送回客戶端之前存在所有必需的 HTTP 標頭,並填寫應用程序未提供的任何標頭。
start_response
的exc_info
參數
start_response
回調應該支持第三個參數exc_info
,用於錯誤處理。 該參數的正確使用和實現對於生產 Web 服務器和應用程序至關重要,但超出了本文的範圍。
可以在 WSGI 規範中獲得有關它的更多信息,here。
start_response
返回值—— write
回調
出於向後兼容的目的,實現 WSGI 的 Web 服務器應該返回一個write
callable。 此回調應允許應用程序將主體響應數據直接寫回客戶端,而不是通過迭代器將其交給服務器。
儘管它存在,但這是一個已棄用的界面,新應用程序應避免使用它。
生成響應體
實現 WSGI 的應用程序應該通過返回一個可迭代對象來生成響應體。 對於大多數應用程序,響應體不是很大,很容易放入服務器的內存中。 在這種情況下,最有效的發送方式是一次發送,使用單元素可迭代。 在特殊情況下,將整個主體加載到內存中是不可行的,應用程序可以通過這個可迭代的接口逐部分返回它。
Python 2 和 Python 3 的 WSGI 之間只有很小的區別:在 Python 3 中,響應體由bytes
對象表示; 在 Python 2 中,正確的類型是str
。
將 UTF-8 字符串轉換為bytes
或str
是一項簡單的任務:
- 蟒蛇 3.5:
body = 'unicode stuff'.encode('utf-8')
- 蟒蛇 2.7:
body = u'unicode stuff'.encode('utf-8')
如果您想了解更多關於 Python 2 的 unicode 和字節串處理的信息,YouTube 上有一個不錯的教程。
如上所述,實現 WSGI 的 Web 服務器還應該支持write
回調以實現向後兼容性。
在沒有 Web 服務器的情況下測試您的應用程序
通過了解這個簡單的界面,我們可以輕鬆地創建腳本來測試我們的應用程序,而無需實際啟動服務器。
以這個小腳本為例:
from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}
通過這種方式,例如,我們可以將一些測試數據和模擬模塊初始化到我們的應用程序中,並進行GET
調用以測試它是否相應地響應。 我們可以看到它不是一個實際的 Web 服務器,而是通過為應用程序提供一個start_response
回調和一個包含我們環境變量的字典,以類似的方式與我們的應用程序交互。 在請求結束時,它使用響應主體迭代器並返回一個包含其所有內容的字符串。 可以為不同類型的 HTTP 請求創建類似的方法(或通用方法)。
包起來
在本文中,我們沒有討論 WSGI 如何處理文件上傳,因為這可能被認為是更“高級”的功能,不適合作為介紹性文章。 如果您想了解更多信息,請查看有關文件處理的 PEP-3333 部分。
我希望這篇文章有助於更好地理解 Python 如何與 Web 服務器通信,並允許開發人員以有趣和創造性的方式使用這個接口。
致謝
我要感謝我的編輯 Nick McCrea 幫助我完成這篇文章。 由於他的工作,原文變得更加清晰,並且有幾個錯誤沒有得到糾正。