1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 16132字,需瀏覽 33分鐘

         ·

        2020-09-18 12:25

        ??Python貓” ,一個值得加星標的公眾號

        4a10b79f5df51514ddf11d5a0a806efe.webp

        劇照 | 《大話西游之大圣娶親》

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

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

        由于我本人不從事專業(yè)的 Python Web 開發(fā),所以在寫這篇文章的時候,借鑒了許多優(yōu)秀的網絡博客,并花了很多的精力閱讀了大量的 OpenStack 代碼。

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

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

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

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

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

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

        574820199cd2f5239dad07cc5e75836d.webp

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

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

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

        WSGI是 Web Server Gateway Interface 的縮寫。

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

        它是一種協(xié)議,一種規(guī)范,其是在 PEP 3333 提出的。這個協(xié)議旨在解決眾多 web 框架和web server軟件的兼容問題。有了WSGI,你不用再因為你使用的web 框架而去選擇特定的 web server軟件。

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

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

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

        WSGI 接口有服務端和應用端兩部分,服務端也可以叫網關端,應用端也叫框架端。服務端調用一個由應用端提供的可調用對象。如何提供這個對象,由服務端決定。例如某些服務器或者網關需要應用的部署者寫一段腳本,以創(chuàng)建服務器或者網關的實例,并且為這個實例提供一個應用實例。另一些服務器或者網關則可能使用配置文件或其他方法以指定應用實例應該從哪里導入或獲取。

        WSGI 對于 application 對象有如下三點要求:

        1. 必須是一個可調用的對象

        2. 接收兩個必選參數environ、start_response。

        3. 返回值是可迭代對象,用來表示http body。

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

        當客戶端發(fā)出一個 HTTP 請求后,是如何轉到我們的應用程序處理并返回的呢?

        關于這個過程,細節(jié)的點這里沒法細講,只能講個大概。

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

        9757c3ea04a3b87f6190cc50b899aa29.webp

        1、兩級結構
        在這種結構里,uWSGI作為服務器,它用到了HTTP協(xié)議以及wsgi協(xié)議,flask應用作為application,實現了wsgi協(xié)議。當有客戶端發(fā)來請求,uWSGI接受請求,調用flask app得到相應,之后相應給客戶端。
        這里說一點,通常來說,Flask等web框架會自己附帶一個wsgi服務器(這就是flask應用可以直接啟動的原因),但是這只是在開發(fā)階段用到的,在生產環(huán)境是不夠用的,所以用到了uwsgi這個性能高的wsgi服務器。

        2、三級結構
        這種結構里,uWSGI作為中間件,它用到了uwsgi協(xié)議(與nginx通信),wsgi協(xié)議(調用Flask app)。當有客戶端發(fā)來請求,nginx先做處理(靜態(tài)資源是nginx的強項),無法處理的請求(uWSGI),最后的相應也是nginx回復給客戶端的。
        多了一層反向代理有什么好處?

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

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

        3. 實現一個簡單的 WSGI Server

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

        從其名字上就看出,它是用純Python編寫的WSGI服務器的參考實現。所謂“參考實現”是指該實現完全符合WSGI標準,但是不考慮任何運行效率,僅供開發(fā)和測試使用。

        有了 wsgiref 這個模塊,你就可以很快速的啟動一個wsgi server。

        from?wsgiref.simple_server?import?make_server

        #?這里的?appclass?暫且不說,后面會講到
        app?=?appclass()
        server?=?make_server('',?64570,?app)
        server.serve_forever()

        當你運行這段代碼后,就會開啟一個 wsgi server,監(jiān)聽 0.0.0.0:64570 ,并接收請求。

        使用 lsof 命令可以查到確實開啟了這個端口

        55e729dc9348ec84c3b80315693b6fa7.webp

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

        4. 實現“高并發(fā)”的 WSGI Server

        上面我們說不能在生產中使用 wsgiref ,那在生產中應該使用什么呢?選擇有挺多的,比如優(yōu)秀的 uWSGI,Gunicore等。但是今天我并不準備講這些,一是因為我不怎么熟悉,二是因為我本人從事 OpenStack 的二次開發(fā),對它比較熟悉。

        所以下面,是我花了幾天時間閱讀 OpenStack 中的 Nova 組件代碼的實現,剛好可以拿過來學習記錄一下,若有理解偏差,還望你批評指出。

        在 nova 組件里有不少服務,比如 nova-api,nova-compute,nova-conductor,nova-scheduler 等等。

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

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

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

        23d5454db923454d47ce80e0540e59b5.webpe4741db4bdec81a35d517e02d78852f4.webp

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

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

        4ff6ce44e35875f480b1bf40f25c270b.webp

        那這里的 WSGI Server 是依靠什么實現的呢?讓我們繼續(xù)深入源代碼。

        398b00ea881a632d5776aa7da89d2882.webp

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

        8b8b394709962ed1393ac4457df14283.webp

        可是我們還沒有看到 WSGI Server 的身影,上面使用eventlet 開啟了線程池,那線程池里的每個線程應該都是一個服務器吧?它是如何接收請求的?

        再繼續(xù)往下,可以發(fā)現,每個線程都是使用 eventlet.wsgi.server 開啟的 WSGI Server,還是使用的 eventlet。

        由于源代碼比較多,我提取了主要的代碼,精簡如下

        #?創(chuàng)建綠色線程池
        self._pool?=?eventlet.GreenPool(self.pool_size)

        #?創(chuàng)建 socket:監(jiān)聽的ip,端口
        bind_addr?=?(host,?port)
        self._socket?=?eventlet.listen(bind_addr,?family,?backlog=backlog)
        dup_socket?=?self._socket.dup()

        #?整理孵化協(xié)程所需的各項參數
        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
        }

        #?孵化協(xié)程
        self._server?=?utils.spawn(**wsgi_kwargs)

        efe3207e56252f8f16769befa8256773.webp

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

        5. 第一次路由:PasteDeploy

        上面我們提到 WSGI Server 的創(chuàng)建要傳入一個 Application,用來處理接收到的請求,對于一個有多個 app 的項目。

        比如,你有一個個人網站提供了如下幾個模塊

        /blog??#?博客?app
        /wiki??#?wiki?app

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

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

        PasteDeploy 到底是做什么的呢?

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

        PasteDeploy 是用來尋找和配置WSGI應用和服務的系統(tǒng)。PasteDeploy給開發(fā)者提供了一個簡單的函數loadapp。通過這個函數,可以從一個配置文件或者Python egg中加載一個WSGI應用。

        使用PasteDeploy的其中一個重要意義在于,系統(tǒng)管理員可以安裝和管理WSGI應用,而無需掌握與Python和WSGI相關知識。

        由于 PasteDeploy 原來是屬于 Paste 的,現在獨立出來了,但是安裝的時候還是會安裝到paste目錄(site-packages\paste\deploy)下。

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

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

        具體可以,看下Nova的實現。

        986f4c8015e3139e58e4a8877f122182.webp

        通過打印的 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 是兩個概念,需要注意區(qū)分) ,可以看出 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 實現的第一層的路由,如果你不感興趣,可以直接略過本節(jié),進入下一節(jié),下一節(jié)是 介紹 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的路由分發(fā);

          [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 規(guī)范的 applicaiton 對象。

        符合 WSGI 規(guī)范的 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,?監(jiān)聽?localhost:22800?
        server?=?make_server("localhost",?"22800",?applications)
        server.serve_forever()

        applications 是URLMap 對象。

        721933820bca1f9ce23c057ca664b778.webp

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

        一切都準備好后,在終端執(zhí)行 python wsgi_server.py來啟動 web server

        c67f59536d311f8b9cfeeb3237caa672.webp

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

        • 訪問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 類的一個實例。

        f85f2958483f2f39532544177ab76738.webp

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

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

        aacdd7678bdd6c5d158b68f46f50daca.webp

        我們知道,application 必須遵叢 WSGI 的規(guī)范

        1. 必須接收environ, start_response兩個參數;

        2. 必須返回 「可迭代的對象」。

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

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

        77afb6427310a82e23c2a91f03387e71.webp

        可以看出,wsgify 在這里,會將 req 這個原始請求(dict對象)封裝成 Request 對象(就是規(guī)范1里提到的 environ)。然后會一層一層地往里地執(zhí)行被wsgify裝飾的函數(self._route), 得到最內部的核心application。

        上面提到了規(guī)范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 路由

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

        6d2e4bd593c019019b635dc769b4a91f.webp

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

        實際上在 WSGI Server 到 WSGI Application 這個過程中,我們加很多的功能(比如鑒權、URL路由),而這些功能的實現方式,我們稱之為中間件。

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

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

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

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

        c4da21c4bd04fa84e6dcc532a8cb354d.webp

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

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

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

        ace076ffda70f8432579e77679cb5df3.webp

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

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

        {'action':?u'detail',?'controller':?0x667bad0>,?'project_id':?u'2ac17c7c792d45eaa764c30bac37fad9'}

        {'action':?u'index',?'controller':?0x6ec8910>,?'project_id':?u'2ac17c7c792d45eaa764c30bac37fad9'}

        {'action':?u'show',?'controller':?0x6ed9710>,?'project_id':?u'2ac17c7c792d45eaa764c30bac37fad9',?'id':?u'68323d9c-ebe5-499a-92e9-32fea900a892'}

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

        看到這里,我有心態(tài)有點要崩了,怎么還沒到 Controller?OpenStack 框架的代碼繞來繞去的,沒有點耐心還真的很難讀下去。

        既然已經開了頭,沒辦法還得硬著頭皮繼續(xù)讀了下去。

        終于我發(fā)現,在APIRouter初始化的時候,它會去注冊所有的 Resource,同時將這些 Resource 交由 routes.Mapper 來管理、創(chuàng)建路由映射,所以上面提到的 routes.middleware.RoutesMiddleware 才能根據url通過 mapper.match 獲取到相應的Resource。

        從 Nova 代碼中看出每個Resource 對應一個 Controller 對象,因為 Controller 對象本身就是對一種資源的操作集合。

        e933824a2316b3a06f078976e5c3deb8.webp

        通過日志的打印,可以發(fā)現 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

        你一定很好奇,這路由是如何創(chuàng)建的吧,關鍵代碼就是如下一行。如果你想要了解更多路由的創(chuàng)建過程,可以看一下這篇文章(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 對象,知道了請求發(fā)出后,wsgi server是如何根據url找到對應的Controller(根據routes.Mapper路由映射)。

        但是很快,你又會問。對于一個資源的操作(action),有很多,比如新增,刪除,更新等

        不同的操作要執(zhí)行Controller 里不同的函數。

        • 如果是新增資源,就調用 create()

        • 如果是刪除資源,就調用 delete()

        • 如果是更新資源,就調用 update()

        那代碼如何怎樣知道要執(zhí)行哪個函數呢?

        以/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方法的名字(從中可以看出規(guī)律,在body里面請求的方法名前加上_aciton即為Controller中對應調用的方法)。

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

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

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

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

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

        通過打印得到的 match 對象如下

        {'action':?u'show',?'controller':?0x667bad0>,?'project_id':?u'2ac17c7c792d45eaa764c30bac37fad9'}

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

        繼續(xù)看 ResourceV21 類里的 __call__ 函數的代碼。

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

        2d2be9060f0440abab1c126597416271.webp

        我將這邊的 action_args打印出來

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

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

        __call__ 的最后,會 調用 _process_stack 方法

        4a0cba3535ea042b4733622d3b9e808c.webp

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

        meth?:0x7be3750>>

        最后,再執(zhí)行這個函數,取得 action_result,在 _process_stack 會對 response 進行初步封裝。

        9e5ece8b9ca633d2d328d5d9cc3930cd.webp

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

        77afb6427310a82e23c2a91f03387e71.webp

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


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

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

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

        最后,還是那句老話,如果你覺得此文對你有幫助,不防點個在看,轉發(fā)一下。1abaefc329adc8a9e3f5e123a3b10f9e.webp

        參考文章


        https://zhuanlan.zhihu.com/p/27600327

        https://www.cnblogs.com/Security-Darren/p/4087587.html

        http://www.fmttr.com/python/thirdpartylibrary/pastedeploy/

        https://blog.csdn.net/baidu_35085676/article/details/80184874








        c8c51355e5a99f2853b7bb762488213d.webp

        優(yōu)質文章,推薦閱讀:

        詳解增強算術賦值:“-=”操作是怎么實現的?

        Python 沒有指針,如何解算法題?

        深度剖析為什么 Python 中整型不會溢出?

        Python小技巧:如何批量更新已安裝的庫?

        感謝創(chuàng)作者的好文badb72b887c88a6be15392fe3b4fec25.webp
        瀏覽 45
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            中文字幕无码不卡免费视频 | 久久99久久久久 | 国产伦理久久精品久久久久 | 操逼12p | 扒开女人下部添高潮的软件 | 苍井空二区| 三级无码不卡度 | 亚洲第一免费视频 | 七七久久| 毛片一区二区 |