Netty:ChannelPipeline、ChannelHandler 与消息流转
一、核心概念
Channel:一条网络连接或 IO 通道。
ChannelPipeline:每个 Channel 拥有自己的 Handler 处理链。
ChannelHandler:真正处理网络事件、编解码和业务逻辑。
ChannelHandlerContext(ctx):Handler 在 Pipeline 中的位置及与 Channel/后续节点交互的上下文。
一条 TCP 消息的典型处理流程:
客户端发送 TCP 字节流
↓
Channel(读到 ByteBuf)
↓
Pipeline 入站方向
↓
LineBasedFrameDecoder(按 \n 拆分帧)
↓
StringDecoder(ByteBuf → String)
↓
BusinessHandler(业务处理)
↓
Pipeline 出站方向
↓
StringEncoder(String → ByteBuf)
↓
Channel 异步写到 Socket二、Pipeline 的入站与出站
2.1 两个写法的区别(高频考点)
ctx.writeAndFlush(msg); // 从当前 Context 向前传播
ctx.channel().writeAndFlush(msg); // 从 Pipeline 尾部向前传播因此,如果 StringEncoder 加在业务 Handler 后面,ctx.writeAndFlush("hello") 可能绕过它,导致编码类型不匹配。推荐明确规划 Handler 顺序,而不是碰运气。
2.2 事件继续传播
ctx.fireChannelRead(msg); // 将入站消息交给后面的入站 Handler
ctx.fireChannelActive(); // 继续传播 channelActiveChannelInboundHandlerAdapter 中如果覆盖回调却既不继续传播又不释放消息,会使数据在中途被“吞掉”,引用计数消息还有泄漏风险。
三、两种常见 Handler
3.1 ChannelInboundHandlerAdapter
适合需要精细控制消息传播或释放的场景,不会默认替你释放收到的 ByteBuf。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ManualHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
System.out.println("可读字节=" + buf.readableBytes());
// 在当前 Handler 消费完数据,不传递给下一个 Handler
}
} finally {
ReferenceCountUtil.release(msg);
}
}
}若调用 ctx.fireChannelRead(msg) 把所有权传给后面的 Handler,就不要在当前 Handler 再释放同一消息。
3.2 SimpleChannelInboundHandler<T>
适合消费指定消息类型。默认自动释放被该 Handler 接受的引用计数消息。
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TextHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
ctx.writeAndFlush("服务端收到:" + msg + "\n");
}
}常见误区:自动释放后还把 ByteBuf 直接交给异步线程读取。正确做法是先复制需要的数据,或谨慎使用
retain()/release()管理生命周期。
四、完整 Java 8 TCP 服务端示例
4.1 Maven 依赖
Netty 是第三方库,不是 Java 8 自带。以下给出课程所用的 4.2.x 示例版本;实际生产要统一所有 Netty 模块版本,并核对项目依赖管理。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.2.19.Final</version>
</dependency>4.2 NettyServer.java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.StandardCharsets;
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup(4);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new LineBasedFrameDecoder(1024));
pipeline.addLast("stringDecoder", new StringDecoder(StandardCharsets.UTF_8));
pipeline.addLast("stringEncoder", new StringEncoder(StandardCharsets.UTF_8));
pipeline.addLast("business", new MyServerHandler());
}
});
ChannelFuture bindFuture = bootstrap.bind(8080).sync();
System.out.println("监听 8080 端口");
bindFuture.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
}
}
}4.3 MyServerHandler.java
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class MyServerHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("上线:" + ctx.channel().remoteAddress());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到:" + msg);
// encoder 在当前 Handler 之前,因此 ctx.writeAndFlush() 可以找到它
ctx.writeAndFlush("服务端收到:" + msg + "\n");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("离线:" + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}4.4 Windows 10 PowerShell 测试
$client = [System.Net.Sockets.TcpClient]::new("127.0.0.1", 8080)
$stream = $client.GetStream()
$writer = [System.IO.StreamWriter]::new($stream)
$reader = [System.IO.StreamReader]::new($stream)
$writer.AutoFlush = $true
$writer.WriteLine("hello")
$reader.ReadLine() # 预期:服务端收到:hello
$reader.Dispose()
$writer.Dispose()
$client.Dispose()这里必须以换行符结束消息,因为使用了 LineBasedFrameDecoder。TCP 是字节流,不能假设 read() 每次对应一条消息。
五、事件执行流程:讲透每一步
ServerBootstrap.bind(8080)绑定监听端口,sync()等待绑定操作完成。客户端连接后,Boss EventLoop 接受连接,Worker EventLoop 注册客户端 Channel。
ChannelInitializer为这个 Channel 配置 Pipeline。Worker EventLoop 接收到可读事件并从 Socket 读取 ByteBuf。
LineBasedFrameDecoder根据换行符形成完整帧。StringDecoder将字节数据转换为 Java String。MyServerHandler.channelRead0()执行业务逻辑。调用
ctx.writeAndFlush,出站操作向前经过StringEncoder。Netty 发起异步写入。若要知道 Netty 写是否完成,可监听
ChannelFuture。
5.1 异步回调示例
ChannelFuture future = ctx.writeAndFlush("OK\n");
future.addListener(f -> {
if (f.isSuccess()) {
System.out.println("Netty 写操作完成");
} else {
System.err.println("写失败:" + f.cause());
}
});注意:写成功不代表对方业务已经处理成功。设备下发命令要依赖自定义业务 ACK,配合请求 ID、超时与幂等。
六、结合电子价签设备的生产设计
若设备协议采用固定头 + 长度字段,推荐:
TCP 字节流
↓
协议帧解码(长度字段、半包处理、最大帧保护)
↓
消息解码(版本、命令类型、设备 ID 等)
↓
连接鉴权 Handler
↓
设备状态/消息 Handler
↓
异步业务执行与响应不能直接照搬 LineBasedFrameDecoder 处理二进制设备协议:如果帧里可能包含换行字节,应使用长度字段或其他协议确定的帧边界。解码时严格校验长度,防止异常输入导致内存过量申请。
七、生产常见踩坑
Decoder 顺序错误:业务 Handler 收到的可能还是 ByteBuf,而不是预期 String/对象。
出站起点错误:
ctx.writeAndFlush从当前 Handler 向前,注意 Encoder 所在位置。忘记释放 ByteBuf:使用
ChannelInboundHandlerAdapter消费消息后不释放,可能导致直接内存泄漏。重复释放 ByteBuf:
SimpleChannelInboundHandler已自动释放,再手动 release 可能出错。阻塞 IO EventLoop:不要在
channelRead0中直接执行慢 SQL、同步远程调用。缺少拆包处理:TCP 不能保证一次
read()收到整条业务消息。误判 writeFuture:Netty 写成功不等于客户端已消费,业务层要有 ACK。
业务线程组每连接新建:线程暴涨;必须复用共享执行器并优雅关闭。
八、常见问题与答案
Q1:Pipeline 有什么作用?
A:每个 Channel 拥有独立 Pipeline,按顺序组织 Handler,负责入站事件和出站操作的处理及传播,实现解码、鉴权和业务处理的解耦。
Q2:ctx.writeAndFlush() 与 channel.writeAndFlush() 区别?
A:前者从当前 Handler Context 向前传播出站操作;后者从 Pipeline 尾部开始向前传播,所以经过哪些 Encoder 可能不同。
Q3:ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler 的区别?
A:前者通常需要自己管理消息传播和引用计数释放;后者对匹配消息默认自动释放,更适合消费确定类型的消息。
Q4:如何避免 ByteBuf 内存泄漏?
A:明确所有权:要么继续 fireChannelRead 交给下游,要么在当前处理完释放;异步转交必须正确 retain/release 或复制数据,避免重复释放和悬空引用。
Q5:TCP 为何需要解码器处理半包、粘包?
A:TCP 提供字节流而非应用消息边界,发送方多次写可能被合并,单次写也可能被分成多次读。需通过固定长度、分隔符或长度字段识别帧。
九、一句话复习
Channel 持有 Pipeline,Pipeline 串联 Handler;入站向后传播、出站从触发点向前传播,重点关注编解码顺序、ByteBuf 生命周期与异步写入结果。
