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

第一時間關關注!

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

在 “三百六十行,行行轉 IT” 的現狀下,很多來自各行各業的同學,都選擇 Python 這門膠水語言作為踏入互聯網大門的第一塊敲門磚,在這些人裡,又有相當大比例的同學選擇了 Web 開發這個方向(包括我,曾經也想選擇)。而做 web 開發,繞不過一個知識點,就是 WSGI。

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

由於我本人不從事專業的 Python Web 開發,所以在寫這篇文章的時候,借鑑了許多優秀的網絡博客,並花了很多的精力閱讀了大量的 OpenStack 代碼。

為了寫這篇文章,我零零散散地花了大概兩個星期的時間。本來可以拆成多篇文章,寫成一個系列的,經過一番思慮,還是準備一篇講完,這就是本篇文章這麼長的原因,微信後臺顯示將近15000字(有一定水分)。

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

在你往下看之前,我先問你幾個問題,你帶著這些問題往下看,可能更有目的性,學習起來效果會更好。

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

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

一個HTTP請求的過程可以分為兩個階段,第一階段是從客戶端到WSGI Server,第二階段是從 WSGI Server 到 WSGI Application

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

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

1、WSGI 是什麼,因何而生?

2、HTTP請求是如何到應用程序的?

3、實現一個簡單的 WSGI Server

4、實現“高併發”的WSGI Server

5、第一次路由:PasteDeploy

6、PasteDeploy 使用說明

7、webob.dec.wsgify 裝飾器

8、第二次路由:中間件 routes 路由

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

WSGI是 Web Server Gateway Interface 的縮寫。

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

它是一種協議,一種規範,其是在 PEP 3333 提出的。這個協議旨在解決眾多 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。

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

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

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

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

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

1、兩級結構

在這種結構裡,uWSGI作為服務器,它用到了HTTP協議以及wsgi協議,flask應用作為application,實現了wsgi協議。當有客戶端發來請求,uWSGI接受請求,調用flask app得到相應,之後相應給客戶端。

這裡說一點,通常來說,Flask等web框架會自己附帶一個wsgi服務器(這就是flask應用可以直接啟動的原因),但是這只是在開發階段用到的,在生產環境是不夠用的,所以用到了uwsgi這個性能高的wsgi服務器。

2、三級結構

這種結構裡,uWSGI作為中間件,它用到了uwsgi協議(與nginx通信),wsgi協議(調用Flask app)。當有客戶端發來請求,nginx先做處理(靜態資源是nginx的強項),無法處理的請求(uWSGI),最後的相應也是nginx回覆給客戶端的。

多了一層反向代理有什麼好處?

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

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

3. 實現一個簡單的 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 命令可以查到確實開啟了這個端口

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

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

4. 實現“高併發”的 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()

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

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

在如下的黃框裡,可以看到在這裡使用了service.WSGIService 啟動了一個 server,就是我們所說的的 wsgi server

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

那這裡的 WSGI Server 是依靠什麼實現的呢?讓我們繼續深入源代碼。

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

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

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

可是我們還沒有看到 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)

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

就這樣,Nova 開啟了一個可以接受1000個請求併發(理論值,應該有瓶頸)的 WSGI Server。

5. 第一次路由: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的實現。

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

通過打印的 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。推薦一定要看。

6. PasteDeploy 使用說明

到上一步,我已經得到了 application 的有用的線索。考慮到很多人是第一次接觸 PasteDeploy,所以這裡結合網上博客做了下總結。對你入門會有幫助。

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

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

2、定義WSGI應用;

3、通過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

  1. app(應用):指明 WSGI 應用的路徑;

[app:home]

paste.app_factory = example:Home.factory

  1. pipeline(管道):給一個 app 綁定多個過濾器。將多個filter和最後一個WSGI應用串聯起來。

[pipeline:main]

pipeline = filter1 filter2 filter3 myapp

[filter:filter1]

...

[filter:filter2]

...

[app:myapp]

...

  1. 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 函數可以接收兩個實參:

  1. WSGI 對於 application 對象有如下三點要求
  2. URI:"config:"

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 對象。

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

完善並整合第二步和第三步的內容,寫成一個 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

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

如果像上圖一樣一切正常,那麼打開瀏覽器

  • 訪問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 的簡單使用。

7. webob.dec.wsgify 裝飾器

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

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

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

APIRouterV21 本身沒有實現 __call__ ,但它的父類 Router實現了 __call__

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

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

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

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

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

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

可以看出,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 是如何為我們實現第二次路由。

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

在文章最開始處,我們給大家畫了一張圖。

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

這張圖把一個 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。

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

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

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

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

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

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

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

{'action': u'detail', 'controller': <nova.api.openstack.wsgi.resourcev21>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}/<nova.api.openstack.wsgi.resourcev21>

{'action': u'index', 'controller': <nova.api.openstack.wsgi.resourcev21>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}/<nova.api.openstack.wsgi.resourcev21>

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

結果令人在失所望呀,這個 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 對象本身就是對一種資源的操作集合。

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

通過日誌的打印,可以發現 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總結:https://blog.csdn.net/bellwhl/article/details/8956088),寫得不錯。

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的__all__ 函數里面會因為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>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}/<nova.api.openstack.wsgi.resourcev21>

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

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

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

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

我將這邊的 action_args打印出來

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

其中 action 還是是函數名,id 是要操作的資源的唯一id標識。

在 __call__ 的最後,會 調用 _process_stack 方法

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

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

meth :<bound>>/<bound>

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

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

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

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

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


你能看到這裡,真的很難得,本篇文章乾貨還是不少的。因為我自己不太喜歡講理論,所以此次我結合了項目,對源碼進行實戰分析。

原本我就只是給自己提出了個小問題,沒想到給自己挖了這麼大一個坑,這篇文章前前後後一共花了兩個星期的時間,幾乎所有的下班時間都花在這裡了,這就是為什麼近兩週更新如此少的緣故。

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

最後,還是那句老話,如果你覺得此文對你有幫助,不防點個在看,轉發一下。

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


分享到:


相關文章: