【轉】aiohttp 源碼解析之 request 的處理過程

来源:http://www.cnblogs.com/yeqf/archive/2016/04/02/5347542.html
-Advertisement-
Play Games

【轉自 "太陽尚遠的博客" : "http://blog.yeqianfeng.me/2016/04/01/python yield expression/" 】 使用過 python 的 aiohttp 第三方庫的同學會知道,利用 aiohttp 來構造一個最簡單的web伺服器是非常輕鬆的事情,只 ...


【轉自 太陽尚遠的博客http://blog.yeqianfeng.me/2016/04/01/python-yield-expression/
使用過 python 的 aiohttp 第三方庫的同學會知道,利用 aiohttp 來構造一個最簡單的web伺服器是非常輕鬆的事情,只需要下麵幾行代碼就可以搞定:

from aiphttp import web
import asyncio

def index(request):
    return web.Response(body=b'<h1>Hello World!</h1>')
    
async def init(loop):
    app = web.Application(loop=loop)
    app.router.add_route('GET', '/index', index)
    server = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
    return server

def main():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(init())
    loop.run_forever()

if __name__ == '__main__':
    main()

這樣我們就實現了一個最簡單的 web 伺服器...

運行這個 python 文件,再打開瀏覽器,在地址欄輸入 http://127.0.0.1:8000/index 你就能看見 Hello World 了。是不是很神奇?那麼有的同學到這裡就會疑惑了,當用戶在瀏覽器輸入 http://127.0.0.1:8000/index 的時候,伺服器究竟是怎麼把請求定位到我們的 url 處理函數 index(request) 里的呢?從代碼來看,可以肯定地判斷,是因為有

app.router.add_route('GET', '/index', index)

這行代碼的原因,伺服器才知道,你的 request 請求(method:GET path:/index) 需要讓 index(request)函數來處理。那麼行代碼的內部究竟做了什麼?伺服器是如何響應一個request請求的呢?讓我們打開單步調試,一步一步跟著伺服器的腳步,看看發生了什麼?

我們先看伺服器是如何接收到請求的,多打幾次斷點就不難發現,當有request進來的時候,伺服器會最終進入到 aiohttp 的 server.py 模塊的 ServerHttpProtocol 類里的 start()函數裡面:

@asyncio.coroutine
def start(self):
   """Start processing of incoming requests.

   It reads request line, request headers and request payload, then
   calls handle_request() method. Subclass has to override
   handle_request(). start() handles various exceptions in request
   or response handling. Connection is being closed always unless
   keep_alive(True) specified.
   """
   # 先看函數註釋,後面的代碼後面說

從源碼的註釋來看,這個函數就是伺服器開始處理request的地方了
繼續分析start()函數的代碼:

......
@asyncio.coroutine
def start(self):
    .......
    while True:
        message = None
        self._keep_alive = False
        self._request_count += 1
        self._reading_request = False

        payload = None
        try:
            # read http request method
            # ....
            # 中間省略若幹行...
            # ....
            yield from self.handle_request(message, payload)
            # ....

我們看到了,在這個代碼快的最後一句,將request交給了handle_request()函數去處理了,如果這個時候你在 ServerHttpProtocol 類裡面找 handler_request() 函數,會發現,它並不是一個 coroutine 的函數,究竟是怎麼回事呢? 我們單步執行到這裡看看,然後 F7 進入到這個函數裡面,發現原來這裡進入的並不是 ServerHttpProtocol 類里的函數,而是 web.py 里的 RequestHandler 類里的 handler_request() 函數,原來 RequestHandler 類是繼承自 ServerHttpProtocol 類的,它裡面覆寫了 hander_request() 函數,並用 @asyncio.coroutine 修飾了,我們看看它的代碼:

@asyncio.coroutine
def handle_request(self, message, payload):
    if self.access_log:
        now = self._loop.time()
    app = self._app
    # 此處才真正構造了Request對象
    request = web_reqrep.Request(
        app, message, payload,
        self.transport, self.reader, self.writer,
        secure_proxy_ssl_header=self._secure_proxy_ssl_header)
    self._meth = request.method
    self._path = request.path
    try:
        # 可以發現,這裡 match_info 的獲得是通過 self._router.resolve(request)函數來得到的。
        match_info = yield from self._router.resolve(request)
        # 得到的 match_info 必須為 AbstractMatchInfo 類型的對象
        assert isinstance(match_info, AbstractMatchInfo), match_info
        resp = None
        request._match_info = match_info
        ......
        if resp is None:
            handler = match_info.handler # 這個handler會不會就是我們的request的最終處理函數呢?
            for factory in reversed(self._middlewares):
                handler = yield from factory(app, handler)
            # 重點來了,這裡好像是在等待我們的 url 處理函數處理的結果啊
            resp = yield from handler(request)
    except:
        ......
    # 下麵這兩句的的作用就是將返回的結果送到客戶端了,具體的執行過程較為複雜,博主也就大致看了下,沒有做詳細思考。這裡就不說了。
    resp_msg = yield from resp.prepare(request)
    yield from resp.write_eof()
    ......

通過上面的代碼中的註釋,我們大致瞭解了幾個關鍵點:

  • 這個 match_info 究竟是什麼,是怎麼獲得的,他裡面包含了哪些屬性?
  • handler 又是什麼,又是怎麼獲得的?
  • handler(request) 看起來很像我們的 request 的最終處理函數,它的執行過程究竟是怎樣的?

瞭解了以上三點,基本上整個 request 請求的過程大概就瞭解了,我們一步一步來看。

先看第一點,match_info 是怎麼來的

還是看代碼,我們進入到 self._route.resolve(request) 的源碼中:

@asyncio.coroutine
def resolve(self, request):
    path = request.raw_path
    method = request.method
    allowed_methods = set()
    # 請留意這裡是 for 迴圈
    for resource in self._resources:
        match_dict, allowed = yield from resource.resolve(method, path)
        if match_dict is not None:
            return match_dict
        else:
            allowed_methods |= allowed
    else:
        if allowed_methods:
            return MatchInfoError(HTTPMethodNotAllowed(method,allowed_methods))
        else:
            return MatchInfoError(HTTPNotFound())

代碼量並不多,上面的代碼里的 path 和 method 就是 request 對象里封裝的客戶端的請求的 url 和 method(例如: /index 和 GET),註意到第9行,return 了一個 match_dict 對象,說明沒有差錯的話,正確的返回結果就是這個 match_dict。match_dict 又是啥呢? 看到 match_dict 通過 resource.resolve(method, path) 函數獲得的,我們不著急看這個函數的內部實現,我們先看看 resource 是什麼類型,這樣看肯定是看不出來的,唯一知道的是它是 self._resource (它是一個list)的元素,我們打開調試器,執行到這一步就可以看到, self._resource 中存儲的元素是 ResourceRoute 類型的對象,這個 ResourceRoute 我們先不細說,只知道它有一個 resolve() 的成員函數:

@asyncio.coroutine
def resolve(self, method, path):
    allowed_methods = set()
    match_dict = self._match(path)
    if match_dict is None:
        return None, allowed_methods
    for route in self._routes:
        route_method = route.method
        allowed_methods.add(route_method)
        if route_method == method or route_method == hdrs.METH_ANY:
            # 這裡的 return 語句是正常情況下的返回結果
            return UrlMappingMatchInfo(match_dict, route), allowed_methods
    else:
        return None, allowed_methods

我們發現了,之前說的那個 match_dict 原來就是一個 UrlMappingMatchInfo 對象,但是,細心的同學可以發現,這個函數里也有一個 match_dict 對象,這裡的 match_dict 是 self._match(path) 的返回結果, 那我們再看看 self._match(path) 是怎樣的一個過程,看調試信息的話,可以看到,這裡的 self 是 PlainResource 類,他的 _match() 方法如下所示:

def _match(self, path):
    # string comparison is about 10 times faster than regexp matching
    if self._path == path:
        return {}
    else:
        return None

代碼非常簡潔,就是將傳入的 path (比如 /index)與 PlainResource 類的實例的 _path 屬性比較,如果相等就返回一個空字典,否則返回 None,我想這個返回結果既然是空字典,那他的作用在上層調用處應該是作為一個 if 語句的判斷條件來用,事實也確實是這樣的。如果,這裡的 PlainResource 是什麼,我在這裡先告訴你,這是你在初始化伺服器的時為伺服器添加路由的時候就實例化的對象,它是作為app的一個屬性存在的,這裡先不管他,但是你要留意它,後面會講到它。

好了,我們再次回到 resolve(self, method, path) 函數中去(註意了,有兩個 resolve 函數,我用參數將他們區分開來),在獲得 match_dict 之後進行 None 的檢查,如果是 None ,說明request的 path 在 app 的route中沒有匹配的, 那就直接返回 None 了,在上上層的 resolve(self, request)函數里繼續遍歷下一個 resource 對象然後匹配(balabala...)。
如果 match_dict 不為 None,說明這個resource對象里的 path 和 request 里的 path 是匹配的,那麼就:

for route in self._routes:
    route_method = route.method
    allowed_methods.add(route_method)
    if route_method == method or route_method == hdrs.METH_ANY:
        # 這裡的 return 語句是正常情況下的返回結果
        return UrlMappingMatchInfo(match_dict, route), allowed_methods

這個操作是當 path 匹配的時候再檢查 method,如果這個 resource 的 method 與 request 的 method 也是相同的,或者 resource 的 method 是 "*",(星號會匹配所有的method),則 return 一個 UrlMappingMatchInfo 對象,構造時傳入了 match_dict 和 route,route 是 ResourceRoute 類型的對象,裡面封裝了 PlainResource 類型的對象,也就是 resource 對象。也就是說,現在返回的 UrlMappingMatchInfo 對象就是封裝了與 request 的 path 和 method 完全匹配的 PlainResource 對象。有點亂啊,是不是,只怪博主水平有限。。。

那麼現在理一理,這個 UrlMappingMatchInfo 返回到哪了,回顧一下上面的內容就發現了,返回到的地方是 resolve(self, request) 函數的 match_dict 對象,還記的麽,這個對象還在 for 迴圈里,match_dict 得到返回值,就判斷是否為 None, 如果是 None 就繼續匹配下一個 PlainResource(後面會說到這個 PlainResource 是怎麼來的,先不要急),如果不是 None,就直接返回 match_dict(是一個UrlMappingMatchInfo對象),這個 match_dict 返回給了誰?不急,再往前翻一翻,發現是返回給了 handler_request(self, message, payload) 函數的 match_info 了,回頭看 handler_request() 的代碼,要求 match_info 是 AbstractMatchInfo 類型的,其實並不矛盾,因為 UrlMappingMatchInfo 類就是繼承自 AbstractMatchInfo 類的。

好了,現在第一個問題搞明白了,我們知道了match_info 是什麼,從哪來的,裡面封裝了那些信息。

現在我們再看看 handler 是什麼:

我們繼續看 handler_request(self, message, payload):

# 這裡是將返回的 match_info 封裝到了 request 對象中了,以便後面使用,先不管他
request._match_info = match_info
......  # 省略號是省去了部分不作為重點的代碼
if resp is None:
    # 這裡我們得到了 handler,看看它究竟是什麼
    handler = match_info.handler
    for factory in reversed(self._middlewares):
        handler = yield from factory(app, handler)
    resp = yield from handler(request)

終於又回到了我們的 handler 了,可以看到,handler 其實是 match_info 的一個屬性,但是我們看調試信息的話發現 match_info 並沒有 handler 這一屬性,原因是因為調試視窗能顯示的都是非函數的屬性,python中,函數也屬於對象的屬性之一,而這裡的 handler 恰好就是一個函數,所以返回的 handler 才能是一個可調用的對象啊。閑話不多說,我們的目的是搞清楚 handler 到底是什麼,為了弄清楚 match_info.handler 是啥,我們進入 AbstractMatchInfo 類裡面看看:

class AbstractMatchInfo(metaclass=ABCMeta):
    ......
    @asyncio.coroutine  # pragma: no branch
    @abstractmethod
    def handler(self, request):
        """Execute matched request handler"""
    ......

很明顯,handler 是一個抽象方法,它的具體實現應該在其子類里,所以我們再看看 UrlMappingMatchInfo 類:

class UrlMappingMatchInfo(dict, AbstractMatchInfo):
    ......
    @property
    def handler(self):
        return self._route.handler
    ......

原來 handler() 函數返回的是 UrlMappingMatchInfo 的 self._route.handler,這個 _route 又是啥呢?不知道就看調試信息啊~,看了調試信息後,原來 _route 是一個 ResourceRoute 類型的對象:
調試視窗
細心的同學會發現,即便是 _route,也依然沒有看到 hanler 啊,說明 handler 在 ResourceRoute 類里也是個函數。所以...,還要去看看 ResourceRoute 類:

class ResourceRoute(AbstractRoute):
    """A route with resource"""
    ......
    # 剩下的不貼了

我找了半天發現並沒有 handler() 函數啊,好,那我們就去它的父類找去:

class AbstractRoute(metaclass=abc.ABCMeta):
    def __init__(self, method, handler, *,
                 expect_handler=None,
                 resource=None):
        self._method = method
        # 此處給 _handler 賦值
        self._handler = handler
        ......
    # 返回的是self._handler
    @property
    def handler(self):
        return self._handler
    ......

哈哈,原來在這裡,小婊砸終於找到你啦。原來層層 handler 的最終返回的東西是 AbstractRoute 類里的 _handler,可以發現這個 _handler 是在 AbstractRoute 構造函數里給它賦值的,那麼這個 AbstractRoute 類型的對象什麼時候會實例化呢?

現在我們回到最原始的地方,就是:

app.router.add_route('GET', '/index', index)

到了這裡,就有必要說一下了,這個 app.router 返回的其實是一個 UrlDispatcher 對象,在 Application 類裡面有一個 @property 修飾的 router() 函數,返回的是Application對象的 _router 屬性,而 _router 代表的就是一個 UrlDispatcher 對象。所以,上面的 add_route() 函數其實是 UrlDisparcher 類的成員函數。這個 add_route() 究竟又做了什麼事呢?。進入到 add_route()函數內部:

class UrlDispatcher(AbstractRouter, collections.abc.Mapping):
    ......
    def add_route(self, method, path, handler, *, name=None, expect_handler=None):
        resource = self.add_resource(path, name=name)
        return resource.add_route(method, handler,
                                  expect_handler=expect_handler)
    ......
    
    def add_resource(self, path, *, name=None):
        if not path.startswith('/'):
            raise ValueError("path should be started with /")
        if not ('{' in path or '}' in path or self.ROUTE_RE.search(path)):
            # 註意這裡構造的 resource 對象是 PlainResource 類型的
            resource = PlainResource(path, name=name)
            self._reg_resource(resource)
            return resource

出於方便,我把接下來要分析的代碼塊也貼在上面,反正都是 UrlDispatcher 類的成員函數。。
看上面的註釋就知道了,函數 add_resource() 返回了一個 PlainResource 類型的對象,前面多次提到的 PlainResource 終於在這裡看到了來源,構造 resource 對象的時候把傳入 add_route()中的 path 給封裝進去了。然後就到了:

return resource.add_route(method, handler,
                                  expect_handler=expect_handler)

看來 PlainResource 類裡面也有一個 add_route() 成員函數,我們繼續 F7 進入PlainResource 的 add_route()裡面:

class Resource(AbstractResource):
    ......
    def add_route(self, method, handler, *,expect_handler=None):
        for route in self._routes:
            if route.method == method or route.method == hdrs.METH_ANY:
                raise RuntimeError("Added route will never be executed, "
                                   "method {route.method} is "
                                   "already registered".format(route=route))
        route = ResourceRoute(method, handler, self,expect_handler=expect_handler)
        self.register_route(route)
        return route
    ......

這個函數實例化了一個 ResourceRoute 對象 route,並且把我們一步步傳進來的 method 和handler(真正的 URL 處理函數)也傳入了 ResourceRoute 的構造方法中,我們來看看這個 ResourceRoute 類的情況:

class ResourceRoute(AbstractRoute):
    """A route with resource"""
    def __init__(self, method, handler, resource, *, expect_handler=None):
        super().__init__(method, handler, expect_handler=expect_handler, resource=resource)

驚喜的發現原來 ResourceRoute 就是 AbstractRoute 的子類,實例化的時候需要調用父類的構造方法,所以我們剛纔疑問的 AbstractRoute 類就是在這個時候實例化的,其內部的 _handler 屬性也是在這個時候賦值的,也就是對應下麵這句話中的 index 函數,

app.router.add_route('GET', '/index', index)

這樣一來,我們添加路由的時候,GET/indexindex 這三個信息最終會被封裝成一個 ResourceRoute 類型的對象,然後再經過層層封裝,最終會變成 app 對象內部的一個屬性,你多次調用這個方法添加其他的路由就會有多個 ResourceRoute 對象封裝進 app.

好了,我們終於也弄清了 handler 的問題,看來 handler 所指向的確實就是我們最終的 url 處理函數。

這樣我們再回到 handle_request() 中看:

@asyncio.coroutine
def handle_request(self, message, payload):
    ......
    handler = match_info.handler
    for factory in reversed(self._middlewares):
        handler = yield from factory(app, handler)
    resp = yield from handler(request)
    .......

看明白了吧,得到了匹配 request 的 handler,我們就可以放心的調用它啦~~

這裡或許有的同學還有一個疑問,就是中間那個 for 迴圈是乾什麼的,我在這裡簡單解釋一下。這裡其實是涉及到初始化 app 的時候所賦值的另一個參數 middlewares,就像這樣:

app = web.Application(loop=loop, middlewares=[
        data_factory, response_factory, logger_factory])

middlewares 其實是一種攔截器機制,可以在處理 request 請求的前後先經過攔截器函數處理一遍,比如可以統一列印 request 的日誌等等,它的原理就是 python 的裝飾器,不知道裝飾器的同學還請自行谷歌,middlewares 接收一個列表,列表的元素就是你寫的攔截器函數,for 迴圈里以倒序分別將 url 處理函數用攔截器裝飾一遍。最後再返回經過全部攔截器裝飾過的函數。這樣在你最終調用 url 處理函數之前就可以進行一些額外的處理啦。

終於寫完了,鑒於博主水平有限,有寫的不妥的地方還請各位小伙伴留言指正,大家共同進步 ^_^

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 註:本文參考自 http://www.jianshu.com/p/0465a2b837d2 swagger用於定義API文檔。 好處: 前後端分離開發 API文檔非常明確 測試的時候不需要再使用URL輸入瀏覽器的方式來訪問Controller 傳統的輸入URL的測試方式對於post請求的傳參比較麻煩 ...
  • 最近這幾天,一直在思考寫伺服器的時候怎麼做資料庫的讀寫服務,用什麼架構來做這個事情,現在終於有了一個大概的想法,用redis+mysql的方法。 目前業內有兩種思路,一種是full-mem模式,即全用redis存儲這種方式。另外一種是redis只存熱數據,大部分數據放到mysql里。具體選哪種還是要 ...
  • 在 PHP 中,預設的錯誤處理很簡單。一條錯誤消息會被髮送到瀏覽器,這條消息帶有文件名、行號以及描述錯誤的消息。 PHP 錯誤處理 在創建腳本和 Web 應用程式時,錯誤處理是一個重要的部分。如果您的代碼缺少錯誤檢測編碼,那麼程式看上去很不專業,也為安全風險敞開了大門。 本教程介紹了 PHP 中一些 ...
  • 當對字元串進行修改的時候,需要使用StringBuffer和StringBuilder類。 和String類不同的是,StringBuffer和StringBuilder類的對象能夠被多次的修改,並且不產生新的未使用對象。 StringBuilder類在Java 5中被提出,它和StringBuff ...
  • import com.sun.image.codec.jpeg.JPEGCodec; 在Eclipse中處理圖片,需要引入兩個包: import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGImage ...
  • 一、協程簡介 什麼是協程? 協程,又稱微線程,線程,英文名Coroutine。協程是一種用戶態的輕量級線程 協程擁有自己的寄存器上下文和棧。 簡單來說,協程就是來回切換,當遇到IO操作,如讀寫文件,網路操作時,就跳到另一個線程執行,再遇到IO操作,又跳回來。不斷的跳過去跳過來執行,因為速度很快,所以 ...
  • 如果使用的是redis2.x,在項目中使用客戶端分片(Shard)機制。(具體使用方式:第九章 企業項目開發--分散式緩存Redis(1) 第十章 企業項目開發--分散式緩存Redis(2)) 如果使用的是redis3.x中的集群,在項目中使用jedisCluster。 1、項目結構 2、pom.x ...
  • HashMap簡介: HashMap在日常的開發中應用的非常之廣泛,它是基於Hash表,實現了Map介面,以鍵值對(key-value)形式進行數據存儲,HashMap在數據結構上使用的是數組+鏈表。允許null鍵和null值,不保證鍵值對的順序。 HashMap檢索數據的大致流程: 當我們使用Ha ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...