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


【轉自 太陽尚遠的博客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 處理函數之前就可以進行一些額外的處理啦。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM