Python基礎入門教程:WSGI


Python基礎入門教程:WSGI

WSGI 是什麼

WSGI 是 Python Web Server Gateway Interface 的縮寫,是描述 Web 服務器與 Python 應用程序之間如何交互的接口規範。該規範具體描述在 PEP-3333python

這個規範至關於 Web 服務器和 Python 應用程序之間的橋樑。對於 Web 服務器,WSGI 描述瞭如何把用戶的請求數據交給 Python 應用程序;對於 Python 應用程序,WSGI 描述瞭如何得到用戶的請求數據,如何將請求的處理結果返回給 Web 服務器。web

WSGI 應用程序

符合 WSGI 規範的 Python 應用程序必須是:bash

  • 一個可調用對象(callable object,例如函數、類、實現了 __call__ 方法的實例)服務器

  • 接受兩個由 WSGI Server 提供的參數:app

    • environ 字典,包含 環境變量,請求數據,如 REQUEST_METHOD,PATH_INFO,QUERY_STRING 等框架

    • start_response 函數,開始響應請求的回調函數,用於發送響應狀態(HTTP status) 和響應頭(HTTP headers)異步

  • 返回一個由 bytes 類型元素組成的可迭代對象(一般是一個字節序列),即響應正文(Response body)函數

下面分別以函數、類、實現了 __call__ 方法的實例來演示符合 WSGI 規範的 Python 應用程序:性能

# 可調用對象是一個函數
def simple_app(environ, start_response):
    # 響應狀態(狀態碼和狀態信息)
    status = '200 OK'
    # 響應體
    response_body = b"Hello WSGI"
    # 響應頭,是一個列表,每對鍵值都必須是一個 tuple
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(response_body)))]
    
    # 回調 WSGI 服務器提供的 start_response,返回響應狀態和響應頭
    start_response(status, response_headers)
    
    # 返回響應體,由 bytes 類型元素組成的可迭代對象
    return [response_body]
​
​
# 可調用對象是一個類
class AppClass:
    """可調用對象是 AppClass 類,調用方法: for body in AppClass(env, start_response): process_body(body) """
​
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response
​
    def __iter__(self):
        status = '200 OK'
        response_body = b"Hello WSGI"
        response_headers = [('Content-type', 'text/plain'),
                            ('Content-Length', str(len(response_body)))]
        self.start(status, response_headers)
        yield response_body
​
​
# 可調用對象是一個類實例
class AnotherAppClass:
    """可調用對象是 AnotherAppClass 類實例,調用方法: app = AnotherAppClass() for body in app(env, start_response): process_body(body) """
​
    def __init__(self):
        pass
​
    def __call__(self, environ, start_response):
        status = '200 OK'
        response_body = b"Hello WSGI"
        response_headers = [('Content-type', 'text/plain'),
                            ('Content-Length', str(len(response_body)))]
        start_response(status, response_headers)
        yield response_body複製代碼

WSGI 服務器

跟 WSGI 應用程序對應的 WSGI 服務器須要完成如下工做:學習

  • 接收 HTTP 請求,返回 HTTP 響應

  • 提供 environ 數據,實現回調函數 start_response

  • 調用 WSGI application,並將 environ,start_response 做爲參數傳入

簡化版 WSGI 服務器內部的實現流程:

import os, sys
​
def unicode_to_wsgi(u):
    return u.decode('utf-8')
​
def wsgi_to_bytes(s):
    return s.encode('utf-8')
​
# application 是 WSGI 應用程序,一個可調用對象
def run_with_cgi(application):
    # 準備 environ 參數數據
    # 內部包含本次 HTTP 請求的數據,如 REQUEST_METHOD, PATH_INFO, QUERY_STRING 等
    environ = {k: unicode_to_wsgi(v) for k,v in os.environ.items()}
    # WSGI 環境變量
    environ['wsgi.input']        = sys.stdin.buffer
    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):
        out = sys.stdout.buffer
        
        if not headers_set:
             raise AssertionError("write() before start_response()")
        elif not headers_sent:
             # 在第一次發送響應體以前,發送已經存在的響應頭
             status, response_headers = headers_sent[:] = headers_set
             out.write(wsgi_to_bytes('Status: %s\r\n' % status))
             for header in response_headers:
                 out.write(wsgi_to_bytes('%s: %s\r\n' % header))
             out.write(wsgi_to_bytes('\r\n'))
​
        out.write(data)
        out.flush()
​
    # start_response 回調函數,根據 WSGI 應用程序傳遞過來的 HTTP status 和 response_headers
    # 設置響應狀態和響應頭
    def start_response(status, response_headers, exc_info=None):
        # 處理異常狀況
        if exc_info:
            pass
​
        headers_set[:] = [status, response_headers]
​
        return write
​
    # 調用 WSGI 應用程序,傳入準備好的 environ(請求數據)和 start_response(開始響應回調函數)
    result = application(environ, start_response)
    
    # 處理響應體
    try:
        for data in result:
            if data:
                write(data)
    finally:
        if hasattr(result, 'close'):
            result.close()複製代碼

Middleware

Middleware(中間件) 處於 WSGI 服務器和 WSGI 應用程序之間。對於 WSGI 應用程序它至關於 WSGI 服務器,而對於 WSGI 服務器 它至關於 WSGI 應用程序。它很像 WSGI 應用程序,接收到請求以後,作一些針對請求的處理,同時它又能在接收到響應以後,作一些針對響應的處理。因此 Middleware 的特色是:

  • 被 WSGI 服務器或其餘 Middleware 調用,返回 WSGI 應用程序

  • 調用 WSGI 應用程序,傳入 environstart_response

咱們以白名單過濾和響應後續處理來演示 Middleware:

from wsgiref.simple_server import make_server
​
def app(environ, start_response):
    # 響應狀態(狀態碼和狀態信息)
    status = '200 OK'
    # 響應體
    response_body = b"Hello WSGI"
    # 響應頭,是一個列表,每對鍵值都必須是一個 tuple
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(response_body)))]
    
    # 回調 WSGI 服務器提供的 start_response,返回響應狀態和響應頭
    start_response(status, response_headers)
    
    # 返回響應體,由 bytes 類型元素組成的可迭代對象
    return [response_body]
​
​
# 針對請求數據進行處理的中間件
class WhitelistMiddleware(object):
    def __init__(self, app):
        self.app = app
​
    # 類實例被調用時,根據從請求中得到的 HTTP_HOST 實現白名單功能
    def __call__(self, environ, start_response):
        ip_addr = environ.get('HTTP_HOST').split(':')[0]
        if ip_addr not in ('127.0.0.1'):
            start_response('403 Forbidden', [('Content-Type', 'text/plain')])
​
            return [b'Forbidden']
​
        return self.app(environ, start_response)
​
​
# 針對響應數據進行處理的中間件
class UpperMiddleware(object):
    def __init__(self, app):
        self.app = app
​
    # 類實例被調用時,將響應體的內容轉換成大寫格式
    def __call__(self, environ, start_response):
        for data in self.app(environ, start_response):
            yield data.upper()
​
​
if __name__ == '__main__':
    app = UpperMiddleware(WhitelistMiddleware(app))
    with make_server('', 8000, app) as httpd:
        print("Serving on port 8000...")
​
        httpd.serve_forever()複製代碼

上面例子是一份完整可運行的代碼。函數 app 是 WSGI 應用程序,WhitelistMiddlewareUpperMiddleware 是 WSGI Middleware,WSGI 服務器使用的是 Python 內置的 wsgiref 模塊(wsgiref 模塊是 Python 3 提供的 WSGI 規範的參考實現,wsgiref 中的 WSGI 服務器可用於開發測試,不能使用在生產環境)。

在 WSGI 規範中給出了一些 Middleware 的使用場景,其中根據請求路徑分發到不一樣應用程序的場景,正是一個 Web Framework 最基本的一項功能。下面咱們來看一個經過 Middleware 實現的路由轉發例子:

from wsgiref.simple_server import make_server
​
# 請求 path 分發中間件
class RouterMiddleware(object):
    def __init__(self):
        # 保存 path 與應用程序對應關係的字典
        self.path_info = {}
    
    def route(self, environ, start_response):
        application = self.path_info[environ['PATH_INFO']]
        return application(environ, start_response)
    
    # 類實例被調用時,保存 path 和應用程序對應關係
    def __call__(self, path):
        def wrapper(application):
            self.path_info[path] = application
        return wrapper
​
router = RouterMiddleware()
​
​
@router('/hello')  # 調用 RouterMiddleware 類實例,保存 path 和應用程序對應關係
def hello(environ, start_response):
    status = '200 OK'
    response_body = b"Hello"
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(response_body)))]
    start_response(status, response_headers)
​
    return [response_body]
​
​
@router('/world')
def world(environ, start_response):
    status = '200 OK'
    response_body = b'World'
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(response_body)))]
    start_response(status, response_headers)
​
    return [response_body]
​
​
@router('/')
def hello_world(environ, start_response):
    status = '200 OK'
    response_body = b'Hello World'
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(response_body)))]
    start_response(status, response_headers)
​
    return [response_body]
​
​
def app(environ, start_response):
    return router.route(environ, start_response)
​
​
if __name__ == '__main__':
    with make_server('', 8000, app) as httpd:
        print("Serving on port 8000...")
​
        httpd.serve_forever()複製代碼

WSGI 接口規範描述的 WSGI 應用程序太過於底層,對於開發人員很不友好。人們一般會使用 Web Framework 來完成一個 Web 應用的開發工做,而後會把這個 Web 應用部署在爲生產環境準備的 Web 服務器上。

經常使用的 Python Web Framework:

  • Django

    • 一個功能完備的 Web 框架,擁有龐大的開發者社區和豐富的第三方庫。

  • Flask

    • 一款微型框架,構建更小應用、API 和 web 服務。是任何不適用 Django 的 Python web 應用的默認選擇。

  • Tornado

    • 一個異步 web 框架,原生支持 WebSocket。

  • Bottle

    • 更小的 Web 框架,整個框架只有一個 Python 文件,是不錯的源碼學習案例。

經常使用的 WSGI Web Server:

  • Gunicorn

    • 純 Python 實現的 WSGI 服務器,擁有十分簡單的配置和十分合理的默認配置,使用簡單。

  • uWSGI

    • 基於 uwsgi 協議的,功能十分強大的 Web 服務器,同時也支持 Python WSGI 協議。性能很好,但配置複雜。

相關文章
相關標籤/搜索