花了兩個星期,我終於把 WSGI 整明白了

在 三百六十行,行行轉 IT 的現狀下,不少來自各行各業的同窗,都選擇 Python 這門膠水語言作爲踏入互聯網大門的第一塊敲門磚,在這些人裏,又有至關大比例的同窗選擇了 Web 開發這個方向(包括我)。而從事 web 開發,繞不過一個知識點,就是 WSGI。html

無論你是不是這些如上同窗中的一員,都應該好好地學習一下這個知識點。node

因爲我本人不從事專業的 python web 開發,因此在寫這篇文章的時候,借鑑了許多優秀的網絡博客,並花了不少的精力閱讀了大量的 OpenStack 代碼。python

爲了寫這篇文章,零零散散地花了大概兩個星期。原本能夠拆成多篇文章,寫成一個系列的,通過一番思慮,仍是準備一篇講完,這就是本篇文章這麼長的緣由。nginx

另外,一篇文章是不能吃透一個知識點的,本篇涉及的背景知識也比較多的,若我有講得不到位的,還請你多多查閱其餘人的網絡博客進一步學習。web

在你往下看以前,我先問你幾個問題,你帶着這些問題往下看,可能更有目的性,學習可能更有效果。shell

問1:一個 HTTP 請求到達對應的 application處理函數要通過怎樣的過程?flask

問2:如何不經過流行的 web 框架來寫一個簡單的web服務?api

一個HTTP請求的過程能夠分爲兩個階段,第一階段是從客戶端到WSGI Server,第二階段是從WSGI Server 到WSGI Application瀏覽器

今天主要是講第二階段,主要內容有如下幾點:bash

  1. WSGI 是什麼,因何而生?
  2. HTTP請求是如何到應用程序的?
  3. 實現一個簡單的 WSGI Server
  4. 實現「高併發」的WSGI Server
  5. 第一次路由:PasteDeploy
  6. PasteDeploy 使用說明
  7. webob.dec.wsgify 裝飾器
  8. 第二次路由:中間件 routes 路由

01. WSGI 是什麼,因何而生?

WSGI是 Web Server Gateway Interface 的縮寫。

它是 Python應用程序(application)或框架(如 Django)和 Web服務器之間的一種接口,已經被普遍接受。

它是一種協議,一種規範,其是在 PEP 333提出的,並在 PEP 3333 進行補充(主要是爲了支持 Python3.x)。這個協議旨在解決衆多 web 框架和web server軟件的兼容問題。有了WSGI,你不用再由於你使用的web 框架而去選擇特定的 web server軟件。

常見的web應用框架有:Django,Flask等

經常使用的web服務器軟件有:uWSGI,Gunicorn等

那這個 WSGI 協議內容是什麼呢?知乎上有人將 PEP 3333 翻譯成中文,寫得很是好,我將這段協議的內容搬運過來。

WSGI 接口有服務端和應用端兩部分,服務端也能夠叫網關端,應用端也叫框架端。服務端調用一個由應用端提供的可調用對象。如何提供這個對象,由服務端決定。例如某些服務器或者網關須要應用的部署者寫一段腳本,以建立服務器或者網關的實例,而且爲這個實例提供一個應用實例。另外一些服務器或者網關則可能使用配置文件或其餘方法以指定應用實例應該從哪裏導入或獲取。

WSGI 對於 application 對象有以下三點要求

  1. 必須是一個可調用的對象
  2. 接收兩個必選參數environ、start_response。
  3. 返回值必須是可迭代對象,用來表示http body。

02. HTTP請求是如何到應用程序的?

當客戶端發出一個 HTTP 請求後,是如何轉到咱們的應用程序處理並返回的呢?

關於這個過程,細節的點這裏無法細講,只能講個大概。

我根據其架構組成的不一樣將這個過程的實現分爲兩種:

一、兩級結構 在這種結構裏,uWSGI做爲服務器,它用到了HTTP協議以及wsgi協議,flask應用做爲application,實現了wsgi協議。當有客戶端發來請求,uWSGI接受請求,調用flask app獲得相應,以後相應給客戶端。 這裏說一點,一般來講,Flask等web框架會本身附帶一個wsgi服務器(這就是flask應用能夠直接啓動的緣由),可是這只是在開發階段用到的,在生產環境是不夠用的,因此用到了uwsgi這個性能高的wsgi服務器。

二、三級結構 這種結構裏,uWSGI做爲中間件,它用到了uwsgi協議(與nginx通訊),wsgi協議(調用Flask app)。當有客戶端發來請求,nginx先作處理(靜態資源是nginx的強項),沒法處理的請求(uWSGI),最後的相應也是nginx回覆給客戶端的。 多了一層反向代理有什麼好處?

提升web server性能(uWSGI處理靜態資源不如nginx;nginx會在收到一個完整的http請求後再轉發給wWSGI)

nginx能夠作負載均衡(前提是有多個服務器),保護了實際的web服務器(客戶端是和nginx交互而不是uWSGI)

03. 實現一個簡單的 WSGI Server

在上面的架構圖裏,不知道你發現沒有,有個庫叫作 wsgiref ,它是 Python 自帶的一個 wsgi 服務器模塊。

從其名字上就看出,它是用純Python編寫的WSGI服務器的參考實現。所謂「參考實現」是指該實現徹底符合WSGI標準,可是不考慮任何運行效率,僅供開發和測試使用。

有了 wsgiref 這個模塊,你就能夠很快速的啓動一個wsgi server。

from wsgiref.simple_server import make_server

# 這裏的 appclass 暫且不說,後面會講到
app = appclass()
server = make_server('', 64570, app)
server.serve_forever()
複製代碼

當你運行這段代碼後,就會開啓一個 wsgi server,監聽 0.0.0.0:64570 ,並接收請求。

使用 lsof 命令能夠查到確實開啓了這個端口

以上使用 wsgiref 寫了一個demo,讓你對wsgi有個初步的瞭解。其因爲只適合在學習測試使用,在生產環境中應該另尋他道。

04. 實現「高併發」的 WSGI Server

上面咱們說不能在生產中使用 wsgiref ,那在生產中應該使用什麼呢?選擇有挺多的,好比優秀的 uWSGI,Gunicore等。可是今天我並不許備講這些,一是由於我不怎麼熟悉,二是由於我本人從事 OpenStack 的二次開發,對它比較熟悉。

因此下面,是我花了幾天時間閱讀 OpenStack 中的 Nova 組件代碼的實現,恰好能夠拿過來學習記錄一下,如有理解誤差,還望你批評指出。

在 nova 組件裏有很多服務,好比 nova-api,nova-compute,nova-conductor,nova-scheduler 等等。

其中,只有 nova-api 有對外開啓 http 接口。

要了解這個http 接口是如何實現的,從服務啓動入口開始看代碼,確定能找到一些線索。

從 Service 文件能夠得知 nova-api 的入口是 nova.cmd.api:main()

打開nova.cmd.api:main() ,一塊兒看看是 OpenStack Nova 的代碼。

在以下的黃框裏,能夠看到在這裏使用了service.WSGIService 啓動了一個 server,就是咱們所說的的 wsgi server

那這裏的 WSGI Server 是依靠什麼實現的呢?讓咱們繼續深刻源代碼。

wsgi.py 能夠看到這裏使用了 eventlet 這個網絡併發框架,它先開啓了一個綠色線程池,從配置裏能夠看到這個服務器能夠接收的請求併發量是 1000 。

但是咱們尚未看到 WSGI Server 的身影,上面使用eventlet 開啓了線程池,那線程池裏的每一個線程應該都是一個服務器吧?它是如何接收請求的?

再繼續往下,能夠發現,每一個線程都是使用 eventlet.wsgi.server 開啓的 WSGI Server,仍是使用的 eventlet。

因爲源代碼比較多,我提取了主要的代碼,精簡以下

# 建立綠色線程池
self._pool = eventlet.GreenPool(self.pool_size)

# 建立 socket:監聽的ip,端口
bind_addr = (host, port)
self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
dup_socket = self._socket.dup()

# 整理孵化協程所需的各項參數
wsgi_kwargs = {
    'func': eventlet.wsgi.server,
    'sock': dup_socket,
    'site': self.app, # 這個就是 wsgi 的 application 函數
    'protocol': self._protocol,
    'custom_pool': self._pool,
    'log': self._logger,
    'log_format': CONF.wsgi.wsgi_log_format,
    'debug': False,
    'keepalive': CONF.wsgi.keep_alive,
    'socket_timeout': self.client_socket_timeout
}

# 孵化協程
self._server = utils.spawn(**wsgi_kwargs)
複製代碼

就這樣,nova 開啓了一個能夠接受1000個綠色協程併發的 WSGI Server。

05. 第一次路由:PasteDeploy

上面咱們提到 WSGI Server 的建立要傳入一個 Application,用來處理接收到的請求,對於一個有多個 app 的項目。

好比,你有一個我的網站提供了以下幾個模塊

/blog  # 博客 app
/wiki  # wiki app
複製代碼

如何根據 請求的url 地址,將請求轉發到對應的application上呢?

答案是,使用 PasteDeploy 這個庫(在 OpenStack 中各組件被普遍使用)。

PasteDeploy 究竟是作什麼的呢?

根據 官方文檔 的說明,翻譯以下

PasteDeploy 是用來尋找和配置WSGI應用和服務的系統。PasteDeploy給開發者提供了一個簡單的函數loadapp。經過這個函數,能夠從一個配置文件或者Python egg中加載一個WSGI應用。

使用PasteDeploy的其中一個重要意義在於,系統管理員能夠安裝和管理WSGI應用,而無需掌握與Python和WSGI相關知識。

因爲 PasteDeploy 原來是屬於 Paste 的,如今獨立出來了,可是安裝的時候仍是會安裝到paste目錄(site-packages\paste\deploy)下。

我會先講下在 Nova 中,是如何藉助 PasteDeploy 實現對url的路由轉發。

還記得在上面建立WSGI Server的時候,傳入了一個 self.app 參數,這個app並非一個固定的app,而是使用 PasteDeploy 中提供的 loadapp 函數從 paste.ini 配置文件中加載application。

具體能夠,看下nova的實現。

經過打印的 DEBUG 內容得知 config_url 和 app name 的值

app: osapi_compute
config_url: /etc/nova/api-paste.inia
複製代碼

經過查看 /etc/nova/api-paste.ini ,在 composite 段裏找到了 osapi_compute 這個app(這裏的app和wsgi app 是兩個概念,須要注意區分) ,能夠看出 nova 目前有兩個版本的api,一個是 v2,一個是v2.1,目前咱們在用的是 v2.1,從配置文件中,能夠獲得其指定的 application 的路徑是nova.api.openstack.compute 這個模塊下的 APIRouterV21 類 的factory方法,這是一個工廠函數,返回 APIRouterV21 實例。

[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v2: openstack_compute_api_v21_legacy_v2_compatible
/v2.1: openstack_compute_api_v21

[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory
複製代碼

這是 OpenStack 使用 PasteDeploy 實現的第一層的路由,若是你不感興趣,能夠直接略過本節,進入下一節,下一節是 介紹 PasteDeploy 的使用,教你實現一個簡易的web server demo。推薦必定要看。

06. PasteDeploy 使用說明

到上一步,我已經獲得了 application 的有用的線索。考慮到不少人是第一次接觸 PasteDeploy,因此這裏結合網上博客作了下總結。對你入門會有幫助。

掌握 PasteDeploy ,你只要按照如下三個步驟逐個完成便可。

一、配置 PasteDeploy使用的ini文件;

二、定義WSGI應用;

三、經過loadapp函數加載WSGI應用;

第一步:寫 paste.ini 文件

在寫以前,咱得知道 ini 文件的格式吧。

首先,像下面這樣一個段叫作 section

[type:name]
key = value
...
複製代碼

其上的type,主要有以下幾種

  1. composite (組合):多個app的路由分發;

    [composite:main]
    use = egg:Paste#urlmap
    / = home
    /blog = blog
    /wiki = wiki
    複製代碼
  2. app(應用):指明 WSGI 應用的路徑;

    [app:home]
    paste.app_factory = example:Home.factory
    複製代碼
  3. pipeline(管道):給一個 app 綁定多個過濾器。將多個filter和最後一個WSGI應用串聯起來。

    [pipeline:main]
    pipeline = filter1 filter2 filter3 myapp
    
    [filter:filter1]
    ...
    
    [filter:filter2]
    ...
    
    [app:myapp]
    ...
    複製代碼
  4. filter(過濾器):以 app 作爲惟一參數的函數,並返回一個「過濾」後的app。經過鍵值next能夠指定須要將請求傳遞給誰。next指定的能夠是一個普通的WSGI應用,也能夠是另外一個過濾器。雖然名稱上是過濾器,可是功能上不侷限於過濾功能,能夠是其它功能,例如日誌功能,即將認爲重要的請求數據記錄下來。

    [app-filter:filter_name]
    use = egg:...
    next = next_app
    
    [app:next_app]
    ...
    
    複製代碼

對 ini 文件有了必定的瞭解後,就能夠看懂下面這個 ini 配置文件了

[composite:main]
use = egg:Paste#urlmap
/blog = blog
/wiki = wiki

[app:blog]
paste.app_factory = example:Blog.factory

[app:wiki]
paste.app_factory = example:Wiki.factory

複製代碼

第二步是定義一個符合 WSGI 規範的 applicaiton 對象。

符合 WSGI 規範的 application 對象,能夠有多種形式,函數,方法,類,實例對象。這裏僅以實例對象爲例(須要實現 __call__ 方法),作一個演示。

import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
    def __init__(self):
        print("Init Blog.")

    def __call__(self, environ, start_response):
        status_code = "200 OK"
        response_headers = [("Content-Type", "text/plain")]
        response_body = "This is Blog's response body.".encode('utf-8')

        start_response(status_code, response_headers)
        return [response_body]

 @classmethod
    def factory(cls, global_conf, **kwargs):
        print("Blog factory.")
        return Blog()

複製代碼

最後,第三步是使用 loadapp 函數加載 WSGI 應用。

loadapp 是 PasteDeploy 提供的一個函數,使用它能夠很方便地從第一步的ini配置文件里加載 app

loadapp 函數能夠接收兩個實參:

  • URI:"config:<配置文件的全路徑>"
  • name:WSGI應用的名稱
conf_path = os.path.abspath('paste.ini')

# 加載 app
applications = deploy.loadapp("config:{}".format(conf_path) , "main")

# 啓動 server, 監聽 localhost:22800 
server = make_server("localhost", "22800", applications)
server.serve_forever()

複製代碼

applications 是URLMap 對象。

完善並整合第二步和第三步的內容,寫成一個 Python 文件(wsgi_server.py)。內容以下

import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
    def __init__(self):
        print("Init Blog.")

    def __call__(self, environ, start_response):
        status_code = "200 OK"
        response_headers = [("Content-Type", "text/plain")]
        response_body = "This is Blog's response body.".encode('utf-8')

        start_response(status_code, response_headers)
        return [response_body]

 @classmethod
    def factory(cls, global_conf, **kwargs):
        print("Blog factory.")
        return Blog()


class Wiki(object):
    def __init__(self):
        print("Init Wiki.")

    def __call__(self, environ, start_response):
        status_code = "200 OK"
        response_headers = [("Content-Type", "text/plain")]
        response_body = "This is Wiki's response body.".encode('utf-8')

        start_response(status_code, response_headers)
        return [response_body]

 @classmethod
    def factory(cls, global_conf, **kwargs):
        print("Wiki factory.")
        return Wiki()
      

if __name__ == "__main__":
    app = "main"
    port = 22800
    conf_path = os.path.abspath('paste.ini')

    # 加載 app
    applications = deploy.loadapp("config:{}".format(conf_path) , app)
    server = make_server("localhost", port, applications)

    print('Started web server at port {}'.format(port))
    server.serve_forever()

複製代碼

一切都準備好後,在終端執行 python wsgi_server.py來啓動 web server

若是像上圖同樣一切正常,那麼打開瀏覽器

  • 訪問http://127.0.0.1:8000/blog,應該顯示:This is Blog's response body.
  • 訪問http://127.0.0.1:8000/wiki,應該顯示:This is Wiki's response body.。

注意:urlmap對url的大小寫是敏感的,例如若是訪問http://127.0.0.1:8000/BLOG,在url映射中未能找到大寫的BLOG。

到此,你學會了使用 PasteDeploy 的簡單使用。

07. webob.dec.wsgify 裝飾器

通過了 PasteDeploy 的路由調度,咱們找到了 nova.api.openstack.compute:APIRouterV21.factory 這個 application 的入口,看代碼知道它其實返回了 APIRouterV21 類的一個實例。

WSGI規定 application 必須是一個 callable 的對象,函數、方法、類、實例,如果一個類實例,就要求這個實例所屬的類實現 __call__ 的方法。

APIRouterV21 自己沒有實現 __call__ ,但它的父類 Router實現了 __call__

咱們知道,application 必須遵叢 WSGI 的規範

  1. 必須接收environ, start_response兩個參數;
  2. 必須返回 「可迭代的對象」。

但從 Router 的 __call__ 代碼來看,它並無聽從這個規範,它不接收這兩個參數,也不返回 response,而只是返回另外一個 callable 的對象,就這樣咱們的視線被一次又一次的轉移,但沒有關係,這些__call__都是外衣,只要扒掉這些外衣,咱們就能看到核心app。

而負責扒掉這層外衣的,就是其頭上的裝飾器 @webob.dec.wsgify ,wsgify 是一個類,其 __call__ 源碼實現以下:

能夠看出,wsgify 在這裏,會將 req 這個原始請求(dict對象)封裝成 Request 對象(就是規範1裏提到的 environ)。而後會一層一層地往裏地執行被wsgify裝飾的函數(self._route), 獲得最內部的核心application。

上面提到了規範1裏的第一個參數,補充下第二個參數start_response,它是在哪定義並傳入的呢?

其實這個無需咱們操心,它是由 wsgi server 提供的,若是咱們使用的是 wsgiref 庫作爲 server 的話。那這時的 start_response 就由 wsgiref 提供。

再回到 wsgify,它的做用主要是對 WSGI app 進行封裝,簡化wsgi app的定義與編寫,它能夠很方便的將一個 callable 的函數或對象,封裝成一個 WSGI app。

上面,其實留下了一個問題,self._route(routes 中間件 RoutesMiddleware對象)是如何找到真正的 application呢?

帶着這個問題,咱們瞭解下 routes 是如何爲咱們實現第二次路由。

08. 第二次路由:中間件 routes 路由

在文章最開始處,咱們給你們畫了一張圖。

這張圖把一個 HTTP 請求粗略簡單地劃分爲兩個過程。但事實上,整個過程遠比這個過程要複雜得多。

實際上在 WSGI Server 到 WSGI Application 這個過程當中,咱們加不少的功能(好比鑑權、URL路由),而這些功能的實現方式,咱們稱之爲中間件。

中間件,對服務器而言,它是一個應用程序,是一個可調用對象, 有兩個參數,返回一個可調用對象。而對應用程序而言,它是一個服務器,爲應用程序提供了參數,而且調用了應用程序。

今天以URL路由爲例,來說講中間件在實際生產中是如何起做用的。

當服務器拿到了客戶端請求的URL,不一樣的URL須要交由不一樣的函數處理,這個功能叫作 URL Routing。

在 Nova 中是用 routes 這個庫來實現對URL的的路由調度。接下來,我將從源代碼處分析一下這個過程。

在routes模塊裏有個中間件,叫 routes.middleware.RoutesMiddleware ,它將接受到的 url,自動調用 map.match()方法,對 url 進行路由匹配,並將匹配的結果存入request請求的環境變量['wsgiorg.routing_args'],最後會調用self._dispatch(dispatch返回真正的application)返回response,最後會將這個response返回給 WSGI Server。

這個中間件的原理,看起來是挺簡單的。並無很複雜的邏輯。

可是,我在閱讀 routes 代碼的時候,卻發現了另外一個令我困惑的點。

self._dispatch (也就上圖中的self.app)函數裏,咱們看到了 app,controller 這幾個很重要的字眼,其是不是我苦苦追尋的 application 對象呢?

要搞明白這個問題,只要看清 match 到是什麼東西?

這個 match 對象 是在 RoutesMiddleware.__call__() 裏塞進 req.environ 的,它是什麼東西呢,我將其打印出來。

{'action': u'detail', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x667bad0>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}

{'action': u'index', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x6ec8910>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}

{'action': u'show', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x6ed9710>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9', 'id': u'68323d9c-ebe5-499a-92e9-32fea900a892'}

複製代碼

結果使人在失所望呀,這個 app 並非咱們要尋找的 Controller 對象。而是 nova.api.openstack.wsgi.ResourceV21 類的實例對象,說白了就是Resource 對象。

看到這裏,我有心態有點要崩了,怎麼還沒到 Controller?OpenStack 框架的代碼繞來繞去的,沒有點耐心還真的很難讀下去。

既然已經開了頭,沒辦法還得硬着頭皮繼續讀了下去。

終於我發現,在APIRouter初始化的時候,它會去註冊全部的 Resource,同時將這些 Resource 交由 routes.Mapper 來管理、建立路由映射,因此上面提到的 routes.middleware.RoutesMiddleware 才能根據url經過 mapper.match 獲取到相應的Resource。

從 Nova 代碼中看出每一個Resource 對應一個 Controller 對象,由於 Controller 對象自己就是對一種資源的操做集合。

經過日誌的打印,能夠發現 nova 管理的 Resource 對象有多麼的多而雜

os-server-groups
os-keypairs
os-availability-zone
remote-consoles
os-simple-tenant-usage
os-instance-actions
os-migrations
os-hypervisors
diagnostics
os-agents
images
os-fixed-ips
os-networks
os-security-groups
os-security-groups
os-security-group-rules
flavors
os-floating-ips-bulk
os-console-auth-tokens
os-baremetal-nodes
os-cloudpipe
os-server-external-events
os-instance_usage_audit_log
os-floating-ips
os-security-group-default-rules
os-tenant-networks
os-certificates
os-quota-class-sets
os-floating-ip-pools
os-floating-ip-dns
entries
os-aggregates
os-fping
os-server-password
os-flavor-access
consoles
os-extra_specs
os-interface
os-services
servers
extensions
metadata
metadata
limits
ips
os-cells
versions
tags
migrations
os-hosts
os-virtual-interfaces
os-assisted-volume-snapshots
os-quota-sets
os-volumes
os-volumes_boot
os-volume_attachments
os-snapshots
os-server-groups
os-keypairs
os-availability-zone
remote-consoles
os-simple-tenant-usage
os-instance-actions
os-migrations
os-hypervisors
diagnostics
os-agents
images
os-fixed-ips
os-networks
os-security-groups
os-security-groups
os-security-group-rules
flavors
os-floating-ips-bulk
os-console-auth-tokens
os-baremetal-nodes
os-cloudpipe
os-server-external-events
os-instance_usage_audit_log
os-floating-ips
os-security-group-default-rules
os-tenant-networks
os-certificates
os-quota-class-sets
os-floating-ip-pools
os-floating-ip-dns
entries
os-aggregates
os-fping
os-server-password
os-flavor-access
consoles
os-extra_specs
os-interface
os-services
servers
extensions
metadata
metadata
limits
ips
os-cells
versions
tags
migrations
os-hosts
os-virtual-interfaces
os-assisted-volume-snapshots
os-quota-sets
os-volumes
os-volumes_boot
os-volume_attachments
os-snapshots
複製代碼

你必定很好奇,這路由是如何建立的吧,關鍵代碼就是以下一行。若是你想要了解更多路由的建立過程,能夠看一下這篇文章(Python Route總結),寫得不錯。

routes.mapper.connect("server",
               "/{project_id}/servers/list_vm_state",
               controller=self.resources['servers'],
               action='list_vm_state',
               conditions={'list_vm_state': 'GET'})

複製代碼

歷盡了千辛萬苦,我終於找到了 Controller 對象,知道了請求發出後,wsgi server是如何根據url找到對應的Controller(根據routes.Mapper路由映射)。

可是很快,你又會問。對於一個資源的操做(action),有不少,好比新增,刪除,更新等

不一樣的操做要執行Controller 裏不一樣的函數。

若是是新增資源,就調用 create()

若是是刪除資源,就調用 delete()

若是是更新資源,就調用 update()

那代碼如何怎樣知道要執行哪一個函數呢?

以/servers/xxx/action請求爲例,請求調用的函數實際包含在請求的body中。

通過routes.middleware.RoutesMiddleware的__call__函數解析後,此時即將調用的Resource已經肯定爲哪一個模塊中的Controller所構建的Resource,而 action 參數爲"action",接下來在Resource的__call__ 函數裏面會由於action=="action"從而開始解析body的內容,找出Controller中所對應的方法。

Controller在構建的過程當中會因爲MetaClass的影響將其全部action類型的方法填入一個字典中,key由每一個_action_xxx方法前的 @wsgi.action('xxx')裝飾函數給出,value爲每一個_action_xxx方法的名字(從中能夠看出規律,在body裏面請求的方法名前加上_aciton_即爲Controller中對應調用的方法)。

以後在使用Controller構建Resource對象的過程當中會向Resource註冊該Controller的這個字典中的內容。這樣,只需在請求的body中給出調用方法的key,而後就能夠找到這個key所映射的方法,最後在Resource的__call__函數中會調用Controller類的這個函數!

其實我在上面咱們打印 match 對象時,就已經將對應的函數打印出來了。

這邊以 nova show(展現資源爲例),來理解一下。

當你調用 nova show [uuid] 命令,novaclient 就會給 nova-api 發送一個http的請求

nova show 1c250b15-a346-43c5-9b41-20767ec7c94b


複製代碼

經過打印獲得的 match 對象以下

{'action': u'show', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x667bad0>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}


複製代碼

其中 action 就是對應的處理函數,而controller 就對應的 Resource 對象,project_id 是租戶id(你能夠不理會)。

繼續看 ResourceV21 類裏的 __call__ 函數的代碼。

圖示地方,會從 environ 裏獲取中看到獲取 action 的具體代碼

我將這邊的 action_args打印出來

{'action': 'show', 'project_id': '2ac17c7c792d45eaa764c30bac37fad9', 'id': '1c250b15-a346-43c5-9b41-20767ec7c94b'}


複製代碼

其中 action 仍是是函數名,id 是要操做的資源的惟一id標識。

__call__ 的最後,會 調用 _process_stack 方法

在圖標處,get_method 會根據 action(函數名) 取得處理函數對象。

meth :<bound method ServersController.show of <nova.api.openstack.compute.servers.ServersController object at 0x7be3750>>


複製代碼

最後,再執行這個函數,取得 action_result,在 _process_stack 會對 response 進行初步封裝。

而後將 response 再返回到 wsgify ,由這個專業的工具函數,進行 response 的最後封裝和返回給客戶端。

至此,一個請求從發出到響應就結束了。


你能看到這裏,真的很可貴,本篇文章乾貨仍是很多的。由於我本身不太喜歡講理論,因此這次我結合了項目,對源碼進行實戰分析。

本來我就只是給本身提出了個小問題,沒想到給本身挖了這麼大一個坑,這篇文章前先後後一共花了兩個星期的時間,幾乎全部的下班時間都花在這裏了,這就是爲何近兩週更新如此少的緣故。

在這個過程當中,確實也學到了很多東西。不少內容都是站在巨人的肩膀上,感謝如此多優秀的網絡博客。同時這期間自行閱讀了大量的OpenStack 源碼,驗證了很多本身疑惑已久的知識點,對本身的提高也頗有幫助。

若文章有寫得不對的地方,還請你幫忙指出,最後感謝你的閱讀,若是文章對於有幫助,不防點個贊收藏一下。


附錄:參考文章


相關文章
相關標籤/搜索