在 從零開始搭建論壇(一):Web服務器與Web框架 中咱們弄清楚了Web 服務器、Web 應用程序、Web框架的概念。對於 Python 來講,愈來愈多的 Web 框架面世,在給咱們更多選擇機會的同時,也限制了咱們對於 Web Server 的選擇。一樣是有着不少 Web 框架的Java,由於有着 servlet API 的存在,任何Java Web框架寫的應用程序均可以運行在任意一個 Web Server 上。html
Python 社區固然也須要這樣一套 API,來適配Web服務器和應用程序,這套 API 就是 WSGI(Python Web Server Gateway Interface),在 PEP 3333 裏有詳細的說明。簡單來講,WSGI是鏈接Web服務器和Web應用程序的橋樑,一方面從Web server 拿到原始 HTTP 數據,處理成統一格式後交給 Web 應用程序,另外一方面從應用程序/框架這邊進行業務邏輯處理,生成響應內容後交給服務器。python
Web服務器和框架經過 WSGI 來進行耦合的詳細過程以下圖所示:git
具體解釋以下:web
下面分別從服務器端和應用程序端來看看 WSGI 是如何作適配的。flask
咱們知道客戶端(一般是瀏覽器)發出的每一個HTTP請求由請求行、消息報頭、請求正文三部分組成,裏面包含了本次請求的相關細節內容。好比:瀏覽器
服務器從客戶端接收HTTP請求以後,WSGI 接口必需要對這些請求字段進行統一化處理,方便傳給應用服務器接口(其實就是給框架)。Web服務器具體傳遞哪些數據給應用程序,早在CGI(Common Gateway Interface,通用網關接口)裏就有詳細規定,這些數據被叫作 CGI 環境變量。WSGI 沿用了 CGI 環境變量的內容,要求 Web 服務器必須建立一個字典用來保存這些環境變量(通常將其命名爲 environ
)。除了 CGI 定義的變量,environ 還必須保存一些WSGI定義的變量,此外還能夠保存一些客戶端系統的環境變量,能夠參考 environ Variables 來看看具體有哪些變量。ruby
接着 WSGI 接口必須將 environ 交給應用程序去處理,這裏 WSGI 規定應用程序提供一個可調用對象 application,而後服務器去調用 application,得到返回值爲HTTP響應正文。服務器在調用 application 的時候,須要提供兩個變量,一個是前面提到的變量字典environ,另外一個是可調用對象 start_response,它產生狀態碼和響應頭,這樣咱們就獲得了一個完整的HTTP響應。Web 服務器將響應返回給客戶端,一次完整的HTTP請求-響應
過程就完成了。服務器
Python 中內置了一個實現了WSGI接口的 Web 服務器,在模塊wsgiref中,它是用純Python編寫的WSGI服務器的參考實現,咱們一塊兒來簡單分析一下它的實現。首先假設咱們用下面代碼啓動一個 Web 服務器:網絡
# Instantiate the server httpd = make_server( 'localhost', # The host name 8051, # A port number where to wait for the request application # The application object name, in this case a function ) # Wait for a single request, serve it and quit httpd.handle_request()
而後咱們以Web服務器接收一個請求、生成 environ,而後調用 application 來處理請求這條主線來分析源碼的調用過程,簡化以下圖所示:
WSGI Server 調用流程
這裏主要有三個類,WSGIServer,WSGIRequestHandler,ServerHandle。WSGIServer 是Web服務器類,能夠提供server_address(IP:Port)和 WSGIRequestHandler 類來進行初始化得到一個server對象。該對象監聽響應的端口,收到HTTP請求後經過 finish_request 建立一個RequestHandler 類的實例,在該實例的初始化過程當中會生成一個 Handle 類實例,而後調用其 run(application) 函數,在該函數裏面再調用應用程序提供的 application對象來生成響應。
這三個類的繼承關係以下圖所示:
其中 TCPServer 使用 socket 來完成 TCP 通訊,HTTPServer 則是用來作 HTTP 層面的處理。一樣的,StreamRequestHandler 來處理 stream socket,BaseHTTPRequestHandler 則是用來處理 HTTP 層面的內容,這部分和 WSGI 接口關係不大,更多的是 Web 服務器的具體實現,能夠忽略。
若是上面的 wsgiref 過於複雜的話,下面一塊兒來實現一個微小的 Web 服務器,便於咱們理解 Web 服務器端 WSGI 接口的實現。代碼摘自 本身動手開發網絡服務器(二),放在 gist 上,主要結構以下:
class WSGIServer(object): # 套接字參數 address_family, socket_type = socket.AF_INET, socket.SOCK_STREAM request_queue_size = 1 def __init__(self, server_address): # TCP 服務端初始化:建立套接字,綁定地址,監聽端口 # 獲取服務器地址,端口 def set_app(self, application): # 獲取框架提供的 application self.application = application def serve_forever(self): # 處理 TCP 鏈接:獲取請求內容,調用處理函數 def handle_request(self): # 解析 HTTP 請求,獲取 environ,處理請求內容,返回HTTP響應結果 env = self.get_environ() result = self.application(env, self.start_response) self.finish_response(result) def parse_request(self, text): # 解析 HTTP 請求 def get_environ(self): # 分析 environ 參數,這裏只是示例,實際狀況有不少參數。 env['wsgi.url_scheme'] = 'http' ... env['REQUEST_METHOD'] = self.request_method # GET ... return env def start_response(self, status, response_headers, exc_info=None): # 添加響應頭,狀態碼 self.headers_set = [status, response_headers + server_headers] def finish_response(self, result): # 返回 HTTP 響應信息 SERVER_ADDRESS = (HOST, PORT) = '', 8888 # 建立一個服務器實例 def make_server(server_address, application): server = WSGIServer(server_address) server.set_app(application) return server
目前支持 WSGI 的成熟Web服務器有不少,Gunicorn是至關不錯的一個。它脫胎於ruby社區的Unicorn,成功移植到python上,成爲一個WSGI HTTP Server。有如下優勢:
和服務器端相比,應用程序端(也能夠認爲框架)要作的事情就簡單不少,它只須要提供一個可調用對象(通常習慣將其命名爲application),這個對象接收服務器端傳遞的兩個參數 environ 和 start_response。這裏的可調用對象不只能夠是函數,還能夠是類(下面第二個示例)或者擁有 __call__
方法的實例,總之只要能夠接受前面說的兩個參數,而且返回值能夠被服務器進行迭代便可。
Application 具體要作的就是根據 environ 裏面提供的關於 HTTP 請求的信息,進行必定的業務處理,返回一個可迭代對象,服務器端經過迭代這個對象,來得到 HTTP 響應的正文。若是沒有響應正文,那麼能夠返回None。
同時,application 還會調用服務器提供的 start_response,產生HTTP響應的狀態碼和響應頭,原型以下:
def start_response(self, status, headers,exc_info=None):
到這裏爲止,咱們就能夠實現一個簡單的 application 了,以下所示:Application 須要提供 status:一個字符串,表示HTTP響應狀態字符串,還有 response_headers: 一個列表,包含有以下形式的元組:(header_name, header_value),用來表示HTTP響應的headers。同時 exc_info 是可選的,用於出錯時,server須要返回給瀏覽器的信息。
def simple_app(environ, start_response): """Simplest possible application function""" HELLO_WORLD = "Hello world!\n" status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [HELLO_WORLD]
class AppClass:或者用類實現以下。
"""Produce the same output, but using a class""" def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): ... HELLO_WORLD = "Hello world!\n" yield HELLO_WORLD
若是想使用 AppClass 類的對象做爲 application,那麼必須給類添加一個 __call__
方法,接受 environ 和 start_response 爲參數,返回可迭代對象,以下所示:注意這裏 AppClass
類自己就是 application,用 environ 和 start_response 調用(實例化)它返回一個實例對象,這個實例對象自己是可迭代的,符合 WSGI 對 application 的要求。
class AppClass: """Produce the same output, but using an object""" def __call__(self, environ, start_response): ...
Flask 中的 WSGI這部分涉及到python的一些高級特性,好比 yield 和 magic method,能夠參考我總結的python語言要點來理解。
flask 是一個輕量級的Python Web框架,符合 WSGI 的規範要求。它的最第一版本只有 600 多行,相對便於理解。下面咱們來看下它最第一版本中關於 WSGI 接口的部分。
def wsgi_app(self, environ, start_response): """The actual WSGI application. This is not implemented in `__call__` so that middlewares can be applied: app.wsgi_app = MyMiddleware(app.wsgi_app) """ with self.request_context(environ): rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() response = self.make_response(rv) response = self.process_response(response) return response(environ, start_response) def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`""" return self.wsgi_app(environ, start_response)
中間件這裏的 wsgi_app 實現了咱們說的 application 功能,rv 是 對請求的封裝,response 是框架用來處理業務邏輯的具體函數。這裏對 flask 源碼不作過多解釋,感興趣的能夠去github下載,而後check 到最第一版本去查看。
前面 flask 代碼 wsgi_app 函數的註釋中提到不直接在 __call__
中實現 application 部分,是爲了可使用中間件
。 那麼爲何要使用中間件,中間件又是什麼呢?
回顧前面的 application/server 端接口,對於一個 HTTP 請求,server 端老是會調用一個 application 來進行處理,並返回 application 處理後的結果。這足夠應付通常的場景了,不過並不完善,考慮下面的幾種應用場景:
上面這些場景有一個共同點就是,有一些必需的操做無論放在服務端仍是應用(框架)端都不合適。對應用端來講,這些操做應該由服務器端來作,對服務器端來講,這些操做應該由應用端來作。爲了處理這種狀況,引入了中間件
。
中間件就像是應用端和服務端的橋樑,來溝通兩邊。對服務器端來講,中間件表現的像是應用端,對應用端來講,它表現的像是服務器端。以下圖所示:
flask 框架在 Flask 類的初始化代碼中就使用了中間件:
self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { self.static_path: target })
下面來看看 DispatcherMiddleware 的實現:這裏的做用和 python 中的裝飾器同樣,就是在執行 self.wsgi_app 先後執行 SharedDataMiddleware 中的一些內容。中間件作的事,很相似python中裝飾器作的事情。SharedDataMiddleware 中間件是 werkzeug 庫提供的,用來支持站點託管靜態內容。此外,還有DispatcherMiddleware 中間件,用來支持根據不一樣的請求,調用不一樣的 application,這樣就能夠解決前面場景 1, 2 中的問題了。
class DispatcherMiddleware(object): """Allows one to mount middlewares or applications in a WSGI application. This is useful if you want to combine multiple WSGI applications:: app = DispatcherMiddleware(app, { '/app2': app2, '/app3': app3 }) """ def __init__(self, app, mounts=None): self.app = app self.mounts = mounts or {} def __call__(self, environ, start_response): script = environ.get('PATH_INFO', '') path_info = '' while '/' in script: if script in self.mounts: app = self.mounts[script] break script, last_item = script.rsplit('/', 1) path_info = '/%s%s' % (last_item, path_info) else: app = self.mounts.get(script, self.app) original_script_name = environ.get('SCRIPT_NAME', '') environ['SCRIPT_NAME'] = original_script_name + script environ['PATH_INFO'] = path_info return app(environ, start_response)
關於 WSGI 的原理部分基本結束,下一篇我會介紹下對 flask 框架的理解。初始化中間件時須要提供一個 mounts 字典,用來指定不一樣 URL 路徑到 application 的映射關係。這樣對於一個請求,中間件檢查其路徑,而後選擇合適的 application 進行處理。