python中aiohttp request 的处理过程解析

使用过 python 的 aiohttp 第三方库的同学会知道,利用 aiohttp 来构造一个最简单的web服务器是非常轻松的事情,只需要下面几行代码就可以搞定:

from aiohttp 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))
    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,/index,index 这三个信息最终会被封装成一个 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 处理函数之前就可以进行一些额外的处理啦。

时间: 2024-12-26 06:09:48

python中aiohttp request 的处理过程解析的相关文章

Python中各种方法的运作原理解析

         这篇文章主要介绍了深入理解Python中各种方法的运作原理,包括抽象方法和静态方法和类方法等之间异同的比较,需要的朋友可以参考下             方法在Python中是如何工作的         方法就是一个函数,它作为一个类属性而存在,你可以用如下方式来声明.访问一个函数: ? 1 2 3 4 5 6 7 8 >>> class Pizza(object): ... def __init__(self, size): ... self.size = size

Python中使用ElementTree解析XML示例

  这篇文章主要介绍了Python中使用ElementTree解析XML示例,本文同时讲解了XML基本概念介绍.XML几种解析方法和ElementTree解析实例,需要的朋友可以参考下 [XML基本概念介绍] XML 指可扩展标记语言(eXtensible Markup Language). XML 被设计用来传输和存储数据. 概念一: 代码如下: # foo元素的起始标签 # foo元素的结束标签 # note: 每一个起始标签必须有对应的结束标签来闭合, 也可以写成 概念二: 代码如下: #

深入解析Python中的WSGI接口

  这篇文章主要介绍了深入解析Python中的WSGI接口,WSGI接口是Python中网络框架连接服务器的必备工具,需要的朋友可以参考下 概述 WSGI接口包含两方面:server/gateway 及 application/framework. server调用由application提供的可调用对象. 另外在server和application之间还可能有一种称作middleware的中间件. 可调用对象是指:函数.方法.类或者带有callable方法的实例. 关于application

详细解析Python中

  这篇文章主要介绍了详细解析Python中__init__()方法的高级应用,包括在映射和elif序列等地方的更为复杂的用法,需要的朋友可以参考下 通过工厂函数对 __init__() 加以利用 我们可以通过工厂函数来构建一副完整的扑克牌.这会比枚举所有52张扑克牌要好得多,在Python中,我们有如下两种常见的工厂方法: 定义一个函数,该函数会创建所需类的对象. 定义一个类,该类有创建对象的方法.这是一个完整的工厂设计模式,正如设计模式书所描述的那样.在诸如Java这样的语言中,工厂类层次结

浅谈Python中数据解析

  本文给大家介绍的是Python中的数据解析的集中方式,包括列表解析.字典解析.集合解析,并附上相关示例,有需要的小伙伴可以参考下. Import os; -- Python自带 print(os.getcwd()) -- 获得当前工作目录 os.chdir('/Users/longlong/Documents') -- 转换到/Users/longlong/Documents目录 os.path.join(parm1, parm2,...) -- 从一个或多个路径片段中构造一个路径名. os

协议解析-python中的正则表达匹配问题

问题描述 python中的正则表达匹配问题 20C ma = re.search(r""^x00x00x00x00x0d"" tcpapp[9:]) 请问这句是什么意思?? 是在qq报文解析中的程序片段. x00x00x00x00x0d如何理解 解决方案 看下基本原则吧 http://m.blog.csdn.net/article/details?id=49151633 解决方案二: Search(patternstringflags=0)方法在一个字符中查找匹配(

parserequest-SpringMVC中parseRequest(request)解析为空,items为0

问题描述 SpringMVC中parseRequest(request)解析为空,items为0 客户端代码: postMethod.addRequestHeader("Content-Type", "multipart/form-data;charset=UTF-8;boundary=----------ThIs_Is_tHe_bouNdaRY_$"); Part[] parts = { new StringPart("xmlhead", xm

Python中lambda的用法及其与def的区别解析_python

python中的lambda通常是用来在python中创建匿名函数的,而用def创建的方法是有名称的,除了从表面上的方法名不一样外,python中的lambda还有如下几点和def不一样: 1. python lambda会创建一个函数对象,但不会把这个函数对象赋给一个标识符,而def则会把函数对象赋值给一个变量. 2. python lambda它只是一个表达式,而def则是一个语句. 下面是python lambda的格式,看起来非常精简. lambda x: print x 如果你在pyt

在Python中使用SimpleParse模块进行解析的教程_python

与大多数程序员一样,我经常需要标识存在于文本文档中的部件和结构,这些文档包括:日志文件.配置文件.分隔的数据以及格式更自由的(但还是半结构化的)报表格式.所有这些文档都拥有它们自己的"小语言",用于规定什么能够出现在文档内. 我编写处理这些非正式解析任务的程序的方法总是有点象大杂烩,其中包括定制状态机.正则表达式以及上下文驱动的字符串测试.这些程序中的模式大概总是这样:"读一些文本,弄清是否可以用它来做些什么,然后可能再多读一些文本,一直尝试下去." 各种形式的解析