Skip to content

Python aiohttp:用单线程撑起万人访问的 Web 服务器

引言:把"服务员巡台"用到极致

上一篇我们学了 asyncio:一个线程通过事件循环并发处理多个 IO 任务,就像一个服务员照顾 10 桌客人

但你可能注意到了:上一篇的例子是客户端——我们的程序主动去抓三个网站。这相当于服务员只给自家人上菜,威力有限。

如果把 asyncio 用在服务器端呢?

想象一下:你的服务员不再服务自家,而是开了一家餐厅,门口排了几千个客人。传统做法是每来一桌客人就雇一个新服务员(多线程),餐厅很快人满为患、工资都发不起。而 asyncio 的做法是:一个服务员加上"巡台小本子"(事件循环),同时照顾所有客人——客人看菜单时他去服务别人,厨房上菜慢时他绝不空等。一个人撑起整个餐厅。

aiohttp 就是基于 asyncio 实现的 HTTP 框架,让你能用单线程写出支撑高并发的 Web 服务器。这一篇,我们从"为什么服务器端更需要异步"讲起,手把手搭一个 aiohttp Web 服务器,补充客户端用法,最后通过常见误区解析帮你避开生产环境的坑。


一、为什么服务器端才是 asyncio 的主战场?

1.1 客户端 vs 服务器端

场景客户端(爬虫)服务器端(Web 服务)
并发量自己控制,几十几百个任务用户决定,可能瞬间几千个请求
持续时间抓完就结束7×24 小时不间断
资源消耗无所谓,跑完就释放线程/进程多了服务器直接崩溃
异步收益快一点生死攸关

生活化理解:客户端爬虫像周末去逛街,多等一会儿没关系;服务器像春运火车站售票窗口,每个窗口(线程)慢一秒,后面就积压几千人。

1.2 HTTP 连接就是典型的 IO 操作

Web 服务器处理请求时,绝大部分时间在等待

  • 等待客户端把请求数据发过来(网络 IO);
  • 等待数据库返回查询结果(数据库 IO);
  • 等待调用第三方接口(外部 API IO);
  • 等待把响应发回客户端(网络 IO)。

真正 CPU 计算的时间不到 1%。这意味着:单线程 + 事件循环能把等待时间全部利用起来,一个线程轻松扛住几千并发连接

1.3 aiohttp 是什么?

asyncio 只实现了底层的 TCP、UDP、SSL 协议,相当于给了你"锅碗瓢盆";而 aiohttp 在这些基础上封装好了 HTTP 协议,相当于给你一套现成的厨房——你只管做菜(写业务逻辑),不用自己砌灶台。

aiohttp 同时提供:

  • Web 服务器框架(类似 Flask,但支持异步);
  • HTTP 客户端(类似 requests,但支持异步)。

二、动手搭建:第一个 aiohttp Web 服务器

2.1 安装

bash
pip install aiohttp

2.2 需求

写一个 HTTP 服务器,处理两种 URL:

  • / —— 首页,返回 Index Page
  • /{name} —— 根据 URL 里的名字,返回 Hello, {name}!

2.3 完整代码

python
# app.py
from aiohttp import web

async def index(request):
    """处理首页 / 的请求"""
    text = "<h1>Index Page</h1>"
    return web.Response(text=text, content_type="text/html")

async def hello(request):
    """处理 /{name} 的请求,从 URL 中提取 name 参数"""
    name = request.match_info.get("name", "World")
    text = f"<h1>Hello, {name}</h1>"
    return web.Response(text=text, content_type="text/html")

# 创建应用
app = web.Application()

# 添加路由:URL 模式 → 处理函数
app.add_routes([
    web.get("/", index),
    web.get("/{name}", hello)
])

if __name__ == "__main__":
    # 启动服务器(默认监听 8080 端口)
    web.run_app(app)

2.4 运行与测试

直接运行:

bash
python app.py

打开浏览器访问:

  • http://localhost:8080/ → 页面显示大标题 Index Page
  • http://localhost:8080/Bob → 页面显示 Hello, Bob
  • http://localhost:8080/Alice → 页面显示 Hello, Alice

2.5 代码逐行拆解

处理函数:async def index(request)

  • 必须是 async 函数——这是 aiohttp 与 Flask 最大的不同,异步处理才能发挥并发威力;
  • request 参数封装了 HTTP 请求的全部信息(URL、参数、请求头、body 等);
  • 返回 web.Response 对象,content_type="text/html" 告诉浏览器"这是 HTML 页面,按网页渲染"。

URL 参数提取:request.match_info.get("name", "World")

  • 路由 /​{name} 中的 {name} 是占位符,实际访问 /Bob 时,match_info 里就有 {"name": "Bob"}
  • .get("name", "World") 表示取不到时默认返回 "World"

路由注册:app.add_routes([web.get("/", index), ...])

  • web.get() 表示只接受 GET 请求(还有 web.post()web.put() 等对应其他 HTTP 方法);
  • 把 URL 模式和处理函数绑定,类似餐厅的"菜单 → 厨师"对照表。

启动服务器:web.run_app(app)

  • 内部自动创建事件循环,监听 8080 端口;
  • 每来一个 HTTP 请求,事件循环就把对应的处理函数包装成协程任务调度执行;
  • 全程单线程,但成百上千的请求可以并发处理

2.6 生活化理解

整个流程像餐厅接单:

  1. 路由表是菜单——客人点什么菜(访问什么 URL),就找对应的厨师(处理函数);
  2. 处理函数是 async——厨师做菜时如果需要等烤箱(IO 操作),不会干站着,而是先去炒别桌的菜;
  3. run_app() 是开店营业——门口挂出招牌(监听端口),客人络绎不绝,一个后厨团队(事件循环)高效运转。

三、进阶:aiohttp 作为异步 HTTP 客户端

aiohttp 不只是服务器,也是强大的异步 HTTP 客户端,可以替代 requests 用于高并发爬虫。

3.1 对比:requests vs aiohttp 客户端

用 requests 抓 3 个网站(串行):

python
import requests

for url in ["https://a.com", "https://b.com", "https://c.com"]:
    r = requests.get(url)   # 每个都要等前一个完成
    print(r.status_code)

用 aiohttp 抓 3 个网站(并发):

python
import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as response:
        print(url, response.status)

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in
                 ["https://a.com", "https://b.com", "https://c.com"]]
        await asyncio.gather(*tasks)   # 三个请求同时发出

asyncio.run(main())

关键点:ClientSession 类似浏览器的一个"会话",可以在其中并发发起多个请求,共享连接池和 Cookie。

3.2 典型应用场景

  • 高并发爬虫:同时抓几百个页面,速度比 requests 快几十倍;
  • 微服务内部调用:服务 A 同时调用服务 B、C、D 的接口,聚合结果后返回;
  • 批量数据同步:并发地把本地数据推送到多个远程系统。

四、aiohttp vs Flask:该选哪个?

对比项Flask(同步)aiohttp(异步)
并发模型每个请求一个线程(或进程)单线程 + 协程并发
高并发能力弱,几百并发就吃力强,单机可扛数千并发
学习曲线平缓,新手友好需要先理解 asyncio
生态库丰富(requests、SQLAlchemy 等)需选异步版本(aiohttp、aiomysql)
适用场景中小型项目、内部工具、快速原型高并发 API、实时服务、微服务网关

简单决策

  • 项目并发量小(内部系统、日活几百)、团队不熟异步 → Flask 省心;
  • 项目并发量大(对外 API、秒杀活动)、需要长连接(WebSocket)→ aiohttp 或其他异步框架(FastAPI、Sanic)。

五、常见误区解析

误区一:aiohttp 处理函数里可以用 requests

严重错误。这是最致命的坑:

python
async def bad_handler(request):
    r = requests.get("https://api.example.com")  # 同步调用!
    return web.Response(text=r.text)

requests.get() 是同步阻塞的,执行它时整个事件循环被卡死,服务器瞬间从"高并发"变成"单线程串行",所有用户的请求都排队等这一个请求完成。

正确做法:协程里必须用异步客户端——aiohttp.ClientSessionhttpx(异步模式)。

误区二:处理函数里用 time.sleep() 模拟耗时操作

错误,原因同上——time.sleep() 会睡死整个线程:

python
async def bad(request):
    time.sleep(5)           # 全服务器卡 5 秒

async def good(request):
    await asyncio.sleep(5)  # 只有当前请求等 5 秒,其他请求正常处理

误区三:数据库操作用普通驱动(如 PyMySQL)

错误。数据库查询是典型 IO 操作,用同步驱动会阻塞事件循环。必须选异步驱动:

数据库同步驱动异步驱动
MySQLPyMySQLaiomysql
Redisredis-pyaioredis
MongoDBpymongomotor

误区四:async 函数能提高 CPU 计算速度

错误。async 解决的是 IO 等待问题,不是计算速度问题。如果处理函数里全是复杂计算(比如图像处理),async 毫无帮助,反而因为单线程而更慢。CPU 密集任务应该用多进程,或把计算扔到线程池:

python
# 把同步 CPU 密集任务扔进线程池,避免阻塞事件循环
result = await loop.run_in_executor(None, cpu_intensive_func, args)

误区五:aiohttp 的并发数没有上限

过度乐观。单线程协程并发也有极限:每个连接都占内存和文件描述符,几万个连接就可能耗尽资源。生产环境通常需要:

  • 多进程 + asyncio:开多个进程,每个进程跑一个事件循环(利用多核);
  • 前置 Nginx:做负载均衡和静态资源分发;
  • 限制并发数:用 asyncio.Semaphore 控制同时执行的任务数量,防止压垮下游服务。

误区六:只要用了 aiohttp 就自动高并发

错误。高并发的前提是全链路异步:处理函数是 async、HTTP 调用用 aiohttp、数据库用异步驱动……任何一个环节混入同步调用,整个链路就退回串行。这就像高速公路上只要有一辆车抛锚,所有车都得堵着。


六、小结

  • aiohttp 是基于 asyncio 的 HTTP 框架,把"单线程高并发"从客户端带到了服务器端——这正是异步 IO 价值最大的战场;
  • 写 aiohttp 服务器的三步:定义 async 处理函数 → app.add_routes() 注册路由 → web.run_app() 启动
  • URL 参数通过 {name} 占位符和 request.match_info 提取,响应通过 web.Response 构造;
  • aiohttp 也可作异步 HTTP 客户端,配合 asyncio.gather() 实现高并发爬虫;
  • 铁律:协程内禁止一切同步阻塞调用(requests、time.sleep、同步数据库驱动),否则事件循环卡死,高并发变单线程;
  • 选型建议:小项目用 Flask,高并发用 aiohttp/FastAPI;CPU 密集任务交给多进程,别指望 async 提速;
  • 生产部署记得:多进程利用多核 + Nginx 负载均衡 + Semaphore 限制并发,才能稳稳撑起万人访问。

至此,我们从协程原理(Basic52)到 asyncio 调度(Basic53)再到 aiohttp 实战(本篇),完成了 Python 异步编程的完整链路。接下来可以尝试用 aiohttp + aiomysql 写一个真正的异步 API 服务,把知识变成生产力。


参考文档:廖雪峰的 Python 教程 - 使用 aiohttp