Python 的異步 IO:Aiohttp Client 代碼分析

Python 的異步 IO:Aiohttp Client 代碼分析

Aiohttp 是 Python 的一個 HTTP 框架,基於 asyncio,因此叫 Aiohttp。html

我主要是看源碼,想理解它的設計,因此附上了類圖與時序圖。不可避免的,可讀性會比較差。
想找教程的話,請移步 官方教程,寫得仍是挺不錯的。python

一個例子

下面這個例子,經過 HTTP GET 列出 GitHub 的 public eventsgit

import asyncio
import aiohttp

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://api.github.com/events') as resp:
            print(resp.status)
            print(await resp.text())

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Response 是一個 JSON 格式的文本:github

[
  {
    "id": "6888907432",
    "type": "PushEvent",
    "actor": {
      "id": 3956266,
      "login": "sekineh",
      "display_login": "sekineh",
      "gravatar_id": "",
      "url": "https://api.github.com/users/sekineh",
      "avatar_url": "https://avatars.githubusercontent.com/u/3956266?"
    },
    ...
]

ClientSession 是一個 Asynchronous Context Manager,因此搭配 async with 語句一塊兒使用。像下面這樣應該也是能夠的:json

async def main():
    session = aiohttp.ClientSession()
    ...
    await session.close()

不過確定是不推薦的,就當是幫助理解吧。api

ClientSession.get() 返回一個 ClientResponse 對象,經過 text() 方法,能夠拿到 response 的文本:session

print(await resp.text())

固然,text() 是一個協程:app

@asyncio.coroutine
    def text(self, encoding=None, errors='strict'):
        """Read response payload and decode."""
        ...

Connector

ClientSession 依賴 Connector 來建立鏈接,缺省爲 TCPConnector,它繼承自 BaseConnector,此外還有 UnixConnector(應該是 Unix Domain Socket)。框架

Connector 的接口比較簡單,主要提供了 connect() 方法(也是協程):異步

@asyncio.coroutine
    def connect(self, req):
        """Get from pool or create new connection."""
        ...

以及 close() 方法:

def close(self):
        """Close all opened transports."""
        ...

ConnectionKey

ClientRequest 有個屬性 connection_key

class ClientRequest:
    @property
    def connection_key(self):
        return ConnectionKey(self.host, self.port, self.ssl)

它是一個 namedtuple

ConnectionKey = namedtuple('ConnectionKey', ['host', 'port', 'ssl'])

hostportssl 三個元素組成,這三個元素能夠惟必定義一個鏈接,因此叫 ConnectionKey
文章開頭的那個例子中,ConnectionKey 爲:

ConnectionKey(host='api.github.com', port=443, ssl=True)

全局函數 request()

Aiohttp 爲 Client 程序提供了一個全局函數 request(),用法以下:

async def main():
    resp = await aiohttp.request('GET', 'http://python.org/')
    print(resp)
    
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

可見 request() 只是 ClientSession 的一個簡單封裝,其步驟大體爲:

  • 建立 TCPConnector
  • 建立 ClientSession
  • 調用 ClientSession._request()

建議不要直接使用 request(),而只把它當成 ClientSession 的一個樣例。由於 Aiohttp 官方文檔是 這樣說的

Don’t create a session per request. Most likely you need a session per application which performs all requests altogether.

A session contains a connection pool inside, connection reusage and keep-alives (both are on by default) may speed up total performance.

即,一個 request 用一個 session,太浪費;一般是一個 application 用一個 session。

一些小問題

我常常發現一個變量,明明能夠是局部變量,卻被當成了成員變量。

Request 裏放了一個 response?

class ClientRequest:
    def send(self, conn):
        ...
        self.response = self.response_class(
            self.method, self.original_url,
            writer=self._writer, ...
        )

        self.response._post_init(self.loop, self._session)
        return self.response

self.responseClientRequest 其餘地方並無用到,是否能夠改爲局部變量?

ClientResponse.start() 裏的 _protocol 應該用局部變量吧?

class ClientResponse:
   @asyncio.coroutine
    def start(self, connection, read_until_eof=False):
        """Start response processing."""
        self._closed = False
        self._protocol = connection.protocol

類圖

圖片描述

時序圖

圖片描述

The End

相關文章
相關標籤/搜索