This document specifies a proposed standard interface between web servers and Python web applications or frameworks, to promote web application portability across a variety of web servers.html
本文檔詳細描述了一個建議用在 Web 服務器和 Python Web 應用或框架之間的標準接口,以提高 Web 應用在各種 Web 服務器之間的可移植性。python
from PEP 3333git
從 PEP 3333 的這段總結來看,WSGI 就是一個 Python 官方建議用在 Web 服務器和 Python Web 應用框架之間的標準接口。程序員
首先,什麼是服務器(server)? 通常來講,server 有兩重意思:github
做爲開發者,通常提到 server 時指的都是後者,即一個長時間運行的軟件程序。web
因此,什麼是 Web Server? 通俗的來說 Web Server 就是一個提供 Web 服務的應用程序。 常見的符合 WSGI 規範的 Web Server 有 uWSGI、gunicorn 等等。數據庫
Web 框架在現在是比較常見的,比較知名的 Python Web 框架有:Django、Flask、Pyramid等等。反卻是 Web 應用不太常見,(我的理解)通常狀況下只有在本地測試的時候會寫一些簡單的 Python Web 應用,平時的開發大多仍是使用開源(或公司內部)的 Web 框架。編程
做爲一個近兩年剛接觸到 Python Web 編程的新手,在平常的編程過程當中徹底沒有見過所謂的 WSGI,可是我依然能夠寫好一個完整的 Web 應用,這是爲何?WSGI 有存在的必要嘛?瀏覽器
答案確定是:有存在的必要。緩存
首先解釋一下爲何我在過去兩年的過程當中沒有見過 WSGI 卻依舊能夠進行 Web 編程:由於如今的大多數框架都已經幫咱們將 WSGI 標準封裝在框架底層。甚至,我用的 Django REST Framework 框架連 HTTP Request 和 HTTP Response 都幫我封裝好了。因此,就算我徹底不瞭解 WSGI 這種偏底層的協議也可以進行平常的 Web 開發。
那 WSGI 到底解決了什麼問題?這個在 PEP 3333 中有詳細的解釋,簡單的說一下個人理解:在 WSGI 誕生以前,就已經存在了大量使用 Python 編寫的 Web 應用框架,相應的也存在不少 Web 服務器。可是,各個 Python Web 框架和 Python Web 服務器之間不能互相兼容。誇張一點說,在當時若是想要開發一個 Web 框架說不定還得單獨爲這個框架開發一個 Web 服務器(並且這個服務器別的框架還不能用)。爲了解決這一現象 Python 社區提交了 PEP 333,正式提出了 WSGI 這個概念。
簡單的理解:只要是兼容 WSGI 的 Web 服務器和 Web 框架就能配套使用。開發服務器的程序員只須要考慮在兼容 WSGI 的狀況下如何更好的提高服務器程序的性能;開發框架的程序員只須要考慮在兼容 WSGI 的狀況下如何適應儘量多業務開發邏輯(以上只是舉例並不是真的這樣)。
WSGI 解放了 Web 開發者的精力讓他們能夠專一於本身須要關注的事情。
注:爲了簡練而寫成了 WSGI 作了什麼事情,實際上 WSGI 只是一個規範並非實際的代碼,準確的來講應該是「符合 WSGI 規範的 Web 體系作了什麼事情?」
上面已經提到,WSGI 經過規範化 Web 框架和 Web 服務器之間的接口,讓兼容了 WSGI 的框架和服務器可以自由組合使用……
因此,WSGI 究竟作了什麼,讓一切變得如此簡單?
在 PEP 3333 中對 WSGI 進行了一段簡單的概述,這裏我結合看過的 一篇博文 進行簡單的歸納:
(簡單來講)WSGI 將 Web 分紅了三個部分,從上到下分別是:Application/Framework, Middleware 和 Server/Grageway,各個部分之間高度解耦儘量的作到不互相依賴。
Middleware 屬於三個部分中最爲特別的一個,對於 Server 他是一個 Application,對於 Application 它是一個 Server。通俗的來講就是 Middleware 面對 Server 時可以展示出 Application 應有的特性,而面對 Application 時可以展示出 Server 應有的特性,因爲這一特色 Middleware 在整個協議中起到了承上啓下的功能。在現實開發過程當中,還能夠經過嵌套 Middleware 以實現更強大的功能。
經過上一小節可以大概的瞭解到 WSGI 在一次完整的請求中究竟作了什麼。下面再來介紹一下一個完整的 WSGI Web 體系是如何工做的。
爲了方便展現先來構建一個符合 WSGI 規範的 Python Web 項目示例:
注:示例基於 Python3
# 本示例代碼改自參考文章 5:
# Huang Huang 的博客-翻譯項目系列-讓咱們一塊兒來構建一個 Web 服務器
# /path_to_code/server.py
# Examples of wsgi server
import sys
import socket
# 根據系統導入響應的 StringIO 模塊
# StringIO:用於文本 I/O 的內存數據流
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
class WSGIServer(object):
request_queeu_size = 1 # 請求隊列長度
address_family = socket.AF_INET # 設置地址簇
socket_type = socket.SOCK_STREAM # 設置 socket 類型
def __init__(self, server_address):
# Server 初始化方法(構造函數)
# Create a listening socket
self.listen_socket = listen_socket = socket.socket(
self.address_family,
self.socket_type
)
# 設置 socket 容許重複使用 address
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind 綁定端口
listen_socket.bind(server_address)
# Activate 激活
listen_socket.listen(self.request_queeu_size)
# 獲取並記錄 server host 和 port
host, port = self.listen_socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
# Return headers set by Web framework/application
self.headers_set = []
def set_app(self, application):
# 將傳入的 application 設置爲實例屬性
self.application = application
def server_forever(self):
# 開啓 server 循環函數
listen_socket = self.listen_socket
while True:
# 獲取 client socket 參數 | client_connection 是 client socket 實例
# 這裏會建立一個阻塞,直到接受到 client 鏈接爲止
self.client_connection, client_address = listen_socket.accept()
# 調用 handle_one_request 方法處理一次請求並關閉 client 鏈接而後繼續等待新的鏈接進入
self.handle_one_request()
def handle_one_request(self):
# 處理請求的入口方法 | 用來處理一次請求
# 從 client socket 中獲取 request data
self.request_data = request_data = self.client_connection.recv(1024)
# 調用 parse_request 方法, 傳入接收到的 request_data 並對其進行解析
self.parse_request(request_data)
# 經過已有數據構造環境變量字典
environ = self.get_environ()
# 調用 application,傳入已經生成好的 environ 和 start_response,返回一個可迭代的 Response 對象
result = self.application(environ, self.start_response)
# 調用 finish_response 方法,構造一個響應並返回給客戶端
self.finish_response(result)
def parse_request(self, text):
# 取行
request_line = text.splitlines()[0]
# 打碎請求行到組件中
(self.request_method,
self.path,
self.request_version
) = request_line.split()
def get_environ(self):
env = {}
env["wsgi.version"] = (1, 0)
env["wsgi.url_scheme"] = "http"
env["wsgi.input"] = StringIO(self.request_data.decode("utf-8"))
env["wsgi.errors"] = sys.stderr
env["wsgi.multithread"] = False
env["wsgi.multiprocess"] = False
env["wsgi.run_once"] = False
# Required CGI variables
env["REQUEST_METHOD"] = self.request_method
env["PATH_INFO"] = self.path.decode("utf-8")
env["SERVER_NAME"] = self.server_name
env["SERVER_PORT"] = str(self.server_port)
return env
def start_response(self, status, response_headers, exc_info=None):
# 按照 WSGI 規範提供一個 start_response 給 application
# Add necessary必要的 server headers
server_headers = [
("Date", "Tue, 31 Mar 2020 12:51:48 GMT"),
("Server", "WSGIServer 0.2")
]
self.headers_set = [status, response_headers + server_headers]
# 按照 WSGI 協議,應該在這裏返回一個 write(),但這裏爲了簡便就省略了
# 會在後續分析 wsgiref 源碼時說起此處
def finish_response(self, result):
# 經過現有參數整理出一個響應體
try:
status, response_headers = self.headers_set
# 響應第一部分:HTTP 協議以及狀態碼
response = f"HTTP/1.1 {status}\r\n"
# 響應第二部分:將生成好的響應頭遞歸式的傳入響應體內
for header in response_headers:
response += "{0}: {1}\r\n".format(*header)
# 經過 \r\n 進行空行
response += "\r\n"
# 響應第三部分:將響應主題信息追加到響應體內
for data in result:
response += data
# 經過 senall 將響應發送給客戶端
# 注意:在 Python3 下,若是你構建的響應體爲 str 類型,須要進行 encode 轉換爲 bytes
self.client_connection.sendall(response.encode())
finally:
# 關閉鏈接
self.client_connection.close()
複製代碼
# /path_to_code/middleware.py
# Examples of wsgi middleware
class TestMiddleware(object):
def __init__(self, application):
self.application = application
def core(self, environ, start_response):
old_response = self.application(environ, start_response)
new_response = old_response + ["middleware add this message\n"]
return new_response
def __call__(self, environ, start_response):
return self.core(environ, start_response)
複製代碼
# /path_to_code/application.py
# Examples of wsgi application
def application(environ, start_response):
status = "200 OK"
response_headers = [("Content-Type", "text/plain")]
start_response(status, response_headers)
return ["hello world from a simple WSGI application!\n"]
複製代碼
# /path_to_code/run.py
# running Example
from server import WSGIServer
from application import application
from middleware import TestMiddleware
# 規定 server host 和 server port
server_address = (host, port) = "", 8888
# 建立 server 實例
server = WSGIServer(server_address)
# 設置本 server 對應的 middleware 以及 application
server.set_app(TestMiddleware(application))
# 輸出提示性語句
print(f"WSGIServer: Serving HTTP on port: {port}...\n")
# 進入 server socket 監聽循環
server.server_forever()
複製代碼
將四段代碼分別複製到同一目錄的四個文件(若是沒有按照示例給出的命名記得更改一下 run 模塊中相應的 import 的模塊名)中。
注:如下操做默認你徹底按照示例代碼中給出的命名進行文件命名
python /path_to_code/run.py
127.0.0.1:8888
查看效果curl -v http://127.0.0.1:8888
查看完整輸出curl -v https://baidu.com
的輸出查看區別上面我根據 WSGI 協議編寫了三個文件(模塊):server.py middleware.py application.py,分別對應 WSGI 裏 server middleware application 這三個概念。而後經過 run.py 引入三個模塊組成了一個完整的 server-middleware-application Web 程序並監聽本地 8888 端口。
經過 run.py 中的代碼咱們可以清晰的看到一個 WSGI 類型的 Web 程序的運行流程:
server.__init__
方法)server.set_app
方法)server.server_forever
方法)經過 server.py 中的代碼可以清晰的看到一個 WSGI 類型的 Web 程序是如何處理 HTTP 請求的:
server_forever
監聽到客戶端請求並記錄請求信息handle_one_request
方法處理此請求
parse_request
方法將請求數據解析成所需格式get_environ
方法利用現有數據構造環境變量字典finish_response
方法構造一個可迭代的響應對象返回給客戶端並結束本次請求經過 middleware.py 中的代碼就可以理解一個 WSGI 中間件是如何工做的:
__init__
方法中接收一個 application 將本身假裝成一個 server__call__
方法中接收 environ 和 start_response 參數將本身假裝成一個 application 經過這兩點假裝 middleware 可以很好的粘合在 server 和 application 之間完成中間邏輯處理,在 PEP 3333 中指明瞭中間件的幾點常見用途。至於 application.py 在這裏就真的只是一個簡單的單文件 WSGI 應用。固然也能夠嘗試用寫好的 server.py 和 middleware.py 對接像 Django 這樣的框架,但須要對代碼作一些修改,這裏就不展開討論了,有興趣能夠本身嘗試。
在運行 run.py 以後使用瀏覽器瀏覽 127.0.0.1:8888
並查看結果以下:
經過控制檯能夠清晰地看到響應頭和響應主體的內容是符合咱們預期的
經過 curl http://127.0.0.1:8888
能夠看到響應主體:
經過 curl -v http://127.0.0.1:8888
能夠看到詳細的請求和響應內容:
經過 curl -v https://baidu.com
獲取百度首頁的響應內容以做比較:
能夠看到目前瀏覽網頁經常使用的正常請求要比本身構建的測試示例要複雜的多,這也是爲何常用 Web 框架而非單文件應用來處理這些請求的緣由。
PEP 3333 我只讀到了 Buffering and Streaming 章節,而且沒能很好的理解此章節所描述的東西,所以在下面的細節分析中大都是此章節以前的一些內容。
可迭代對象(callable)和可迭代對象(iterable)在 PEP 3333 中最多見的兩個詞彙,在 WSGI 規範中它們分別表明:實現了 __call__
的對象和實現了 __iter__
的對象。
這是一組比較基礎的概念:
Python3 中字符串的默認類型是 str,在內存中以 Unicode 表示。若是要在網絡中傳輸或保存爲磁盤文件,須要將 str 轉換爲 bytes 類型。
Python3 裏面的 str 是在內存中對文本數據進行使用的,bytes 是對二進制數據使用的。
str 能夠 encode 爲 bytes,可是 bytes 不必定能夠 decode 爲 tr。實際上
bytes.decode(‘latin1’)
能夠稱爲 str,也就是說 decode 使用的編碼決定了decode()
的成敗,一樣的,UTF-8 編碼的 bytes 字符串用 GBK 去decode()
也會出錯。bytes通常來自網絡讀取的數據、從二進制文件(圖片等)讀取的數據、以二進制模式讀取的文本文件(.txt, .html, .py, .cpp等)
WSGI 中規定了兩種 String:
在 PEP 3333 中有對這部分的詳細說明。
瞭解了以上基礎概念以後再具體的看一下 WSGI 的三個主要組成部件:
application(environ, start_response)
。並且這兩個參數只能以位置參數的形式被傳入。start_response(status, response_headers, exc_info=None)
。"200 OK"
__iter__
的對象。不管如何,application 必須返回一個可以產生零個或多個字符串 iterable。len(iterable)
可以被成功執行(這裏的 iterable 指的是第 10 條中的 iterable)則其返回的必須是一個 server 可以信賴的結果。也就是說 application 返回的 iterable 若是提供了一個有效的 __len__
方法就必須可以得到準確值。能夠參考個人開源庫 read-python 中 practices/for_wsgiref 目錄下的 server.py 文件。
在這個文件中我提取了 Python wsgiref 官方庫的必要代碼匯聚成一個文件實現了一個和 wsgiref.WSGIServer
大體一樣功能的 WSGIServer
類。
Python wsgiref 官方庫對 WSGI 規範的實現更加抽象,加上一些歷史緣由使得代碼分佈在多個官方庫中,我在抽離代碼的過程當中學到了不少可是一樣也產生了不少困惑,我在源碼中使用 TODO 疑惑 XXX
的形式將個人困惑表達出來了,若是你感興趣而且剛好知道解決我疑惑的方法,歡迎直接給個人代碼倉庫提交 Issues。