前言
我们在《网络是如何连接的》这个专栏中讲过:网络包是如何从浏览器一路传输到服务器的。
然而,当这些数据包抵达服务器后,它们是如何从内核协议栈最终送达Netty 应用程序的?这部分才是真正的“网络到应用”的关键路径。
前几篇文章我们分析了 Netty 服务端的启动流程和连接接入机制。
今天,我们继续往下看:当网络包到达服务器协议栈之后,Netty 是如何被内核通知并把数据读取上来的?
一、网络包从协议栈到Netty
当客户端发送数据包后,服务器的网卡(NIC)首先通过 DMA(直接内存访问)将数据写入内核缓冲区。然后进入如下步骤:
-
协议栈接收与重组
内核 TCP/IP 协议栈会对收到的网络包进行校验(例如校验和、序列号、窗口大小),确保其合法且顺序正确。
如果数据被拆分成多个包(分片),协议栈会将这些数据片段重新拼装成连续的字节流,放入 TCP 的接收缓冲区(sk_buff链表)。 -
内核通知应用层(中断与事件驱动)
当接收缓冲区中有新数据到达时,内核会通过以下机制之一通知应用程序有数据可读:-
对传统阻塞 socket:应用程序在
read()阻塞等待时,内核直接唤醒该进程。 -
对非阻塞 socket(Netty 使用的):内核通过 事件通知机制(如
epoll)将该 socket 标记为“可读”,并把事件放入 epoll 的就绪队列。
具体过程:- 当 TCP 缓冲区收到新数据后,协议栈调用
sock_def_readable()。 - 该函数会触发等待在此 socket 上的 等待队列(wait queue) 。
- 如果是 epoll 机制,内核会在
ep_poll_callback()中将该 fd 加入 epoll 实例的“就绪链表”。
- 当 TCP 缓冲区收到新数据后,协议栈调用
换句话说:协议栈不会主动推数据到用户态,而是告诉应用层“这个 socket 上有数据可以读” 。
-
-
Netty 的 Reactor 线程检测事件
Netty 的 I/O 线程(NioEventLoop)在底层通过epoll_wait()(Linux)或select()(Windows)持续轮询内核的就绪队列。
当内核把某个连接标记为“可读”后,epoll_wait()会返回该事件,Netty 得知“该 Channel 有数据可以读取”。 -
从内核缓冲区读取数据
Netty 收到事件后,调用底层的SocketChannel.read()(即recv()系统调用)从内核态复制数据到用户态的ByteBuf。- 内核会从 TCP 接收缓冲区中将数据复制到用户空间内存(Netty 分配的直接内存或堆内存)。
- 这一步是一次真正的用户态-内核态数据拷贝。
- 读完后,Netty 将
ByteBuf封装成事件,通过 Pipeline 向上触发channelRead()等回调。
整个过程
整个“网络包从协议栈到 Netty 应用”的路径可以概括为:
客户端发包
↓
网卡接收 → DMA写内核缓冲
↓
内核协议栈(TCP/IP层)重组校验
↓
将完整数据放入 TCP 接收缓冲区
↓
协议栈触发 epoll 回调 → 标记 fd 可读
↓
Netty 的 epoll_wait() 被唤醒
↓
调用 recv() 从内核缓冲复制数据到 ByteBuf
↓
触发 pipeline → 业务逻辑(channelRead)
二、拆包器与解码器
前面我们提到过,TCP 协议并不关心网络包是否具有业务意义。在实际传输过程中,由于网络拥塞、操作系统调度、Nagle 算法等因素,可能会出现以下几种情况:
- 多个独立的业务包被合并成一个 TCP 包;
- 一个业务包被拆成多个 TCP 包(半包);
- 即使一个 TCP 包恰好对应一个业务包,Netty 在读取数据时也可能因
ByteBuf大小限制将其截断。
因此,应用层必须自行根据业务协议,将这些“原始流式字节数据”正确拆分成完整的业务包。这正是 拆包器(Frame Decoder) 的作用。
拆包器的原理
在没有 Netty 的情况下,若用户自己实现拆包逻辑,通常遵循以下原则:
- 数据不足则继续读取
当当前读取的数据不足以构成完整的业务包时,暂存这部分数据,等待下一次从 TCP 缓冲区读取更多字节后再拼接。 - 数据足够则组包并透传
当累计的数据已能拼接出一个完整的业务包时,将其组装成一个完整的数据帧,交由业务逻辑处理;若存在多余数据,则保留用于下次拼接。
Netty 的拆包机制本质上也遵循相同思路,只不过框架为我们封装了通用逻辑。
其核心基类是 ByteToMessageDecoder,该类内部维护一个累加器(Cumulator) ,用于合并多次从网络读取到的字节流,并尝试解析出完整的业务帧。
累加器实现(MERGE_CUMULATOR)
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
ByteBuf buffer;
if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
|| cumulation.refCnt() > 1) {
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
} else {
buffer = cumulation;
}
buffer.writeBytes(in);
in.release();
return buffer;
}
};
MERGE_CUMULATOR 是 ByteToMessageDecoder 默认的累加器实现,用于将新读到的 ByteBuf in 合并到已有的累加区 ByteBuf buffer 中。
在合并之前,它会检查缓冲区容量是否足够,若不足则进行扩容。
三、核心方法:channelRead
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
boolean first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
channelRead() 是 ByteToMessageDecoder 的核心逻辑。Netty 从协议栈取到字节数据后,会触发该方法。整体流程可以分为四个阶段:
1. 数据累加
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
每次从内核读取到新数据时,将其累加到 cumulation 中,形成一个完整的连续字节流。
2. 尝试拆包
callDecode(ctx, cumulation, out);
该方法内部会调用抽象方法 decode():
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
decode(ctx, in, out);
}
}
decode() 由子类实现,用于根据协议规则将累加的字节流拆分成完整业务帧。
常见的实现有:
LengthFieldBasedFrameDecoder(基于长度字段拆包)LineBasedFrameDecoder(基于换行符拆包)DelimiterBasedFrameDecoder(基于分隔符拆包)
3. 缓冲区清理
若当前 ByteBuf 已无可读数据,则直接释放;
若连续多次(默认 16 次)读取后仍有未处理数据,则进行压缩(discardSomeReadBytes()),将有效数据段移至首部以节省空间。
4. 触发业务逻辑
当成功拆出业务包后:
fireChannelRead(ctx, out, size);
Netty 会将拆好的业务帧逐一触发到下游的 Handler 中继续处理。
期间 decodeWasNull 标记用于指示本次解码是否产生了新对象。
四、解码器(Decoder)
解码器的作用是将经过拆包器得到的完整字节帧,进一步反序列化为应用层能理解的对象。
例如,在自定义通信协议中,包结构可能包含以下字段:
| 字段 | 含义 |
|---|---|
| Magic | 协议标识 |
| Version | 协议版本 |
| Command | 命令类型 |
| Length | 数据长度 |
| Body | 实际序列化数据 |
解码器会按约定读取这些字段,解析出业务对象并交由上层业务逻辑使用。
在 Netty 的 pipeline 中,通常是“拆包器 → 解码器 → 业务 Handler”这样的责任链顺序。
小结
本章我们完整梳理了网络包到达协议栈后,如何在应用层变成可识别业务对象的过程。
拆包器负责把连续的 TCP 字节流切分成完整的业务帧;
解码器则进一步将这些字节反序列化为应用层对象。
至此,Netty 的主流程已经基本闭环。
下一章,我们将深入分析 ChannelPipeline 与 writeAndFlush() 的内部机制,探索它们在事件传播与数据写出中的关键细节。