所謂 WSGI

注意

  1. 若是你也想閱讀 WSGI 相關的 PEP 規範,建議直接閱讀 PEP 3333,由於 PEP 3333PEP 333 是向下兼容的,也能夠說 PEP 3333 是對 PEP 333 的補充。

何爲 WSGI?

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 應用框架之間的標準接口。程序員

何爲 Web 服務器

首先,什麼是服務器(server)? 通常來講,server 有兩重意思:github

  1. 有時 server 表示硬件,也就是一臺機器,也可稱爲「主機」;
  2. 更多的時候 server 表示軟件程序,這種程序主要用來對外提供某種服務,好比:郵件服務、FTP 服務、數據庫服務、網頁服務等等。

做爲開發者,通常提到 server 時指的都是後者,即一個長時間運行的軟件程序。web

因此,什麼是 Web Server? 通俗的來說 Web Server 就是一個提供 Web 服務的應用程序。 常見的符合 WSGI 規範的 Web Server 有 uWSGI、gunicorn 等等。數據庫

何爲 Web 應用或框架

Web 框架在現在是比較常見的,比較知名的 Python Web 框架有:Django、Flask、Pyramid等等。反卻是 Web 應用不太常見,(我的理解)通常狀況下只有在本地測試的時候會寫一些簡單的 Python Web 應用,平時的開發大多仍是使用開源(或公司內部)的 Web 框架。編程

爲何須要 WSGI

做爲一個近兩年剛接觸到 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 只是一個規範並非實際的代碼,準確的來講應該是「符合 WSGI 規範的 Web 體系作了什麼事情?」

上面已經提到,WSGI 經過規範化 Web 框架和 Web 服務器之間的接口,讓兼容了 WSGI 的框架和服務器可以自由組合使用……

因此,WSGI 究竟作了什麼,讓一切變得如此簡單?

PEP 3333 中對 WSGI 進行了一段簡單的概述,這裏我結合看過的 一篇博文 進行簡單的歸納:

(簡單來講)WSGI 將 Web 分紅了三個部分,從上到下分別是:Application/Framework, Middleware 和 Server/Grageway,各個部分之間高度解耦儘量的作到不互相依賴。

  1. (一般狀況下)客戶端(通常爲瀏覽器)會向 Server 發送 HTTP 請求以獲取數據。
  2. 符合 WSGI 規範的 Server 在接收到請求以後會調用指定的符合 WSGI 規範的 Web Application,並傳入 environ 和 start_response 兩個參數(並不強制命名,只是通常會這麼命名)。
  3. Web Application 在接收到請求後會生成一個打包好的 HTTP Response 傳給 start_response。
  4. Server 會將 HTTP Response 進行彙總待請求處理完且沒有錯誤時將整個 HTTP Response 內容返回給客戶端。

Middleware 屬於三個部分中最爲特別的一個,對於 Server 他是一個 Application,對於 Application 它是一個 Server。通俗的來講就是 Middleware 面對 Server 時可以展示出 Application 應有的特性,而面對 Application 時可以展示出 Server 應有的特性,因爲這一特色 Middleware 在整個協議中起到了承上啓下的功能。在現實開發過程當中,還能夠經過嵌套 Middleware 以實現更強大的功能。

WSGI 是如何工做的?

經過上一小節可以大概的瞭解到 WSGI 在一次完整的請求中究竟作了什麼。下面再來介紹一下一個完整的 WSGI Web 體系是如何工做的。

一個符合 WSGI 規範的 Python 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 的模塊名)中。

注:如下操做默認你徹底按照示例代碼中給出的命名進行文件命名

  1. 啓動 server:python /path_to_code/run.py
  2. 經過瀏覽器瀏覽 127.0.0.1:8888 查看效果
  3. 經過 curl 命令 curl -v http://127.0.0.1:8888 查看完整輸出
  4. 對比 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 程序的運行流程:

  1. 建立 wsgi server socket 實例對象(調用 server.__init__ 方法)
  2. 將準備好的 middleware 以及 application 對象導入給 server 實例(調用 server.set_app 方法)
  3. 運行 server 監聽指定端口(調用 server.server_forever 方法)

經過 server.py 中的代碼可以清晰的看到一個 WSGI 類型的 Web 程序是如何處理 HTTP 請求的:

  1. 經過 server_forever 監聽到客戶端請求並記錄請求信息
  2. 調用 handle_one_request 方法處理此請求
    1. 經過請求 socket 獲取請求數據
    2. 經過 parse_request 方法將請求數據解析成所需格式
    3. 經過 get_environ 方法利用現有數據構造環境變量字典
    4. 將生成好的 environ 參數和 start_response 方法傳給 application 對象(也多是 middleware 假裝的 application 對象),並獲取響應結果
    5. 將響應結果傳給 finish_response 方法構造一個可迭代的響應對象返回給客戶端並結束本次請求

經過 middleware.py 中的代碼就可以理解一個 WSGI 中間件是如何工做的:

  1. 經過在 __init__ 方法中接收一個 application 將本身假裝成一個 server
  2. 經過在 __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 並查看結果以下:

瀏覽器結果 1
瀏覽器結果 2
瀏覽器結果 3

經過控制檯能夠清晰地看到響應頭和響應主體的內容是符合咱們預期的

curl 結果分析

經過 curl http://127.0.0.1:8888 能夠看到響應主體:

curl 結果 1

經過 curl -v http://127.0.0.1:8888 能夠看到詳細的請求和響應內容:

curl 結果 2

經過 curl -v https://baidu.com 獲取百度首頁的響應內容以做比較:

curl 結果 3

能夠看到目前瀏覽網頁經常使用的正常請求要比本身構建的測試示例要複雜的多,這也是爲何常用 Web 框架而非單文件應用來處理這些請求的緣由。

解讀 PEP-3333 中的某些細節

PEP 3333 我只讀到了 Buffering and Streaming 章節,而且沒能很好的理解此章節所描述的東西,所以在下面的細節分析中大都是此章節以前的一些內容。

可迭代對象和可調用對象

可迭代對象(callable)和可迭代對象(iterable)在 PEP 3333 中最多見的兩個詞彙,在 WSGI 規範中它們分別表明:實現了 __call__ 的對象和實現了 __iter__ 的對象。

Unicode | bytes | str

這是一組比較基礎的概念:

  1. Unicode 是一種字符編碼標準
  2. bytes 和 str 是 Python 中兩種不一樣的數據類型

Python3 中字符串的默認類型是 str,在內存中以 Unicode 表示。若是要在網絡中傳輸或保存爲磁盤文件,須要將 str 轉換爲 bytes 類型。

Unicode | UCS | UTF

  1. Unicode(萬國碼、國際碼、統一碼、單一碼)是計算機科學領域裏的一項業界標準。它對世界上大部分的文字系統進行了整理、編碼,使得電腦能夠用更爲簡單的方式來呈現和處理文字。Unicode 伴隨着通用字符集的標準而發展,同時也以書本的形式對外發表。
  2. UCS(Universal Character Set,通用字符集)是由ISO制定的ISO 10646(或稱ISO/IEC 10646)標準所定義的標準字符集。
  3. UTF(Unicode Transformation Format),Unicode 定義了兩種映射方式:一種叫 the Unicode Transformation Format (UTF) 編碼, 還有一種叫 Universal Character Set (UCS) 編碼。一種編碼映射必定範圍(多是子集)的 Unicode 碼點(code points )成代碼值(code value)的序列。編碼名字後面的數字表明一個代碼值的位數(UTF使用位數,UCS 使用字節數),UTF-8 和UTF-16是最常使用的編碼。

bytes | str

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等)

from 知乎-猿人學-Python 3 中str 和 bytes 的區別

WSGI 中的 String

WSGI 中規定了兩種 String:

  1. Native String(常說的 str)用來表示 request/response headers and metadata
  2. ByteString(Python3 中用 byte type 來表示)用於 request/response 的 body(例如:PUT/POST 輸入和 HTML 頁面輸出)

PEP 3333 中有對這部分的詳細說明。

三個主要組成部件

瞭解了以上基礎概念以後再具體的看一下 WSGI 的三個主要組成部件:

Application/Framework | 下文簡稱 application

  1. application 是一個必須且只能接收兩個參數的 callable,形如 application(environ, start_response)。並且這兩個參數只能以位置參數的形式被傳入。
  2. environ 和 start_response 只是習慣性命名,對於具體傳入的對象名稱沒有作要求。
  3. application 必須可被屢次調用,由於全部的 server/gateway(CGI 除外)都會發出此類的重複請求。
  4. environ 是一個字典參數,包含了 CGI 風格的環境變量。必須使用內置的 Python 字典類型(不能是子類或自定義的 UserDict),而且容許 application 以任何它想要的方式修改。字典還包括某些 WSGI 變量,而且還可能包括 server 特定的拓展參數,它們的命名須要遵照相應規範
  5. start_response 參數也是一個 callable,接收兩個必要的未知參數和一個可選參數,三個參數依次默認命名爲:status, response_headers, exc_info,即 start_response(status, response_headers, exc_info=None)
  6. status 是一個狀態字符串(str),例如:"200 OK"
  7. response_headers 是一個描述 HTTP Response Headers 的 (header_name, header_value) 元組列表
  8. 可選參數 exc_info 只有當 application 捕獲到錯誤而且視圖向瀏覽器(客戶端)顯示時纔會調用。
  9. start_response callable 必須返回一個 write(body_data) callable,這個 callable 須要一個位置參數:一個要做爲 HTTP 響應體一部分的 bytestring(注意:wirte callabel 只是爲了支持某些現有框架的必要輸出 API 而提供的;若是能夠避免的話,新的 application/gateway 應該避免使用它)。
  10. 當 callable(若是實現了 write 這個 callable 指的就是 write;若是沒有,這個 callable 指的就是 start_response 自己)被 server 調用時,必須返回一個產生零個或多個字符串的 iterable。能夠經過多種方式實現,如:一個字符串列表、application 是一個 generator 函數或 application 是一個實現了 __iter__ 的對象。不管如何,application 必須返回一個可以產生零個或多個字符串 iterable。
  11. application 應該負責確保被寫入的字符串是適合 client 的格式的。
  12. 若是 len(iterable) 可以被成功執行(這裏的 iterable 指的是第 10 條中的 iterable)則其返回的必須是一個 server 可以信賴的結果。也就是說 application 返回的 iterable 若是提供了一個有效的 __len__ 方法就必須可以得到準確值
  13. 若是 application 返回的 iterable 有 close 方法,server 必須在當前請求完成後調用它,不管請求是否正常完成(爲了支持 application 釋放資源)。
  14. application 應該檢查其所須要的變量是否存在並對變量不存在的狀況作好處理方案。

Server/Gateway | 下文簡稱 server

  1. server 必須以無緩衝(unbuffered)的方式將 yielded bytestrings 傳輸到 client,在下一次請求以前完成每個 bytestring 的傳輸。換句話說 application 應該本身實現緩存。(對於這部分我理解的不是很透徹,大多都是直譯的 PEP 3333
  2. server 不能直接使用 application 返回的 iterable 的其餘屬性。
  3. server 應該儘量多的提供 CGI 變量。
  4. 符合 WSGI 規範的 server 應該記錄所提供的變量。

Middleware

  1. middleware 是一個單獨的對象,可能在一些 application 中扮演 server 同時在一些 server 中扮演 application。

WSGI 中的坑

  1. 要肯定在那些地方使用 str,在那些地方使用 bytes

Python wsgiref 官方庫源碼分析

能夠參考個人開源庫 read-python 中 practices/for_wsgiref 目錄下的 server.py 文件。

在這個文件中我提取了 Python wsgiref 官方庫的必要代碼匯聚成一個文件實現了一個和 wsgiref.WSGIServer 大體一樣功能的 WSGIServer 類。

Python wsgiref 官方庫對 WSGI 規範的實現更加抽象,加上一些歷史緣由使得代碼分佈在多個官方庫中,我在抽離代碼的過程當中學到了不少可是一樣也產生了不少困惑,我在源碼中使用 TODO 疑惑 XXX 的形式將個人困惑表達出來了,若是你感興趣而且剛好知道解決我疑惑的方法,歡迎直接給個人代碼倉庫提交 Issues。

參考

  1. PEP 333 Python Web Server Gateway Interface v1.0
  2. PEP 3333 Python Web Server Gateway Interface v1.0.1
  3. 知乎-方應杭-「每日一題」什麼是 Web 服務器(server)
  4. Skyline75489-Python WSGI學習筆記
  5. Huang Huang 的博客-翻譯項目系列-讓咱們一塊兒來構建一個 Web 服務器
  6. 掘金- liaochangjiang-Python Web開發:開發wsgi中間件
  7. 維基百科-Unicode
  8. 維基百科-通用字符集
  9. 知乎-猿人學-Python 3 中str 和 bytes 的區別
  10. Python 官方文檔-術語對照表
相關文章
相關標籤/搜索