【轉自 "太陽尚遠的博客" : "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,/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 處理函數之前就可以進行一些額外的處理啦。
終於寫完了,鑒於博主水平有限,有寫的不妥的地方還請各位小伙伴留言指正,大家共同進步 ^_^