在 三百六十行,行行轉 IT 的現狀下,很多來自各行各業的同學,都選擇 Python 這門膠水語言做為踏入網際網路大門的第一塊敲門磚,在這些人裡,又有相當大比例的同學選擇了 Web 開發這個方向(包括我)。而從事 web 開發,繞不過一個知識點,就是 WSGI。
不管你是否是這些如上同學中的一員,都應該好好地學習一下這個知識點。
由於我本人不從事專業的 python web 開發,所以在寫這篇文章的時候,借鑑了許多優秀的網路部落格,並花了很多的精力閱讀了大量的 OpenStack 程式碼。
為了寫這篇文章,零零散散地花了大概兩個星期。本來可以拆成多篇文章,寫成一個系列的,經過一番思慮,還是準備一篇講完,這就是本篇文章這麼長的原因。
另外,一篇文章是不能吃透一個知識點的,本篇涉及的背景知識也比較多的,若我有講得不到位的,還請你多多查閱其他人的網路部落格進一步學習。
在你往下看之前,我先問你幾個問題,你帶著這些問題往下看,可能更有目的性,學習可能更有效果。
問1:一個 HTTP 請求到達對應的 application處理函式要經過怎樣的過程?
問2:如何不透過流行的 web 框架來寫一個簡單的web服務?
一個HTTP請求的過程可以分為兩個階段,第一階段是從客戶端到WSGI Server,第二階段是從WSGI Server 到WSGI Application
今天主要是講第二階段,主要內容有以下幾點:
- WSGI 是什麼,因何而生?
- HTTP請求是如何到應用程式的?
- 實現一個簡單的 WSGI Server
- 實現“高併發”的WSGI Server
- 第一次路由:PasteDeploy
- PasteDeploy 使用說明
- webob.dec.wsgify 裝飾器
- 第二次路由:中介軟體 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 物件有如下三點要求
- 必須是一個可呼叫的物件
- 接收兩個必選引數environ、start_response。
- 返回值必須是可迭代物件,用來表示http body。
02. HTTP請求是如何到應用程式的?
當客戶端發出一個 HTTP 請求後,是如何轉到我們的應用程式處理並返回的呢?
關於這個過程,細節的點這裡沒法細講,只能講個大概。
我根據其架構組成的不同將這個過程的實現分為兩種:
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)
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-packagespastedeploy)下。
我會先講下在 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 ,你只要按照以下三個步驟逐個完成即可。
1、配置 PasteDeploy使用的ini檔案;
2、定義WSGI應用;
3、透過loadapp函式載入WSGI應用;
第一步:寫 paste.ini 檔案
在寫之前,我們得知道 ini 檔案的格式吧。
首先,像下面這樣一個段叫做 section
。
[type:name] key = value ...
其上的type,主要有如下幾種
-
composite
(組合):多個app的路由分發;[composite:main] use = egg:Paste#urlmap / = home /blog = blog /wiki = wiki
-
app(應用):指明 WSGI 應用的路徑;
[app:home] paste.app_factory = example:Home.factory複製程式碼
-
pipeline(管道):給一個 app 繫結多個過濾器。將多個filter和最後一個WSGI應用串聯起來。
[pipeline:main] pipeline = filter1 filter2 filter3 myapp [filter:filter1] ... [filter:filter2] ... [app:myapp] ...
-
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')# 載入 appapplications = 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.
- 訪問,應該顯示:This is Wiki's response body.。
注意:urlmap對url的大小寫是敏感的,例如如果訪問,在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 的規範
- 必須接收
environ
,start_response
兩個引數; - 必須返回 「可迭代的物件」。
但從 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
你一定很好奇,這路由是如何建立的吧,關鍵程式碼就是如下一行。如果你想要了解更多路由的建立過程,可以看一下這篇文章(),寫得不錯。
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 原始碼,驗證了不少自己疑惑已久的知識點,對自己的提升也很有幫助。
更多python相關文章請關注。