WSGI:Python 的服務器-應用程序接口

已發表: 2022-03-11

1993 年,網絡仍處於起步階段,擁有大約 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 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

雖然我們在這種情況下使用了一個函數,但任何可調用的都可以。 此處應用程序對象的規則是:

  • 必須是帶有environstart_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.inputwsgi.errors

大多數environ鍵都很簡單,但其中兩個需要進一步說明: wsgi.input ,它必須包含一個帶有來自客戶端的請求正文的流,以及wsgi.errors ,應用程序報告它遇到的任何錯誤。 從應用程序發送到wsgi.errors的錯誤通常會發送到服務器錯誤日誌。

這兩個鍵必須包含類似文件的對象; 也就是說,提供接口以作為流讀取或寫入的對象,就像我們在 Python 中打開文件或套接字時獲得的對像一樣。 起初這可能看起來很棘手,但幸運的是,Python 為我們提供了很好的工具來處理這個問題。

首先,我們在談論什麼樣的流? 根據 WSGI 定義, wsgi.inputwsgi.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參數必須是一個可調用的,帶有兩個必需參數,即statusheaders ,以及一個可選參數exc_info 。 在將正文的任何​​部分發送回 Web 服務器之前,應用程序必須調用它。

在本文開頭的第一個應用示例中,我們將響應的主體作為列表返回,因此我們無法控制何時迭代列表。 因此,我們必須在返回列表之前調用start_response

在第二個中,我們在產生響應正文的第一個(並且在本例中是唯一一個)片段之前調用了start_response 。 無論哪種方式在 WSGI 規範中都是有效的。

從 Web 服務器端,調用start_response實際上不應該將標頭髮送給客戶端,而是將其延遲到響應正文中至少有一個非空字節串要發送回客戶端。 這種架構允許在應用程序執行的最後一刻之前正確報告錯誤。

start_responsestatus參數

傳遞給start_response回調的status參數必須是一個由 HTTP 狀態代碼和描述組成的字符串,由一個空格分隔。 有效示例是: '200 OK''404 Not Found'

start_responseheaders參數

傳遞給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_responseexc_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 字符串轉換為bytesstr是一項簡單的任務:

  • 蟒蛇 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 是幾乎所有 Python Web 框架的關鍵部分。

在本文中,我們沒有討論 WSGI 如何處理文件上傳,因為這可能被認為是更“高級”的功能,不適合作為介紹性文章。 如果您想了解更多信息,請查看有關文件處理的 PEP-3333 部分。

我希望這篇文章有助於更好地理解 Python 如何與 Web 服務器通信,並允許開發人員以有趣和創造性的方式使用這個接口。

致謝

我要感謝我的編輯 Nick McCrea 幫助我完成這篇文章。 由於他的工作,原文變得更加清晰,並且有幾個錯誤沒有得到糾正。