Computer NetworkNotes

第 24 章:高性能网络编程

zjc 于 2026-01-24 发布

这是《计算机网络 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 高性能网络服务的关键不是把参数调到最大,而是让 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:耗时业务

设计原则:

  1. IO 线程不做慢业务;
  2. 耗时任务投递业务线程池;
  3. 避免锁竞争热点;
  4. 每连接状态独立;
  5. 写出必须处理 EAGAIN;
  6. 有界队列和背压;
  7. 优雅关闭。

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);

收益:

  1. 减少内核与用户态复制;
  2. 降低 CPU;
  3. 减少内存分配;
  4. 提升大文件吞吐。

24.7 序列化与协议

协议 特点
JSON 可读,体积较大
Protobuf 二进制,跨语言,高效
FlatBuffers 支持零拷贝访问
自定义 TCP 协议 灵活,需要处理粘包

长度前缀协议:

| Magic | Version | Type | Length | Payload |

必须限制:

  1. 最大帧长度;
  2. 最大头部长度;
  3. 字段长度;
  4. 嵌套深度;
  5. 解压后大小。

24.8 背压与过载保护

请求链路:

网络事件
  -> IO 线程
     -> 有界任务队列
        -> 业务线程池
           -> 下游连接池

过载策略:

策略 语义
Reject 快速失败
Drop 丢弃可丢消息
Latest 只保留最新值
Shed 按用户或优先级丢弃
Degrade 降级非核心逻辑
Throttle 限流

没有背压时,入口速度超过处理速度会导致内存上涨、GC 停顿、超时和雪崩。

24.9 单机容量评估

指标:

  1. QPS;
  2. 并发连接;
  3. 每秒新建连接;
  4. 请求体大小;
  5. 响应体大小;
  6. P99 处理耗时;
  7. CPU;
  8. 内存;
  9. 网卡带宽;
  10. 后端依赖延迟。

简化计算:

并发 = QPS × 平均处理时间
线程数 ≈ CPU 核数 × (1 + 等待时间 / 计算时间)

示例:

QPS 10000
平均处理 20ms
并发请求数 ≈ 10000 × 0.02 = 200

如果实际连接远高于该值,可能存在长连接、慢消费者或空闲连接。

24.10 调优清单

[ ] 监听 backlog 足够
[ ] accept 和 IO 不做耗时业务
[ ] 使用有界队列
[ ] 每连接发送缓冲有限制
[ ] 慢消费者被断开或降级
[ ] 解析器限制消息大小
[ ] TCP_NODELAY 按低延迟场景开启
[ ] 连接池上限和借出超时明确
[ ] 优雅启动与关闭
[ ] 监控 fd、线程、队列、GC、重传

本章小结

高性能网络编程通过非阻塞 IO、epoll、Reactor、线程池、零拷贝和高效序列化提升单机能力。核心工程约束是背压:每一段队列、线程池、连接池和发送缓冲都必须有上限。容量评估要同时看 QPS、并发、请求大小、CPU、内存、带宽和下游延迟。

思考题

  1. 阻塞 IO 为什么难以支撑大量并发连接?
  2. 主从 Reactor 如何分离 accept 和 IO?
  3. 边缘触发为什么必须读到 EAGAIN?
  4. sendfile 减少了哪些复制?
  5. 设计一个网关的线程池、队列和连接池容量方案。