今天在 git.oschina 的首頁上看到他們推出演示平臺,其中,Python 的演示平臺支持 WSGI 接口的應用。雖然,這個演示平臺連它本身提供的示例都跑不起來,可是,它仍是成功的勾起了我對 WSGI 的好奇心。一番瞭解,對該機制的認識,總結以下。若有不妥,還望斧正。python
爲何是 WSGI?git
寫過網頁應用的各位親,應該對 CGI 有了解,咱們知道,CGI 的全程是「Common Gateway Interface」,即 「通用 Gateway Interface「。沒錯,這裏的 WSGI,就是隻針對 Python的網頁應用接口「Python Web Server Gateway Interface」。經過這樣的類比,想必你們對他的地位就有所瞭解了。web
它只是一個接口定義:它不負責服務器的實現,也不負責網頁應用的實現,它只是一個兩邊接口方式的約定。因此,它並非另外一個 WEB 應用框架。一般意義上的 WEB 應用框架,也只至關於 WSGI 網頁應用端的一種實現。設計模式
這樣作的好處是?PEP 0333 中的解釋是,爲了實現一個相似於 Java Servelet 的 API,使得遵循該接口的應用擁有更普遍的適用性。是的,有了該接口,你就不用去考慮,服務器對 Python 的支持究竟是如何實現——不管是「 直接用 Python 實現的服務器」,仍是「服務器嵌入 Python」,或者是 「 經過網關接口(CGI, Fastcgi...)」——應用程序都有很好的適用性。就像是今天故事的開始,咱們遇到了雲平臺,它提供了對 WSGI 接口的支持,那麼,只要應用是基於 WSGI 的,那麼應用就能夠直接跑起來。緩存
此外,WSGI 的設計,也提供了另一種可能性,那就是中間件(middleware)。或者說,咱們能夠寫一些對 server 和 application 都兼容的模塊,咱們能夠把他們部署在 Server 端,也能夠部署在 Application 端,完成好比緩存、字符編碼轉換、根據 url 作應用 routing 等功能。這種設計模式,是 WSGI 下降了 server 和 application 耦合度以後的產物,同時,它從另外一個角度大大提高了設計的靈活性。服務器
WSGI 實施概略多線程
上一小節,簡要對 WSGI 作了介紹。這裏從 application、server、middleware 三個角度對 WSGI 稍微進行深刻,使咱們對它有一個更具體的印象。架構
1)Application 端app
WSGI 要求,應用端必須提供一個可被調用的實體(PEP 0333 使用的是 Object,文檔還特別解釋這有別於Object instance),該實體能夠是:一個函數(function)、一個方法(method)、一個類(class)、或者是有__call__方法的對象(Object instance)。框架
這裏有兩個網頁應用端的實現示例,一個是 function object,一個 class object:
def simple_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return ['Hello world!\n']
上面的 function 只是直接對請求直接作了 「200 ok」 迴應,並無處理傳進來的參數 environ——裏面是由 WSGI Server 端提供的各類 HTTP 請求參數。須要特別注意的是,這個函數在最後,返回的一個 list(用「[]」包含在內)以保證結果的 iterable。下面的 class 相似。
在下面例子中,AppClass 做爲應用實體。當調用發生時,實際上是對 class 進行了例化( python 固有特性,能夠參考後面 server 端的實現代碼進一步理解),正如咱們看到,此次調用(call)的返回值也是可迭代的——雖然只迭代一次(yield)。
class AppClass: def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): status = '200 OK' response_headers = [('Content-type', 'text/plain')] self.start(status, response_headers) yield "Hello world!\n" """ In fact, the interator ‘ends‘ here because of no more yield field"""
與上面兩種情形不一樣,使用 object instance 做爲應用實體時,須要爲類定義添加 __call__ 方法,同時,參考上面使用 function 做爲實體時情形,__call__ 方法的返回值需爲 iterable(好比 return [ something ])。
最後,無論咱們的 app 是 function 仍是 class, application 都須要處理兩個參數,並且是兩個位置相關的參數(不是命名參數),分別是:一個存放了 CGI 環境變量的 dictionary object,和一個可調用實體(須要給它三個位置相關的參數,兩個必須,一個可選)。
其中,可調用實體(前例中的 start_response)必須調用一次,兩個必須的參數分別爲「 HTTP Response的狀態(str 類型)「 和 「HTTP Response Header(list of tuples)「;一個可選的參數 exc_info,必須是 Python sys.exc_info() tuple,只有在出錯須要顯示錯誤信息時使用。完整調用:start_response(status, response_headers,exc_info).
2)Server 端
下面是從 PEP 0333 拿來的一個簡單的 WSGI 容器,適用於 Python 做爲某 WEB Server 上 CGI 時的應用情形。
import os, sys def run_with_cgi(application): environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin 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): if not headers_set: raise AssertionError("write() before start_response()") elif not headers_sent: # Before the first output, send the stored headers status, response_headers = headers_sent[:] = headers_set sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: # Re-raise original exception if headers sent raise exc_info[0], exc_info[1], exc_info[2] finally: exc_info = None # avoid dangling circular ref elif headers_set: raise AssertionError("Headers already set!") headers_set[:] = [status, response_headers] return write result = application(environ, start_response)
try: for data in result: if data: # don't send headers until body appears write(data) if not headers_sent: write('') # send headers now if body was empty finally: if hasattr(result, 'close'): result.close()
上面的容器,大概實現了:a)將 CGI 環境變量放入 dictionary object (environ)中,供 Application 實體使用;b)定義了 start_response 方法,供 Application 實體調用;c)調用 application 實體,對 web 請求進行處理;d)將 application 的返回結果,以及經過 start_response 設置的 HTTP Response HEADER,寫到 stdout ——像其餘 CGI 同樣,其實是被髮往網頁。
3) 做爲 middleware
由於 WSGI 的寬鬆耦合的特性,咱們能夠輕鬆的在 Application 和 Server 以前插入任何的中間插件,在不須要改動 Server 和 Application 的前提下,實現一些特殊功能。可是,這種放在 Server 和 Application 「中間」的模塊,並非這裏要講的 middleware ;或者,這隻能算是一種特殊的 middleware,由於它僅僅是實現了 PEP 0333 中 middleware 定義的 Application 側的功能。這種僅實施在一側的 middleware,須要在發佈時,特別的聲明。
PEP 0333 中約定,中間件是一些便可以在 Server 端實施,又能夠在 Application 端實施的模塊。因此,在設計的時候,對兩邊的特性都要作適當考慮。幸虧,WSGI 接口設計的足夠簡單。
class Router(): def __init__(self): self.path_info = {} def route(self, environ, start_response): application = self.path_info[environ['PATH_INFO']] return application(environ, start_response) def __call__(self, path): def wrapper(application): self.path_info[path] = application return wrapper
""" The above is the middleware"""
router = Router() @router('/world') def world(environ, start_response): status = '200 OK' output = 'World!'start_response(status, response_headers) return [output] @router('/hello') def hello(environ, start_response): status = '200 OK' output = 'Hello' response_headers = [('Content-type', 'text/plain'), ('Content-Length', str(len(output)))] start_response(status, response_headers) return [output]
簡單解釋一下:
- 做爲 Application 時,咱們用 Router 實例化一個對象。而後對 「 PATH-APP 「 進行註冊,根據不一樣的 PATH,咱們要進一步選擇哪一個 App。接着,就是把 router.route() 餵給 Server ,做爲 Application 側的可調用實體。有請求到來時,根據已經註冊的 「PATH-APP」 對選擇應用並執行。
- Server 端相似,咱們要先實例化並完成註冊。而後,好比,拿咱們上一小節實現的 WSGI 容器爲例,咱們須要修改 result = router.route(environ, start_response),一樣完成了router的功能。
下面是另一個,實現了 postprocessor 的一個例子,在 Application 返回的 HTTP Header 裏面再加一個 Header。
def myapp(environ, start_response): response_headers = [('content-type', 'text/plain')] start_response('200 OK', response_headers) return ['Check the headers!'] class Middleware: def __init__(self, app): self.wrapped_app = app def __call__(self, environ, start_response): def custom_start_response(status, headers, exc_info=None): headers.append(('X-A-SIMPLE-TOKEN', "1234567890")) return start_response(status, headers, exc_info) return self.wrapped_app(environ, custom_start_response) app = Middleware(myapp)
這裏經過改寫傳遞給 Application 的實體,實現了 postprocess 的目的。
順手整理的其餘資源:
- WSGI 的一些詳細資料,包括應用列表什麼的:http://wsgi.readthedocs.org/en/latest/
- 支持 WSGI 的多線程 WEB 服務器,基於SimpleHttpServer:http://www.owlfish.com/software/wsgiutils/
- Paste 爲構建以 WSGI 爲基礎的 WEB 應用程序或框架提供一個良好的基礎
- 官方的 WSGI 實現參考:https://pypi.python.org/pypi/wsgiref
- 啄木鳥社區的 WSGI 中文 wiki:http://wiki.woodpecker.org.cn/moin/WSGI
- 和 Paste 同樣有名的基本架構:https://pypi.python.org/pypi/Pylons/1.0
- 目前 Python 比較流行的三大 WEB 框架:TurboGears,Django,web2py。+1,代碼在 K 級別的服務小框架:webpy。
- 另外三個聽說高性能的 App 開發框架:Falcon、Tornado、Bootle.py.
- 還有個價格不錯的 vps,恩:https://www.hostwinds.com/