http.client 本身是同步的。

它是一个阻塞式的库,当你调用 request(), getresponse() 等方法时,当前线程会一直等待,直到网络请求完成或发生超时/错误,在等待期间,你的程序无法执行其他任何任务。
要实现异步 HTTP 请求,我们不能直接使用 http.client,而是需要将它与异步编程模型(如 asyncio)结合使用,这通常意味着在一个单独的线程中运行同步的 http.client 代码,然后通过 asyncio 的机制将其结果“回调”给主异步事件循环。
下面我将介绍几种实现异步 HTTP 请求的方法,从最基础的(手动封装 http.client)到现代推荐的(使用专门的异步库)。
手动封装 http.client(使用 run_in_executor)
这是最直接的方法,利用 asyncio 的 run_in_executor 将同步的 http.client 调用放到一个线程池中执行,从而避免阻塞事件循环。

工作原理:
- 你的异步函数 (
async def) 在事件循环中运行。 - 当需要进行 HTTP 请求时,调用
await loop.run_in_executor(executor, sync_function, *args)。 asyncio会从线程池中拿出一个空闲线程,在其中执行sync_function。- 原来的异步函数被挂起,事件循环可以去处理其他任务。
- 当
sync_function执行完毕(即 HTTP 请求完成),线程会返回结果,asyncio会唤醒挂起的异步函数,并将结果返回。
示例代码:
import asyncio
import http.client
import concurrent.futures
# 1. 定义同步的 HTTP 请求函数
def fetch_data_sync(url, path):
"""
使用 http.client 发送同步 GET 请求
"""
print(f"[同步] 开始请求: {url}{path}")
try:
# 创建连接 (根据 URL 选择 http 或 https)
if url.startswith("https://"):
conn = http.client.HTTPSConnection(url)
else:
conn = http.client.HTTPConnection(url)
# 发送请求
conn.request("GET", path)
# 获取响应
response = conn.getresponse()
# 读取响应数据
data = response.read()
# 关闭连接
conn.close()
print(f"[同步] 请求成功,状态码: {response.status}")
return data.decode('utf-8')
except Exception as e:
print(f"[同步] 请求失败: {e}")
return None
# 2. 定义异步函数,并使用 run_in_executor
async def fetch_data_async(url, path):
"""
异步获取数据,通过 run_in_executor 调用同步函数
"""
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 创建一个线程执行器 (默认会使用 ThreadPoolExecutor)
# 也可以自定义: executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
# 然后在 run_in_executor 中传入 executor
# 将同步任务提交到线程池执行
# fetch_data_sync 是要执行的函数,url 和 path 是它的参数
data = await loop.run_in_executor(None, fetch_data_sync, url, path)
return data
# 3. 主程序入口
async def main():
# 模拟并发请求
urls_to_fetch = [
("httpbin.org", "/get"),
("httpbin.org", "/delay/2"), # 这个接口会延迟2秒返回
("httpbin.org", "/status/200"),
]
# 使用 asyncio.gather 并发执行多个异步任务
tasks = [fetch_data_async(url, path) for url, path in urls_to_fetch]
results = await asyncio.gather(*tasks)
print("\n--- 所有请求完成 ---")
for i, result in enumerate(results):
print(f"任务 {i+1} 的结果长度: {len(result) if result else 'N/A'}")
if __name__ == "__main__":
asyncio.run(main())
优点:
- 可以直接复用现有的
http.client代码。 - 原理清晰,是理解异步与同步结合的绝佳例子。
缺点:
- 线程开销:每个并发请求都会占用一个线程,在需要极高并发的场景下(如数千个请求),线程可能会成为瓶颈。
- 代码冗余:需要为每个同步的 HTTP 客户端代码写一个异步的包装器,比较繁琐。
- 连接管理:手动管理连接的创建和关闭,容易出错,理想情况下应该使用连接池。
使用现代异步 HTTP 客户端库(推荐)
对于异步编程,社区已经开发出了非常成熟且高效的异步 HTTP 客户端,它们原生支持 asyncio,性能远超手动封装的同步库。
最流行的两个库是:
httpx: 现代的选择,功能强大,API 友好,支持 HTTP/1.1 和 HTTP/2。aiohttp: 老牌的异步 HTTP 客户端/服务器库,在asyncio生态中非常流行。
推荐方案:使用 httpx
httpx 的 API 设计非常现代化,与流行的 requests 库非常相似,学习成本低。
安装:
pip install httpx
示例代码:
import asyncio
import httpx
async def fetch_data_with_httpx(url, path):
"""
使用 httpx 进行异步 GET 请求
"""
print(f"[httpx] 开始请求: {url}{path}")
try:
# httpx.AsyncClient 是一个异步上下文管理器,推荐使用
# 它会自动管理连接池,性能更好
async with httpx.AsyncClient() as client:
response = await client.get(f"{url}{path}")
# response.raise_for_status() # 如果状态码不是 2xx,则抛出异常
print(f"[httpx] 请求成功,状态码: {response.status_code}")
return response.text
except httpx.RequestError as e:
# 捕获 httpx 的请求错误
print(f"[httpx] 请求失败: {e}")
return None
async def main():
urls_to_fetch = [
"http://httpbin.org/get",
"http://httpbin.org/delay/2",
"http://httpbin.org/status/200",
]
# 使用 asyncio.gather 并发执行
tasks = [fetch_data_with_httpx(url) for url in urls_to_fetch]
results = await asyncio.gather(*tasks)
print("\n--- 所有请求完成 ---")
for i, result in enumerate(results):
print(f"任务 {i+1} 的结果长度: {len(result) if result else 'N/A'}")
if __name__ == "__main__":
asyncio.run(main())
优点:
- 原生异步:代码简洁、高效,没有线程开销。
- 连接池:
httpx.AsyncClient内置了连接池,复用 TCP 连接,性能极佳。 - 现代特性:支持 HTTP/2、WebSocket 等。
- 优秀的 API:与
requests类似,非常易用。
备选方案:使用 aiohttp
aiohttp 是另一个非常强大的选择,尤其在构建异步 Web 服务时非常流行。
安装:
pip install aiohttp
示例代码:
import asyncio
import aiohttp
async def fetch_data_with_aiohttp(session, url):
"""
使用 aiohttp 进行异步 GET 请求
注意:session 需要在外部创建和管理
"""
print(f"[aiohttp] 开始请求: {url}")
try:
async with session.get(url) as response:
response.raise_for_status() # 检查响应状态
text = await response.text()
print(f"[aiohttp] 请求成功,状态码: {response.status}")
return text
except aiohttp.ClientError as e:
print(f"[aiohttp] 请求失败: {e}")
return None
async def main():
urls_to_fetch = [
"http://httpbin.org/get",
"http://httpbin.org/delay/2",
"http://httpbin.org/status/200",
]
# aiohttp 推荐在外部创建一个 session 并在整个应用中复用
# session 本身就是一个连接池
async with aiohttp.ClientSession() as session:
tasks = [fetch_data_with_aiohttp(session, url) for url in urls_to_fetch]
results = await asyncio.gather(*tasks)
print("\n--- 所有请求完成 ---")
for i, result in enumerate(results):
print(f"任务 {i+1} 的结果长度: {len(result) if result else 'N/A'}")
if __name__ == "__main__":
asyncio.run(main())
总结与对比
| 特性 | http.client + run_in_executor |
httpx (推荐) |
aiohttp (备选) |
|---|---|---|---|
| 类型 | 同步库,需手动封装 | 原生异步 | 原生异步 |
| 易用性 | 低,需要写包装器 | 高,API 类似 requests |
中等,API 略有不同 |
| 性能 | 较低,受线程池限制 | 高,基于 asyncio |
高,基于 asyncio |
| 连接管理 | 手动,易出错 | 自动 (连接池) | 自动 (连接池) |
| 现代特性 | 无 | 支持 HTTP/2, WebSocket | 支持 WebSocket |
| 适用场景 | - 现有代码库无法替换,且需要快速引入异步。 - 学习原理。 |
- 新项目首选。 - 需要 HTTP/2 或现代 API 的项目。 |
- 已在使用 aiohttp 生态的项目。- 构建异步 Web 服务。 |
最终建议
对于任何新的 Python 异步项目,请直接使用 httpx。
它提供了最佳的开发体验、性能和现代化特性,你几乎不需要再考虑直接使用 http.client 来进行异步编程,除非你有非常特殊的需求(在一个极其受限的环境中,无法安装第三方库,并且并发量很低)。
http.client 的主要价值在于它是 Python 标准库的一部分,作为理解 HTTP 协议和底层网络通信的基础,在生产级的异步应用中,httpx 和 aiohttp 是无可争议的更优选择。
