这是《计算机网络 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 高性能网络服务的关键不是把参数调到最大,而是让 IO、线程、内存、协议和背压协同工作。单机从 C10K 到 C100K、C1M,需要理解 Socket、IO 多路复用、Reactor 模型、零拷贝、连接池、池化和队列治理。
24.1 Socket 编程基础
TCP 服务端流程:
socket
-> bind
-> listen
-> accept
-> read / write
-> close
Python 示例:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 9000))
server.listen(1024)
while True:
conn, addr = server.accept()
with conn:
data = conn.recv(4096)
conn.sendall(b"ack\n")
该示例一次只能处理一个连接。并发模型需要多进程、多线程或 IO 多路复用。
24.2 阻塞与非阻塞
| 模式 | 读写行为 | 适合 |
|---|---|---|
| 阻塞 | 没数据时等待 | 简单、低并发 |
| 非阻塞 | 没数据立即返回 EAGAIN | 事件驱动 |
非阻塞必须搭配事件循环:
while True:
events = wait(readable_or_writable_fds)
handle(events)
否则会忙轮询浪费 CPU。
24.3 IO 多路复用
| 机制 | 特点 |
|---|---|
| select | fd 数量有限,线性扫描 |
| poll | 无 1024 限制,仍线性扫描 |
| epoll | Linux,事件通知,适合大量连接 |
| kqueue | BSD / macOS |
| IOCP / IO_URING | 不同平台的异步 IO 机制 |
epoll 示例:
int epfd = epoll_create1(0);
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event);
while (1) {
struct epoll_event events[1024];
int n = epoll_wait(epfd, events, 1024, -1);
for (int i = 0; i < n; i++) {
handle_event(events[i]);
}
}
边缘触发必须读到 EAGAIN,水平触发可以在每次可读时读一次。
24.4 Reactor 模型
单 Reactor:
Event Loop
-> accept
-> read
-> decode
-> business
-> encode
-> write
主从 Reactor:
Main Reactor:accept
-> 分发连接
Sub Reactor 1:IO 事件
Sub Reactor 2:IO 事件
Worker Pool:耗时业务
设计原则:
- IO 线程不做慢业务;
- 耗时任务投递业务线程池;
- 避免锁竞争热点;
- 每连接状态独立;
- 写出必须处理 EAGAIN;
- 有界队列和背压;
- 优雅关闭。
24.5 Netty 示例
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup workers = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, workers)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LineBasedFrameDecoder(4096));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture future = bootstrap.bind(9000).sync();
future.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
workers.shutdownGracefully();
}
EchoServerHandler:
public class EchoServerHandler
extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(
ChannelHandlerContext ctx,
String message) {
ctx.writeAndFlush("ack:" + message + "\n");
}
}
24.6 内存与零拷贝
传统文件发送:
磁盘 -> 内核页缓存 -> 用户缓冲 -> Socket 缓冲 -> 网卡
零拷贝:
sendfile
磁盘 -> 内核页缓存 -> 网卡
Java 示例:
FileChannel fileChannel = FileChannel.open(path);
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
收益:
- 减少内核与用户态复制;
- 降低 CPU;
- 减少内存分配;
- 提升大文件吞吐。
24.7 序列化与协议
| 协议 | 特点 |
|---|---|
| JSON | 可读,体积较大 |
| Protobuf | 二进制,跨语言,高效 |
| FlatBuffers | 支持零拷贝访问 |
| 自定义 TCP 协议 | 灵活,需要处理粘包 |
长度前缀协议:
| Magic | Version | Type | Length | Payload |
必须限制:
- 最大帧长度;
- 最大头部长度;
- 字段长度;
- 嵌套深度;
- 解压后大小。
24.8 背压与过载保护
请求链路:
网络事件
-> IO 线程
-> 有界任务队列
-> 业务线程池
-> 下游连接池
过载策略:
| 策略 | 语义 |
|---|---|
| Reject | 快速失败 |
| Drop | 丢弃可丢消息 |
| Latest | 只保留最新值 |
| Shed | 按用户或优先级丢弃 |
| Degrade | 降级非核心逻辑 |
| Throttle | 限流 |
没有背压时,入口速度超过处理速度会导致内存上涨、GC 停顿、超时和雪崩。
24.9 单机容量评估
指标:
- QPS;
- 并发连接;
- 每秒新建连接;
- 请求体大小;
- 响应体大小;
- P99 处理耗时;
- CPU;
- 内存;
- 网卡带宽;
- 后端依赖延迟。
简化计算:
并发 = QPS × 平均处理时间
线程数 ≈ CPU 核数 × (1 + 等待时间 / 计算时间)
示例:
QPS 10000
平均处理 20ms
并发请求数 ≈ 10000 × 0.02 = 200
如果实际连接远高于该值,可能存在长连接、慢消费者或空闲连接。
24.10 调优清单
[ ] 监听 backlog 足够
[ ] accept 和 IO 不做耗时业务
[ ] 使用有界队列
[ ] 每连接发送缓冲有限制
[ ] 慢消费者被断开或降级
[ ] 解析器限制消息大小
[ ] TCP_NODELAY 按低延迟场景开启
[ ] 连接池上限和借出超时明确
[ ] 优雅启动与关闭
[ ] 监控 fd、线程、队列、GC、重传
本章小结
高性能网络编程通过非阻塞 IO、epoll、Reactor、线程池、零拷贝和高效序列化提升单机能力。核心工程约束是背压:每一段队列、线程池、连接池和发送缓冲都必须有上限。容量评估要同时看 QPS、并发、请求大小、CPU、内存、带宽和下游延迟。
思考题
- 阻塞 IO 为什么难以支撑大量并发连接?
- 主从 Reactor 如何分离 accept 和 IO?
- 边缘触发为什么必须读到 EAGAIN?
- sendfile 减少了哪些复制?
- 设计一个网关的线程池、队列和连接池容量方案。