【轉】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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...