您的位置:首页 > 大数据 > 人工智能

python之aiohttp源码解析——add_route和middleware的工作方式

2017-04-14 21:39 676 查看
因为最近在用python写一个博客,使用到了aiohttp,这是一个基于asyncio的http框架,支持服务器和客户端的模式。具体自行百度或谷歌啦。

之所以写下这篇博客是因为自己在使用的过程中对它的运行方式存在很多疑问,而且貌似能找到的资料很少,官方文档上也只是给出简单的使用方法。

这里要非常感谢这篇文章以及它的作者:

 太阳尚远的博客http://www.cnblogs.com/yeqf/p/5347542.html(aiohttp
源码解析之 request 的处理过程)

这篇文章很好的阐述了request的处理过程,给了我很大的启发。但是它也没有完全解答我的疑问,于是就有了这篇博客。建议看这篇文章之前先去阅读一下上面那篇。本文作为它的延伸个拓展。

一个aiohttp的简单服务器模型可以写成:

from aiohttp import web

async def hello(request):
return web.Response(text="Hello, world")

app = web.Application()
app.router.add_get('/', hello)
web.run_app(app)


以上是官网的例子。这里添加两个方法,和middleware来实现一个稍微复杂一点的例子来分析:

import asyncio
from aiohttp import web

#通过localhost:8080访问
async def index(request):
resp = web.Response(body=b'<h1>Index</h1>')
# 如果不添加content_type,某些严谨的浏览器会把网页当成文件下载,而不是直接显示
resp.content_type = 'text/html;charset=utf-8'
return resp

#通过localhost:8080/hello/输入一个字符串 访问
async def hello(request):
text = '<h1>hello,%s</h1>' % request.match_info['name']
resp = web.Response(body=text.encode('utf-8'))
#如果不添加content_type,某些严谨的浏览器会把网页当成文件下载,而不是直接显示
resp.content_type = 'text/html;charset=utf-8'
return resp

async def init(loop):
app = web.Application(middlewares={
logger1_factory, logger2_factory
})
app.router.add_route('GET','/',index)
app.router.add_route('GET','/hello/{name}',hello)
server = await loop.create_server(app.make_handler(),'localhost',8080)
print('accepting request.....')
return server

async def logger1_factory(app,handler):
async def logger1_handler(request):

print('i am logger1')
return await handler(request)
return logger1_handler

async def logger2_factory(app,handler):
async def logger2_handler(request):

print('i am logger2')
return await handler(request)
return logger2_handler

loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()


这里分析两点:第一点,在add_route的时候,aiohttp做了什么。第二点,在接收到请求的时候,aiohttp怎么处理,middleware是怎么工作的

第一点:在add_route的时候,aiohttp做了什么?

打个断点,然后调试分析。

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


app.router是一个UrlDispatcher对象,它有一个add_route方法,进去看源码:

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)


顺便贴上add_resource源码:

def add_resource(self, path, *, name=None):
if path and not path.startswith('/'):
raise ValueError("path should be started with / or be empty")
if not ('{' in path or '}' in path or self.ROUTE_RE.search(path)):
url = URL(path)
resource = PlainResource(url.raw_path, name=name) #这里path = '/'  name=None,参数比较简单,就不进去看了
self.register_resource(resource)
return resource
#以下省略若干行,add_resource还未完
对于path=‘/’,会生成一个PlainRescource对象,并且把这个对象注册到UrlDispatcher类的对象app.router里。先进入PlainResource类里看看,发现了一条继承链

PlainResource继承自Resource继承自AbstractResource。现在进去register_rescource看看:

def register_resource(self, resource):
assert isinstance(resource, AbstractResource), \
'Instance of AbstractResource class is required, got {!r}'.format(
resource)
………………省略
name = resource.name
………………省略
self._named_resources[name] = resource
self._resources.append(resource)
可以看到这个PlainRescource类的对象resource被添加到app.router的_resources属性list里。这时候回到上面,app.router的add_route方法里。

resource实际上是得到一个self._resources里一个刚构建的PlainResource的对象,然后对这个对象执行add_route方法,进入PlainResource类里查看这个add_route方法,但是PlainResource没有这个方法,于是到父类Resource里找,找到了这个add_route方法:

def add_route(self, method, handler, *,
expect_handler=None):
#先检查这个路径有没有注册过,不过这时候由于PlainResource是新创建的,self._routes是空list,会略过这一步
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) #method=‘GET’, handler = 'index',注意这里把self作为参数传进去了
self.register_route(route)
return route
由于这里参数比较多,为了跟踪handler参数是怎么被处理的,进去ResourceRoute的__init__方法看一下:

def __init__(self, method, handler, resource, *,
expect_handler=None):
super().__init__(method, handler, expect_handler=expect_handler,
resource=resource)
这里调用了父类的__init__方法,ResourceRoute的父类是AbstractRoute类,进去看它的__init__方法,重头戏要来了:

def __init__(self, method, handler, *,
expect_handler=None,
resource=None):

if expect_handler is None:
expect_handler = _defaultExpectHandler

assert asyncio.iscoroutinefunction(expect_handler), \
'Coroutine is expected, got {!r}'.format(expect_handler)

method = method.upper()
if not HTTP_METHOD_RE.match(method):
raise ValueError("{} is not allowed HTTP method".format(method))

assert callable(handler), handler
if asyncio.iscoroutinefunction(handler):
pass
elif inspect.isgeneratorfunction(handler):
warnings.warn("Bare generators are deprecated, "
"use @coroutine wrapper", DeprecationWarning)
elif (isinstance(handler, type) and
issubclass(handler, AbstractView)):
pass
else:#如果添加的方法既不是生成器,也不是协程。则aiohttp帮我们写一个处理函数的协程版本
@asyncio.coroutine
def handler_wrapper(*args, **kwargs):
result = old_handler(*args, **kwargs)
if asyncio.iscoroutine(result):
result = yield from result
return result
old_handler = handler  #此时handler被赋值给old_handler,即old_handler = index
handler = handler_wrapper #handler = handler_wrapper

self._method = method #self._method = 'GET'
self._handler = handler  #self._handler = index
self._expect_handler = expect_handler
self._resource = resource  #这里的resource是上一步中,刚创建的PlainResource类对象
这里可以看到,我们app.router.add_route里的handler,最终成了ResourceRoute的_handler属性。现在回去Resource的add_route方法里的,route得到了这个刚创建的ResourceRoute类对象,并且被PlainResource类对象self.register_route(route)注册,这里查看一下这个register_route方法:

def register_route(self, route):
assert isinstance(route, ResourceRoute), \
'Instance of Route class is required, got {!r}'.format(route)
self._routes.append(route)
到这里,app.router.add_route方法就执行完了。如果你第一次看这个流程估计会有点晕,我的表述也说不上多好,这里贴上我画的流程图帮助理解吧,希望能够帮到你。



第二点:在接收到请求的时候,aiohttp怎么处理,middleware是怎么工作的?

假设我们在浏览器中输入 localhost:8080  浏览器会向服务器发送一个请求,method = 'GET'  path = '/'

由于上面推荐的博文已经讲解了request是怎么被处理,配合我上面画的流程图,其实会很好理解。简而言之,服务器接收到浏览器请求后,进入 ServerHttpProtocol 类里的 start()函数,然后再进入RequestHandler 类的 hander_request() 函数。在hander_request函数里,根据浏览器的请求构造出了request对象,并用app._router(是一个UrlDispatcher类对象)的resolve方法处理这个request,在resolve方法里,遍历了_resource属性里的所有PlainResource类对象(是不是很熟悉?没错,就是上面app.router.add_route方法的时候创建的),然后从PlainResource里找到它的list属性_routes,遍历里面所有ResourceRoute类对象,找到它们可以跟请求的method和path相匹配的_handler方法,其实这个_handler方法就是我们添加的index方法r。

好累啊。。。。怎么还没写完。。。。是我太啰嗦了吗。。。。。。。%>_<%

抖擞精神,继续~

这时候轮到middleware登场了。还记得创建web.Application的时候传入的参数吗?

app = web.Application(middlewares={
logger1_factory, logger2_factory
})
在处理请求的源码中有这么一段:

for factory in app._middlewares:
handler = yield from factory(app, handler)
resp = yield from handler(request)
在老版本的aiohttp中是for factory in reversed(app._middlewares),现在在添加middleware的时候已经作了倒序处理,所以得到的第一个factory是logger2_factory。logger2_factory(app,index)返回的结果是logger2_handler(request)。所以新handler=logger2_handler(request)。这时候遍历到第二个factory,即logger1_factory(app,logger2_handler),返回的结果是logger1_handler(request)。所以新handler=logger1_handler(request)。

这时候往下执行

resp = yield from handler(request)
即执行了logger1_handler(request),打印了‘i am logger1’之后根据自己的handler参数,执行logger2_handler(request),打印了‘i am logger2’之后根据自己的handler参数执行index(request),返回index网页。

本文只分析了app.router.add_route('GET','/',index) 和访问localhost:8080时内部的处理过程。

对于app.router.add_route('GET','/hello/{name}',hello) 和访问localhost:8080/任意字符串 ,内部的处理过程是差不多的,无非就是多了一些对url分析判断的条件之类。

到这里,本文结束了。写这篇博客的同时,我自己也重温了一下,加深了认识。确实挺好的,就是花了不少时间。希望能帮到一些跟我一开始有同样疑问的同学啦。如果本文哪里说得不对,不恰当,希望可以指出来。(我个人的表述里对象啊,属性啊,实例啊,有时候混着用,其实都是一个意义,希望不会干扰各位解读。这是我自己的一个毛病,希望读者包涵。)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息