前言

aiohttp 请求生命周期对比requests库使用的区别

aiohttp 客户端 API

当你第一次使用 aiohttp 时,你会注意到一个简单的 HTTP 请求不是一次执行的,而是最多三个步骤:

async with aiohttp.ClientSession() as session:
    async with session.get('http://python.org') as response:
        print(await response.text())

当来自其他库时尤其出乎意料,例如非常流行的requests,其中"hello world"看起来像这样:

response = requests.get('http://python.org')
print(response.text)

那么为什么 aiohttp 片段如此冗长呢?

  • 因为 aiohttp 是异步的,所以它的 API 旨在充分利用非阻塞网络操作。在这样的代码中,requests 会阻塞 3 次,并且是透明的,而 aiohttp 给了事件循环 3 次切换上下文的机会:
    执行 时.get(),两个库都会向远程服务器发送 GET 请求。对于aiohttp,这意味着异步I/O,这里用一个标记,它可以保证它不仅不会阻塞,而且它已经干净地完成了。async with

  • 在请求中执行response.text时,您只需读取一个属性。以阻塞方式调用.get()已经预加载和解码的整个响应负载。aiohttp 在.get()执行时仅加载标头,让您决定在第二个异步操作中支付之后加载正文的成本。因此.await response.text()

  • async with aiohttp.ClientSession()进入block时不执行I/O,但在结束时会确保所有剩余资源正确关闭。同样,这是异步完成的,必须这样标记。会话也是一种性能工具,因为它为您管理一个连接池,允许您重复使用它们,而不是在每个请求时打开和关闭一个新连接。您甚至可以通过传递连接器对象来管理池大小。

使用会话作为最佳实践

requests 库实际上也提供了一个会话系统。事实上,它可以让你做到:

with requests.Session() as session:
    response = session.get('http://python.org')
    print(response.text)

这不是默认行为,也没有在文档的早期宣传。正因为如此,大多数用户的性能都会受到影响,但可以很快开始黑客攻击。对于请求,这是一个可以理解的权衡,因为它的目标是成为“人类的 HTTP”,而在这种情况下,简单性总是比性能更重要。
但是,如果使用 aiohttp,则选择异步编程,这是一种进行相反权衡的范式:更冗长以获得更好的性能。因此库默认行为反映了这一点,鼓励您从一开始就使用性能最佳实践。

如何使用客户端会话?

默认情况下,该aiohttp.ClientSession对象将拥有一个最多具有 100 个连接的连接器,将其余连接放入队列中。这是一个相当大的数字,这意味着您必须同时连接到一百个不同的服务器(不是页面!),然后才能考虑您的任务是否需要资源调整。

事实上,您可以将会话对象想象为用户启动和关闭浏览器:每次您想要加载新选项卡时都这样做是没有意义的。

因此,您应该重用会话对象并从中发出许多请求。对于大多数脚本和中等大小的软件,这意味着您可以创建一个会话,并在程序的整个执行过程中重复使用它。您甚至可以将会话作为函数中的参数传递。例如,典型的“hello world”:

import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://python.org') as response:
            html = await response.text()
            print(html)

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

可以变成这样:

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'http://python.org')
        print(html)

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

在更复杂的代码库上,您甚至可以创建一个中央注册表来保存来自代码中任何位置的会话对象,或者Client保存对它的引用的更高级别的类。

那么何时创建多个会话对象呢?当您需要更精细的资源管理时,就会出现这种情况:

  • 您想通过通用配置对连接进行分组。例如:会话可以设置它们持有的所有连接共享的 cookie、标头、超时值等。
  • 您需要多个线程并希望避免在它们之间共享可变对象。
  • 您希望多个连接池从不同的队列中受益并分配优先级。eg:一个会话从不使用队列并且用于高优先级请求,另一个会话具有较小的并发限制和很长的队列,用于非重要请求。

优雅关闭

当在块ClientSession结束时 (或通过直接调用)关闭时,由于 asyncio 内部细节,底层连接保持打开状态。在实践中,底层连接将在片刻后关闭。但是,如果事件循环在底层连接关闭之前停止, 则会发出警告(启用警告时)。async withClientSession.close()ResourceWarning: unclosed transport

为了避免这种情况,必须在关闭事件循环之前添加一个小的延迟,以允许任何打开的底层连接关闭。

对于ClientSession没有 SSL 的情况,一个简单的零睡眠 ( ) 就足够了:await asyncio.sleep(0)

async def read_website():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://example.org/') as resp:
            await resp.read()

loop = asyncio.get_event_loop()
loop.run_until_complete(read_website())
# Zero-sleep to allow underlying connections to close
loop.run_until_complete(asyncio.sleep(0))
loop.close()

对于ClientSession使用 SSL,应用程序必须在关闭前等待一小段时间:

...
# Wait 250 ms for the underlying SSL connections to close
loop.run_until_complete(asyncio.sleep(0.250))
loop.close()

请注意,等待的适当时间量因应用程序而异。
如果这最终会在 asyncio 内部发生变化时变得过时,以便 aiohttp 本身可以等待底层连接关闭。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐