WSGI必知必會

1. 簡介 
html

WSGI是Web Server Gateway Interface的簡稱。它不是服務器,python模塊,框架,API和任何種類的軟件。它僅僅是一個服務器和應用間的接口規範。python

這個規範的具體內容能夠參考(https://www.python.org/dev/peps/pep-0333/  注:新規範爲 https://www.python.org/dev/peps/pep-3333  )。規範中提到,儘管在Python中存在着各類各樣的Web框架,可是,你選擇框架的同時,也限制了你的選擇。規範裏提到了Java的Servlet,儘管Java也有那麼多的Web框架,可是你卻可使用基礎的Servlet構建任何複雜的Web應用。這就是通用性,你不會受到任何框架的限制。web

WSGI就是這樣一個規範,它的目的就是規範Web服務器與Web應用(框架)之間的交互。shell


2. 概念設計模式

WSGI提出了三個概念,應用,服務器,中間件。緩存

    * 應用:指能夠被調用的一個對象,通常指的是包含一個__call__方法的對象。服務器

    * 服務器:指的是實現了調用應用的部分。
app

    * 中間件:處於服務器和應用兩側,起粘合做用,具體包括:請求處理,environ處理等。框架

這裏仍是舉個例子吧,要不都是概念了。工具

from wsgiref.simple_server import make_server
from webob import Request, Response
#application
class AppTestByManual(object):        
    def __call__(self, environ, start_response):  
        req = Request(environ)
        return self.test(environ, start_response)
        
    def test(self, environ, start_response):
        res = Response()
        res.status = 200     #middleware
        res.body   = "spch"  #middleware
        return res(environ, start_response)
        
application = AppTestByManual()

#server
httpd = make_server('0.0.0.0', 8000, application)  
httpd.serve_forever()

在這個例子中,httpd服務器調用AppTestByManual應用,中間件部分對Response作了處理。

在這裏須要注意的是應用部分實現了一個__call__方法,這是WSGI規範中規定的。

其實,對於中間件的定義是比較寬泛的,中間件主要是處理通過的Request和Response。


3. environ 變量

須要注意的是environ 變量,environ 字典中包含了在 CGI 規範中定義了的 CGI 環境變量。

REQUEST_METHODHTTP 請求的類型,好比「GET」或者「POST」。這個不多是空字符串,因此是必須給出的。

SCRIPT_NAMEURL 請求中路徑的開始部分,對應應用程序對象(?),這樣應用程序就知道它的虛擬位置。若是該應用程序對應服務器的根目錄的話,它多是空字符串。

PATH_INFOURL 請求中路徑的剩餘部分,指定請求的目標在應用程序內部的虛擬位置(?)。若是請求的目標是應用程序根目錄而且沒有末尾的斜槓的話,可能爲空字符串。

QUERY_STRINGURL 請求中跟在「?」後面的那部分,可能爲空或不存在。

CONTENT_TYPEHTTP 請求中任何 Content-Type 域的內容,可能爲空或不存在。

CONTENT_LENGTHHTTP 請求中任何 Content-Length 域的內容,可能爲空或不存在。

SERVER_NAME,SERVER_PORT這些變量能夠和 SCRIPT_NAME、PATH_INFO 一塊兒組成完整的URL。然而要注意的是,重建請求 URL 的時候應該優先使用 HTTP_HOST 而非 SERVER_NAME 。。SERVER_NAME 和 SERVER_PORT 永遠不能爲空字符串,也老是必須存在的。

SERVER_PROTOCOL客戶端發送請求所使用協議的版本。一般是相似「HTTP/1.0」或「HTTP/1.1」的東西,能夠被用來判斷如何處理請求包頭。(既然這個變量表示的是請求中使用的協議,並且和服務器響應時使用的協議無關,也許它應該被叫作REQUEST_PROTOCOL。不過爲了保持和 CGI 的兼容性,咱們仍是使用這個名字。)

wsgi.version:(1, 0) 元組,表明 WSGI 1.0 版

wsgi.url_scheme:字符串,表示應用請求的 URL 所屬的協議,一般爲「http」或「https」。

wsgi.input: 類文件對象的輸入流,用於讀取 HTTP 請求包體的內容。(服務端在應用端請求時開始讀取,或者預讀客戶端請求包體內容緩存在內存或磁盤中,或者視狀況而定採用任何其餘技術提供此輸入流。)

wsgi.errors: 類文件對象的輸出流,用於寫入錯誤信息,以集中規範地記錄程序產生的或其餘相關錯誤信息。這是一個文本流,即應用應該使用「n」來表示行尾,並假定其會被服務端正確地轉換。(在 str 類型是 Unicode 編碼的平臺上,錯誤流應該正常接收並記錄任意 Unicode 編碼而不報錯,而且容許自行替代在該平臺編碼中沒法渲染的字符。)不少 Web 服務器中 wsgi.errors 是主要的錯誤日誌,也有一些使用 sys.stderr 或其餘形式的文件來記錄。Web 服務器的自述文檔中應該包含如何配置錯誤日誌以及如何找到記錄的位置。服務端能夠在被要求的狀況下,向不一樣的應用提供不一樣的錯誤日誌。

wsgi.multithread:若是應用對象可能會被同一進程的另外一個線程同步調用,此變量值爲真,不然爲假。

wsgi.multiprocess:若是同一個應用對象可能會被另外一個進程同步調用,此變量值爲真,不然爲假。

wsgi.run_once:若是服務端指望(可是不保證能獲得知足)應用對象在生命週期中之輩調用一次,此變量值爲真,不然爲假。通常只有在基於相似 CGI 的網關服務器中此變量纔會爲真。

from webob import Request
from pprint import pprint
req = Request.blank('/article?id=1')
pprint(req.environ)

輸出結果爲:

{'HTTP_HOST': 'localhost:80',
 'PATH_INFO': '/article',
 'QUERY_STRING': 'id=1',
 'REQUEST_METHOD': 'GET',
 'SCRIPT_NAME': '',
 'SERVER_NAME': 'localhost',
 'SERVER_PORT': '80',
 'SERVER_PROTOCOL': 'HTTP/1.0',
 'wsgi.errors': <open file '<stderr>', mode 'w' at 0x0157D0D0>,
 'wsgi.input': <_io.BytesIO object at 0x01EE8360>,
 'wsgi.multiprocess': False,
 'wsgi.multithread': False,
 'wsgi.run_once': False,
 'wsgi.url_scheme': 'http',
 'wsgi.version': (1, 0)}


4. 相關模塊

wsgirefwsgiref是一個實現wsgi規範的模塊,它提供了操縱WSGI環境變量和response頭的工具,而且還實現了一個WSGI服務器。具體能夠參考(https://docs.python.org/2/library/wsgiref.html )。

webob(WebOb):webob提供了包裝後的WSGI請求(Request)環境,並輔助建立WSGI響應(Response)。具體能夠參考(http://webob.org/ )。

安裝:

pip install webob

routes:是一個管理URL(路由)的模塊。具體參考(https://routes.readthedocs.org/en/latest/ )。

安裝:

pip install routes

在本文中將主要使用這三個模塊來講明相關的例子。


5. Request和Response

在「3. environ 變量」中Request,它是對environ的包裝,固然,咱們也能夠打印出Response。

from webob import Response
res = Response(content_type='text/plain', charset='utf8')
f = res.body_file
f.write('hey')
print res.status, res.headerlist, res.body

輸出結果:

200 OK [('Content-Type', 'text/plain; charset=utf8'), ('Content-Length', '3')] hey


通常狀況下,Request和Response二者是相關的,咱們會根據請求來肯定響應。

def MyApp(environ, start_response):
    req = Request(environ)
    res = Response()
    if req.path_info == '/':
        res.body_file.write('url true!')
    else:
        res.body_file.write('url error!')
    return res(environ, start_response)
    
req = Request.blank('/')
res = req.get_response(MyApp)
print res

這裏假設了一個請求(請求URL爲"/"),當請求爲"/"時,響應的body設置爲「url true!」,不然設置爲「url error!」。

固然這裏僅僅是假設了這樣一個過程,當請求進入MyApp,在MyApp中作對應處理再生成相應的響應。


6. 加上服務器,簡化App的書寫

在「2. 概念」中,已經有一個例子,如今再讓咱們看看這個例子:

from wsgiref.simple_server import make_server
from webob import Request, Response

#application
class AppTestByManual(object):        
    def __call__(self, environ, start_response):  
        req = Request(environ)
        return self.test(environ, start_response)
        
    def test(self, environ, start_response):
        res = Response()
        res.status = 200     #middleware
        res.body   = "spch"  #middleware
        return res(environ, start_response)
        
application = AppTestByManual()

#server
httpd = make_server('0.0.0.0', 8000, application)  
httpd.serve_forever()

例子中AppTestByManual這個App實現了一個__call__方法供服務器調用,在App中將請求部分放入__call__處理,處理完成後調用test生成相應的響應。固然,這種寫法是能夠改進的。

from wsgiref.simple_server import make_server
from webob import Request, Response
from webob.dec import *

class AppTestByAuto(object):
    @wsgify
    def __call__(self, req):
        return self.test(req) 
        
    @wsgify
    def test(self, req):
        res = Response()
        res.status = 200
        res.body   = "spch"
        return res 
        
application = AppTestByAuto()

httpd = make_server('0.0.0.0', 8000, application)  
httpd.serve_forever()

這個例子是對上一個例子的改進,這裏使用了@wsgify裝飾器,這樣咱們就不須要寫那麼多參數了,這個語法糖爲咱們作了封裝。


7. 路由處理

本節將會在上節例子的基礎上,加入路由(URL)處理。這個例子裏面,使用routes模塊來管理URL。

from wsgiref.simple_server import make_server
from webob import Request, Response
from webob.dec import *
from routes import Mapper, middleware

class Test(object):
    def index(self):
        return "do index()"
    def add(self):
        return "do show()"
        
class MyApp(object):
    def __init__(self):
        print '__init__'
        self.controller = Test()
        m = Mapper()
        m.connect('blog', '/blog/{action}/{id}', controller=Test,
                  conditions={'method': ['GET']})
        m.redirect('/', '/blog/index/1')
        self.router = middleware.RoutesMiddleware(self.dispatch, m)
        
    @wsgify
    def dispatch(self, req):
        match = req.environ['wsgiorg.routing_args'][1]
        print req.environ['wsgiorg.routing_args']
        if not match:
            return 'error url: %s' % req.environ['PATH_INFO']
        action = match['action']
        if hasattr(self.controller, action):
            func = getattr(self.controller, action)
            ret = func()
            return ret
        else:
            return "has no action:%s" % action
            
    @wsgify
    def __call__(self, req):
        print '__call__'
        return self.router
        
if __name__ == '__main__':
    httpd = make_server('0.0.0.0', 8000, MyApp())
    print "Listening on port 8000...."
    httpd.serve_forever()

在這個例子中,routes模塊是WSGI中典型的中間件模塊。在MyApp的構造方法中,將路由和dispatch方法放入中間件self.router,在實現__call__方法時返回中間件self.router。

「dispatch」中的「getattr(self.controller, action)」使用了python的內省機制(相似設計模式中的反射),經過匹配map中的url規則自動匹配對應的類和方法。


按照這個例子,咱們還能夠修改爲這樣:

from wsgiref.simple_server import make_server
from webob import Request, Response
from webob.dec import *
from routes import Mapper, middleware

class images(object):
    def index(self):
        return "images do index()"
    def create(self):
        return "images do create()"
    def detail(self):
        return "images do detail()"
        
class servers(object):
    def index(self):
        return "servers do index()"
    def create(self):
        return "servers do create()"
    def detail(self):
        return "servers do detail()"
        
class APIRouter(object):
    def __init__(self):
        print '__init__'
        self.controller = None
        m = Mapper()
        m.connect('blog', '/{class_name}/{action}/{id}',
                  conditions={'method': ['GET']})
        m.redirect('/', '/servers/index/1')
        self.router = middleware.RoutesMiddleware(self.dispatch, m)
        print self.router
        
    @wsgify
    def dispatch(self, req):
        match = req.environ['wsgiorg.routing_args'][1]
        print req.environ['wsgiorg.routing_args']
        if not match:
            return 'error url: %s' % req.environ['PATH_INFO']
        
        class_name = match['class_name']
        self.controller = globals()[class_name]()
            
        action = match['action']
        if hasattr(self.controller, action):
            func = getattr(self.controller, action)
            ret = func()
            return ret
        else:
            return "has no action:%s" % action
    @wsgify
    def __call__(self, req):
        print '__call__'
        return self.router
        
if __name__ == '__main__':
    httpd = make_server('0.0.0.0', 8000, APIRouter())
    print "Listening on port 8000...."
    httpd.serve_forever()

是否是像OpenStack裏面的某一部分,^_^

相關文章
相關標籤/搜索