前言

网络服务的性能和稳定性不仅取决于协议,也取决于 I/O 模型、事件循环、缓冲管理和连接治理。本文区分 TCP、HTTP、BIO/NIO 与 Netty 的职责,进一步说明粘包拆包、零拷贝、背压和连接异常等生产问题的处理思路。

先区分网络协议、IO 模型和框架

TCP 解决可靠字节流传输,HTTP 定义应用层请求响应语义,BIO/NIO 描述 Java 的 IO 编程模型,Netty 是基于 NIO 的网络应用框架。面试中不要把“非阻塞”误说成“没有线程”或“没有等待”。

层次 关注点
TCP 连接、可靠传输、流控、拥塞控制、字节流无消息边界
HTTP 方法、Header、状态码、Body、Keep-Alive/HTTP2 等语义
Java IO 阻塞/非阻塞读写、内核事件通知、缓冲区与通道
Netty EventLoop、Pipeline、编解码、连接管理、背压和业务线程隔离

BIO、NIO 与 Selector

BIO 的 accept()read() 通常阻塞线程。传统“一连接一线程”在连接数上升时会带来大量线程、栈内存和上下文切换;它适合连接数小、代码简单的内部服务。

NIO 的核心对象是 BufferChannelSelector。Channel 可配置非阻塞,一个 Selector 可以注册多个 Channel,单个 EventLoop 通过 select() 获取就绪的连接事件,再分发读取和写入逻辑。它解决的是“少量线程管理大量空闲连接”,不是让 CPU 密集计算更快。

flowchart LR
    A[多个 SocketChannel] --> B[Selector]
    B --> C[EventLoop]
    C --> D[Read/Decode]
    D --> E[业务任务]
    E --> F[Encode/Write]
概念 说明 常见坑
Buffer positionlimitcapacity 描述读写位置 写入后 flip() 再读,读完 clear()/compact();忘记切换会读到空数据
Channel 数据读写通道,如 SocketChannel 非阻塞 read 返回 0 不表示连接关闭,返回 -1 才是 EOF
Selector 监听注册 Channel 的就绪事件 只处理 selectedKeys 后要移除,避免重复处理
OP_ACCEPT/READ/WRITE 接受连接、可读、可写事件 不要长期注册 OP_WRITE,socket 缓冲区通常一直可写,会造成空转

TCP 字节流、粘包拆包与协议设计

TCP 只保证字节顺序,不保留发送端的消息边界。一次 write 可能被拆为多次 read,多次 write 也可能合并为一次 read。因此业务协议必须自己定义边界,不能假设“一次 read 就是一条完整 JSON”。

常用方案是定长头部加长度字段:

1
2
3
+--------+--------+---------+------------+---------+
| magic | version| length | requestId | payload |
+--------+--------+---------+------------+---------+

接收端先累计头部字节,再验证 magic、版本和 length 上限,只有拿到完整 payload 才交给反序列化器。长度字段必须设置最大值,防止恶意客户端声明数 GB Body 导致内存分配攻击。HTTP、WebSocket、Protobuf 等成熟协议已经定义了边界和编解码,不要为普通内部 HTTP 服务手写 TCP 协议。

零拷贝是什么

“零拷贝”不是数据完全不移动,而是减少用户态/内核态之间的额外复制和上下文切换。文件下载中 FileChannel.transferTo() 可利用 sendfile 等内核能力,让文件页缓存数据更直接地发送到 socket;DMA 由设备与内存传输数据,减少 CPU 参与。

它适合大文件、日志或静态资源转发。若还需要加密、压缩、内容改写或复杂业务解析,数据仍可能进入用户态。生产优化前先确认瓶颈是 CPU copy、磁盘、网络还是下游限速,不要把零拷贝当作通用性能开关。

Netty 的线程模型和 Pipeline

Netty 将 Channel 绑定到一个 EventLoop,EventLoop 既处理该连接的 IO 事件,也顺序执行该 Channel Pipeline 的 Handler。这样可减少同一连接的锁竞争,但有一个硬约束:不要在 EventLoop 上执行慢 SQL、阻塞 HTTP、文件压缩或长循环,否则同一 EventLoop 管理的其他连接都会延迟。

1
2
3
BossGroup: accept 新连接
WorkerGroup/EventLoop: read -> decode -> inbound handlers -> write
BusinessExecutor: 慢业务、阻塞调用、CPU 重计算

Pipeline 中 inbound 事件通常从头到尾传播,outbound 写事件反向传播。Decoder 负责字节到消息,Encoder 负责消息到字节,业务 Handler 专注协议语义。Handler 若有实例字段且被多个 Channel 共享,必须线程安全;否则每个 Channel 创建独立 Handler,或使用 @Sharable 前明确无状态。

ByteBufByteBuffer 更适合网络框架,支持读写索引、池化和引用计数。引用计数对象在异步传递后必须正确 retain/release;泄漏检测只用于发现问题,不能替代资源所有权设计。业务代码不要长期保存 ByteBuf,应尽快拷贝为不可变业务对象或在约定范围内释放。

连接治理、心跳和背压

长连接服务必须处理半开连接、空闲连接、客户端断网和慢消费者。应用心跳不是为了替代 TCP,而是更快发现业务层失活;使用 Netty IdleStateHandler 检测读/写空闲后发送 ping 或关闭连接。服务端不能只依赖客户端心跳,需设置空闲超时和最大连接数。

写入也会背压:当客户端读取慢时,Channel 出站缓冲积压。Netty 的 Channel.isWritable() 和高低水位线可用于限制继续写入;达到高水位后暂停上游生产或丢弃允许丢失的实时推送,恢复可写再继续。无界队列缓存每个客户端消息会让少数慢连接拖垮 JVM。

现象 常见原因 优先处理
大量 CLOSE_WAIT 本端未关闭 socket/response body 检查异常路径 close、HTTP 客户端资源释放
大量 TIME_WAIT 主动关闭方连接频繁短连 优先连接复用和 Keep-Alive,而非盲调内核参数
EventLoop CPU 高 Handler 阻塞、空转 OP_WRITE、编解码死循环 线程 dump 定位 Handler,移交业务线程池
内存上涨 ByteBuf 泄漏、慢客户端出站积压、超大包 LeakDetector、写水位、包大小上限和连接限额

高频面试题

问题 回答要点
NIO 为什么能用少量线程处理大量连接? 非阻塞 Channel 注册到 Selector,线程只处理就绪事件,不为每个空闲连接阻塞等待。
TCP 为什么有粘包拆包? TCP 是连续字节流,没有应用消息边界;由协议长度字段、分隔符或固定长度解决。
Netty 为什么不能阻塞 EventLoop? 一个 EventLoop 管理多条连接,阻塞会让这些连接的读写和心跳都延迟。
零拷贝有什么收益? 减少额外内存复制和上下文切换,适合大文件传输,但不等于完全无复制。
如何治理慢客户端? 写水位和 isWritable 做背压,限制每连接缓冲和消息大小,必要时断开或降级。