前言
在使用 Python 的 aiohttp 库进行异步网络请求时,我们经常会看到如下代码:
python
scss
import aiohttp
import asyncio
async def fetch(url):
session = aiohttp.ClientSession()
response = await session.get(url)
text = await response.text()
print(text)
response.close()
await session.close()
asyncio.run(fetch("https://example.com"))
这个 10 行的代码,乍一看很容易,但是你细品就会发现很多的疑点:
session = aiohttp.ClientSession()
这里实例化的 session 到底是个什么对象?如果是普通对象的话,为何它的.get()
方法的调用需要使用await
?明明源码中是def get
这个普通函数啊?response = session.get()
返回的这个 response 是什么对象?为何response.text()
是协程函数,而response.close()
就变成了同步函数?- 为何
session.close()
又是异步函数??
下面我用大白话来一一解释。
一、ClientSession 是什么?做了什么?
一句话来说 :这句代码创建了一个连接池实例对象 session
(ClientSession
),其实这个连接池对象的底层是(TCPConnector
)。
这个连接池对象相对于其他普通连接池对象而言有以下几个特点:
🔥 核心特性
- 懒加载:池中的连接器对象(socket)是懒加载的(当有连接操作的时候才去生成),且可以复用的
- 异步调度:连接池对象可以异步调度池中的连接器对象,当遇到 IO 操作的时候,进行任务并发,不阻塞
- 异步关闭 :正因为该连接池对象对池中的连接对象的异步调度的特性,使得该连接池对象在关闭的时候也需要是异步操作
await session.close()
🏭 形象比喻
这个连接池对象就像一个工厂 ,里面的连接对象就像一个个工人 。工人手里的活干完了,就可以走了,但是工厂下班关门需要等所有的工人都结束工作,才能结束。所以
session.close()
是个可等待对象,需要使用await
。
二、session.get(url) 背后发生了什么?
response = await session.get(url)
这一行背后发生了以下几件事:
📡 执行流程
- 通过 aiohttp 的连接池(TCPConnector)获取或创建一个 TCP 连接
- 发送 HTTP 请求行、请求头等数据
- 等待服务器响应,接收响应头(Response Header)
- 创建一个 ClientResponse 实例并返回
- 此时你已经拥有了响应对象,但尚未读取响应体(body)
⚠️ 关键理解
这个非常关键!!只是获得了响应对象,而不是响应体!!!
可以将这一步类比为:
📦 挂号信通知送达(header),但真正的信件要凭通知去邮局去取(body)
三、为什么需要 await response.text()?
很多人误以为 response 中的数据已经全部获取完毕,事实上如我们上面讲到的,response 只是拥有了响应对象,但尚未读取响应体(body)。
如果要进一步获取响应体内容和解析数据,需要调用:
python
vbnet
text = await response.text()
🔄 这一步真正做的是:
- 异步读取响应体数据(可能是分块读取)
- 解码字节流为字符串(通常是 utf-8)
✅ 为什么设计为异步?
原因很现实:
- 响应体可能非常大(如视频流、长网页、文件下载)
- 网络传输速度慢,数据并非一次性到达
- aiohttp 采用流式读取机制,读取是分段完成的
因此,await response.text()
代表的是 IO 密集型的数据读取过程,绝不是普通的属性访问。
这一设计体现了 aiohttp 非常细致的异步颗粒度划分。
📦 总结对比
python
ini
# ❌ 错误理解:以为 response 就是完整数据
response = await session.get(url)
print(response) # 这只是对象本身,不是内容
# ✅ 正确理解:需要显式读取响应体
response = await session.get(url)
content = await response.text() # 这才是真正的内容
print(content)
精准总结 :
session.get()
获取的是"包裹壳 "(响应头),text()
才是"拆包取货"(响应体)
四、为什么 response.close() 是同步的?
python
go
response.close()
这是一个常见疑问:既然读取数据需要异步,为何关闭资源是同步?
💡 答案很直接:
response.close()
的本质是关闭 socket 或释放连接资源- 这类操作属于快速执行、无等待的任务,因此 aiohttp 将其设计为同步
- 就像我们上面工厂和工人的例子,response 就类似于工人,他的活干完了,他就下班回家,无需等待
五、最佳实践:使用异步上下文管理器
推荐使用异步上下文管理器来自动管理资源:
python
python
import aiohttp
import asyncio
async def fetch(url):
# ✅ 推荐写法:自动资源管理
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
text = await response.text()
print(text)
# 自动调用 response.close() 和 session.close()
asyncio.run(fetch("https://example.com"))
🎯 为什么推荐这种写法?
- 自动资源清理 :无需手动调用
close()
方法 - 异常安全:即使出现异常也能正确释放资源
- 代码简洁:减少样板代码,专注业务逻辑
六、完整总结
scss
对象/方法 同步/异步 作用 比喻aiohttp.ClientSession() 同步创建 创建连接池管理器 🏭 建立工厂
await session.get() 异步 发送请求,获取响应头 📦 收到包裹通知单
await response.text() 异步 读取响应体数据 📬 凭通知单取包裹
response.close() 同步 释放单个连接资源 👷 工人下班回家
await session.close() 异步 关闭整个连接池 🏭 工厂等所有工人下班后关门
🔑 核心要点
- ClientSession 内部封装异步连接池(TCPConnector),实现连接复用
- await session.get() 只完成请求发送和响应头接收,返回的是一个 ClientResponse 对象,尚未包含完整 body 数据
- await response.text() 是真正读取响应体的过程,由于涉及网络 IO 与大数据量读取,因此是异步的
- response.close() 是同步操作,快速释放资源无需挂起任务
- 推荐使用异步上下文管理器进行资源管理
延伸阅读
如果你想进一步了解 aiohttp 的内部实现、分块读取机制、以及与 asyncio 的调度关系,推荐阅读以下资料:
👍 如果这篇文章对你有帮助,请点个赞支持一下!有任何疑问欢迎在评论区讨论。