化整為零的次世代網頁開發標準: WSGI

reference: http://blog.ez2learn.com/2010/01/27/introduction-to-wsgi/

今天,我要介紹Python網頁開發的標準: WSGI,我個人在看見這類英文縮寫時,都一定會試著去記住它的全寫,因為縮寫本身一點意義都沒有,難以記憶,WSGI的全寫是”Web Server Gateway Interface“,它的發音有點像是whiskey,光知道這個名字還是很難理解這到底是用來做什麼用的,簡單的來說,它是Python定義網頁程式和伺服器溝通的介面

如果你有寫過CGI (Common Gateway Interface),它的作用基本上就是和CGI類似的功用,定義一個標準的溝通方式,讓你寫的程式可以和伺服器溝通,但是WSGI不是設計用來給任何語言使用的,它是設計給Python用的,而它其實是基於CGI的延伸,在Python的部份進一步做更多的定義,而因為他是基於CGI,所以它也可以和CGI的介面相容,只要透過一個轉接器,就能把WSGI的程式接到CGI

說了這麼多,相信大部份人對於WSGI是什麼還是一頭霧水,會有一堆疑問,為什麼有了CGI還要有WSGI? Middleware又是什麼? 這很正常,我一開始也對WSGI一點概念都沒有,接下來我們就來介紹WSGI的特色。

一些基本的定義

WSGI是由Python的官方在PEP333所定義出來的,細節的定義請自行閱讀該規格,這篇文章希望能從較高的層面來著眼,所以在此只簡單介紹基本的概念,首先,如果你有寫過CGI的話,就知道CGI透過環境變數來取得外部資訊,基本上WSGI也是一樣透過環境變數來取得資訊,例如REQUEST_METHOD、SERVER_NAME、HTTP_xxx,如你所見它繼承了自CGI的環境變數,除此之外它還定義了一些額外的變數,例如wsgi.version是包含著wsgi介面版本的變數,我們取部份它定義的環境變數


REQUEST_METHOD
The HTTP request method, such as “GET” or “POST”. This cannot ever be an empty string, and so is always required.

SCRIPT_NAME
The initial portion of the request URL’s “path” that corresponds to the application object, so that the application knows its virtual “location”. This may be an empty string, if the application corresponds to the “root” of the server.

PATH_INFO
The remainder of the request URL’s “path”, designating the virtual “location” of the request’s target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash.

QUERY_STRING The portion of the request URL that follows the “?”, if any. May be empty or absent.

到這裡其實都還看不見任何特別的地方,接下來才是重點,它定義了WSGI的應用程式是用一個固定形式的函數來和伺服器進行溝通,我們取自PEP333的範例:


def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello world!n']
我知道單單這樣看來好像很平常,不過就是定義一個標準的函數介面,環境變數一律從environ丟進去,要開始進行response時就呼叫第二個參數start_response,第一個放狀態,第二個放headers,然後函數的回傳值是一個包含了網頁內容的list,可以是generator,也就是內容不一定要回傳時就產生,而是回傳一個generator再被呼叫從裡面取得要回傳給browser的網頁內容,好了,寫到這裡,一切似乎簡單到似乎沒有什麼用處,是的,跟據Python的哲學,簡單比複雜來得好,雖然它的定義簡單,但是背後卻隱涵著更重大的意義,你可能一開始和我一樣覺得二丈金剛摸不著頭腦,搞不清楚這到底有什麼好處,但很快你就會知道這簡單定義所帶來的強大威力,我們繼續看下去。

Middleware (中介層)

在PEP333裡最令人困惑的名字,我想莫過於Middleware,對於這類名詞我傾向於寫原文,但是如果硬要給它個中文翻譯,我暫時把它譯做”中介層”,不是什麼響亮的名詞,但是卻有極大的彈性,別被這個名字給嚇著了,我們先來看看一個例子,我們這樣說好了,假如你是注音文的推廣者,實在很討厭看見網頁裡都沒有注音文,那麼你要怎樣改寫你的程式呢? 最簡單的做法就是改寫原本的程式對吧? 但是在WSGI的架構下,我們有更好的做法,那就是寫一個Middleware來幫我們取代掉一般中文裡的某些字,我們先看看原本的程式:


from wsgiref.simple_server import make_server

def my_app(environ, start_response):
    """a simple wsgi application"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [u"你好! 歡迎來到Victor的第一個WSGI程式".encode('utf8')]

httpd = make_server('', 8000, my_app)
print "Serving on port 8000..."
httpd.serve_forever()
我們用Python內建的模組wsgiref裡的簡單伺服器來執行這個wsgi程式,我們來看看執行的結果

一點都不吸引人對吧? 我們需要的是注音文! 滿滿的注音文才能讓人滿足,於是利用WSGI你可以寫一個Middleware不需改寫原本程式的任何一個部份,就能把你的網站變成一個充滿注音文的網站! 我們來看一下新版的程式:


from wsgiref.simple_server import make_server

def my_app(environ, start_response):
    """a simple wsgi application"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [u"你好! 歡迎來到Victor的第一個WSGI程式".encode('utf8')]

class MarsLanguage(object):

    def __init__(self, app, encoding='utf8'):
        self.app = app
        self.encoding = encoding
        self.dictionary = {
            u'好': u'ㄏ',
            u'到': u'ㄉ',
            u'個': u'ㄍ',
            u'的': u'ㄉ'
        }

    def __call__(self, environ, start_response):
        content = self.app(environ, start_response)
        for piece in content:
            piece = piece.decode(self.encoding)
            for key, value in self.dictionary.iteritems():
                piece = piece.replace(key, value)
            yield piece.encode(self.encoding)

# We warp the original WSGI application with
# the MarsLanguage middleware!!!
app = MarsLanguage(my_app)

httpd = make_server('', 8000, app)
print "Serving on port 8000..."
httpd.serve_forever()
看到了嗎? 我們原本的wsgi一點更動都沒有,我們只是在原本的app外面再包一層MarsLanguage這個Middleware,沒錯,MarsLanguage就是一個最簡單Middleware,它讓我們再不用改原本的程式下就能改變網頁的行為或是結果,我們不用改任何原本的程式,就可以把網頁產生的結果全部變成注音文,很棒不是嗎? 我們來看看結果

看到注音文讓人開心不是嗎? 當然,我知道,光是這火星文Middleware沒辦法滿足各位,火星文Middleware只存在於本篇文章,如果你有興趣可以把它應用在你的WSGI網站上,當然你可能得做好受到使用者投訴的心理準備。

Middleware的概念

其實Middleware並不是什麼新的概念,如果你有研究過Apache的Module怎麼寫,你會發現Apache有個機制叫做Filter,有類似的概念,如這份文件裡這張圖所示

Apache的模組可以在輸入和對資料進行修改,MarsLanguage的Middleware其實在Apache模組中也可以寫一個火星文模組用來達到同樣的效果,只是WSGI的Middleware不只有針對資料,還有其它像可以對環境變數進行控制的功能,它有錯誤的控制,所以當內層拋出例外時,它可以進行處理,有著極大的彈性。

再仔細想一想,如果你知道Design Pattern,你就會發現它其實是Decorator (裝飾者) 的pattern,它的威力和彈性就來自於一致的介面,使得可以做出Middleware來改變WSGI application的行為,上下游都不需要知道彼此的存在,他們只要在每一層做好他們該做的事就可以了。

實際的應用

當使用者送出一個Request來到你伺服器,接著你的網頁程式產生內容,然後送出Response回去給Browser,最簡單的網頁程式做的事情大概就像是這樣,但是真實世界的網頁程式要做的事情很多,他們得記錄Session資訊、取得Cookie、認證、錯誤處理等等雜事,而傳統的架構你可以想像一下這些東西都沒有標準可言,都只限於單一的網頁程式,他們可能都跟特定模組糾結在一起,而WSGI的Middleware將這些該做的事都限定於Middleware中,它們跟上下游溝通的方式都是按照標準來實作的,這表示這些Middleware都能重覆被利用,這就是WSGI帶來的好處之一。

我們借用來自Pylons文件的一張圖

從這張你可以看到,使用者的Request進到我們的網頁伺服器裡來,首先經過Registry Manager是用來管理request-local的相關物件,接著Status Code Redirect在Request上不做任何事,往下層跑ErrorHandler也是不對Request做任何事,接著是Cache Middle,視情況記錄Request相關資料,然後是Session Middleware取出cookie還有在環境中準備session的資料,接著Routes依網址決定要給哪個Controller來處理,最後Request經過層層關卡來到了真正的Pylons App,產生網頁結果後,Response會一層一層被回傳回去,到Cache層如果有需要可以把結果暫存起來,如果這之中有例外被丟出,Error Handler會處理,debug模式下會顯示友善的除錯介面,非debug模式下可以把錯誤報告寄到信箱去,然後回到Status Code Redirect如果有需要可以重導網頁到特定的錯誤頁面去,再來是Registry Manager,整個Request進去和產生Response的過程在Pylons裡大概就像這樣子,講到這裡,我相信大家已經對於WSGI越來越有感覺。

留言

張貼留言

這個網誌中的熱門文章

Json概述以及python對json的相關操作

Docker容器日誌查看與清理

利用 Keepalived 提供 VIP