12 · 网络编程 IO 模型
为什么 Node 单线程能抗高并发,而阻塞式服务会卡死?答案在 IO 模型。本篇讲阻塞/非阻塞、IO 多路复用与异步,给前后端一个理解"高性能服务网络底座"的直觉。更深层的操作系统视角见 IO 计算机基础。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 服务端怎么用有限的线程扛海量连接 |
| 核心内容 | 阻塞/非阻塞、select/epoll、Reactor、异步 |
| 读完能回答 | Node 单线程为什么能扛住?高并发服务的底座是什么? |
| 相关板块 | N02 TCP | N07 WebSocket | C07 性能与容量 |
一、为什么需要 IO 模型
text
一万个连接 = 一万个"在等数据"的任务
如果每个连接占一个线程 → 线程爆炸
模型的目标:用少量线程管大量连接二、阻塞 vs 非阻塞
| 模型 | 行为 | 代价 |
|---|---|---|
| 阻塞 | 数据没到就卡住线程 | 连接多线程不够用 |
| 非阻塞 | 没数据立刻返回"还没好" | 需要反复问,CPU 空转 |
心智模型:阻塞像点餐后干等,非阻塞像不断去柜台问"好了吗"。前者浪费线程,后者浪费 CPU。
三、IO 多路复用:一次盯一群连接
text
select/poll:把一堆连接交给内核,等有事件再处理(但每次全量扫描)
epoll:内核帮你记住关心的连接,只返回就绪的(Linux 高性能标配)| 机制 | 特点 |
|---|---|
| select | 有连接数上限,每次全量查 |
| poll | 去掉上限,仍全量查 |
| epoll | 事件驱动,只通知就绪项 |
四、Reactor:多路复用的工程封装
text
事件循环(Reactor)收到就绪事件 → 分发给处理器
Node / Netty / Python asyncio 都是这个套路核心思想:永远不要在一个线程里"干等"某个连接,而是"谁好了处理谁"。这就是事件循环的心智模型。
五、同步 vs 异步:差别在"谁等着"
| 维度 | 同步阻塞 | 异步事件驱动 |
|---|---|---|
| 线程等待 | 是 | 否 |
| 高并发能力 | 差 | 好 |
| 代码复杂度 | 直观 | 需回调/协程管理 |
| 代表 | 传统多线程 | Node、Netty、asyncio |
六、与前端的关系
| 前端概念 | 对应 |
|---|---|
| 事件循环 | 就是 Reactor 的 JS 版 |
await / Promise | 把异步写得更像同步 |
| 大量并发请求 | 浏览器连接池 + 服务端多路复用 |
你写
await fetch(...)时,底层正是"非阻塞 + 事件循环"在为你服务。
七、自检清单
- [ ] 能说清为什么不能"每连接一线程"
- [ ] 能区分阻塞/非阻塞、同步/异步
- [ ] 能解释 epoll 比 select 强在哪
- [ ] 能画出 Reactor 事件循环的分发流程
- [ ] 知道 Node / asyncio / Netty 用的都是同一套心智
八、常见误区
| 误区 | 事实 |
|---|---|
| "异步 = 更快" | 是更省线程,单请求未必更快 |
| "事件循环会并行执行" | 单线程内仍是轮流,并行靠多进程/多线程 |
| "高并发只要框架选对" | 底层是 IO 模型 + 连接管理 + 容量设计 |
九、动手练习
练习 A:阻塞 vs 非阻塞对比
用 Python 分别写阻塞 socket 与 asyncio 版本,压 100 个并发连接,观察耗时与线程占用。
练习 B:看 Node 事件循环
写一个慢 IO 模拟,观察它在等待时不阻塞其他请求,理解单线程真相。
练习 C:读源码注释
读 asyncio 或 libuv 的 README 关于 event loop 的描述,与 Reactor 图对照。
相关文档
本区
- 02 · TCP 握手/挥手与拥塞控制 —— 连接生命周期
- 07 · WebSocket 与长连接 —— 海量长连接的管理
其他域
- 服务端性能与容量:连接池/线程模型 —— 生产容量设计
- 计算机基础面试题 —— select/epoll、Reactor 深度