这是《计算机网络 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Socket 是应用访问网络的编程接口,IO 模型决定了应用如何等待和搬运数据。阻塞 IO 简单但并发成本高,IO 多路复用适合大量连接,异步 IO 进一步减少等待和复制开销。
28.1 Socket API
TCP 客户端:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(3)
client.connect(("10.20.1.10", 9000))
client.sendall(b"hello\n")
print(client.recv(4096))
client.close()
UDP:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2)
sock.sendto(b"hello", ("10.20.1.10", 9000))
print(sock.recvfrom(4096))
常用选项:
| 选项 | 作用 |
|---|---|
| SO_REUSEADDR | 重启时快速绑定地址 |
| SO_KEEPALIVE | TCP 保活 |
| TCP_NODELAY | 关闭 Nagle |
| SO_SNDBUF / SO_RCVBUF | 缓冲区 |
| SO_LINGER | close 行为 |
| O_NONBLOCK | 非阻塞 |
28.2 阻塞 IO
read()
-> 没有数据就等待
-> 有数据复制到用户态并返回
适合:
- 连接数少;
- 逻辑简单;
- 内部低并发任务;
- 每连接一个线程成本可接受。
问题:
- 连接数增加,线程增多;
- 上下文切换增加;
- 慢连接占用线程;
- 阻塞点难治理;
- 故障时线程池耗尽。
28.3 非阻塞 IO
read()
-> 没有数据返回 EAGAIN
-> 应用稍后再试或注册事件
非阻塞解决“等待”问题,但仍需事件通知机制知道什么时候读。
28.4 IO 多路复用
| 模型 | 特点 |
|---|---|
| select | 跨平台,fd 数量和效率受限 |
| poll | 无 select 数量限制,仍线性扫描 |
| epoll | Linux 事件通知,适合大量空闲连接 |
| kqueue | BSD/macOS 事件通知 |
适合:
- 大量连接;
- 大部分连接不完全活跃;
- 单机网关、推送、代理;
- 长连接服务。
注意:
- 惊群问题;
- 事件循环阻塞;
- 边缘触发漏读;
- 写事件需要动态注册;
- 仍需业务线程池隔离。
28.5 信号驱动与异步 IO
信号驱动 IO:
内核准备好人通知应用
应用自己调用 read 复制数据
异步 IO:
应用提交请求
内核完成等待和复制
通过回调或完成队列通知
Linux 的 io_uring 提供高效异步 IO,适合高 PPS、文件与网络混合 IO 的场景,但编程复杂度更高。
28.6 Java NIO 与 Netty
Java NIO:
Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_CONNECT
| SelectionKey.OP_READ | SelectionKey.OP_WRITE);
Netty 在 NIO 之上封装了:
- EventLoopGroup;
- ChannelPipeline;
- ByteBuf;
- 编解码器;
- 空闲检测;
- 背压;
- 内存池;
- 优雅关闭。
业务代码不应把耗时操作放在 EventLoop 中,否则会阻塞该 EventLoop 上所有 Channel。
28.7 缓冲与消息边界
读取到 ByteBuf:
readableBytes
readerIndex
writerIndex
capacity
自定义协议:
| Length | Payload |
解码时必须处理:
- 半包;
- 粘包;
- 帧超长;
- 非法长度;
- 拆包攻击;
- 解压后大小。
28.8 IO 与业务隔离
EventLoop: accept / read / write / codec
Business Pool: 数据库、RPC、计算
External Pool: 第三方调用
好处:
- 慢业务不影响 IO;
- 线程池可独立观测;
- 不同依赖可独立隔离;
- 故障不扩散;
- 容量更可预测。
每个池必须有:
- 最大线程;
- 有界队列;
- 拒绝策略;
- 执行耗时;
- 队列长度;
- 活跃数;
- 失败数。
28.9 模型选择
| 场景 | 推荐模型 |
|---|---|
| 管理工具、低并发 | 阻塞 IO + 线程池 |
| Web 服务 | 框架异步或线程池 |
| 网关 / 代理 | epoll + Reactor |
| 长连接推送 | epoll + 业务池 |
| 高性能存储 | io_uring / SPDK 等专用方案 |
| 简单内网调用 | 同步 RPC |
先满足正确性和可观测性,再引入复杂模型。
本章小结
Socket 提供网络编程接口,阻塞 IO 简单但并发扩展性差,非阻塞 IO 需要事件机制,epoll 和 Reactor 支撑大量连接,io_uring 提供异步 IO 能力。生产代码要处理消息边界、读写事件、慢业务隔离、有界队列和背压。模型选择应基于连接数、请求耗时和团队维护能力。
思考题
- 阻塞 IO 的主要扩展瓶颈是什么?
- select、poll 和 epoll 的差异是什么?
- 非阻塞 read 返回 EAGAIN 后应用应该做什么?
- 为什么耗时业务不能放在 EventLoop?
- 设计一个自定义 TCP 协议的解码器状态机。