Netty 核心组件学习记:小白也能看懂的内部机制
Netty 核心组件学习记:小白也能看懂的内部机制
这是一个小白的学习记录
边学边练,把踩过的坑都记下来
为啥要学核心组件?
上回写了个 Echo 服务器,虽然跑起来了,但我心里没底啊。那些 Channel、EventLoop 都是啥玩意儿?就知道照着代码写,根本不理解原理。
要想真正用好 Netty,必须理解它的核心组件。于是我又开始啃这些概念,边学边记,把踩过的坑都写下来。
一、Channel 和 ChannelPipeline
Channel 是啥?
我理解的 Channel 就是一根水管,数据在里面流。Netty 提供了好几种 Channel:
NioSocketChannel:TCP 客户端用的NioServerSocketChannel:TCP 服务端用的NioDatagramChannel:UDP 用的EpollSocketChannel:Linux 系统用的,据说性能更好
刚开始我还纳闷,为啥搞这么多 Channel?后来才明白,不同的场景用不同的实现嘛。
ChannelPipeline 是啥?
ChannelPipeline 就像一串过滤器,数据要经过多个处理器。用 Mermaid 图表表示更清楚:
入站和出站
- 入站:数据从网络到应用,比如
channelRead、channelActive - 出站:数据从应用到网络,比如
write、flush
我刚开始搞反了,以为出站是从网络到应用,后来测试了才明白。
二、EventLoop 和 EventLoopGroup
EventLoop 是啥?
EventLoop 就像一个工人,负责处理水管里的水流(I/O 事件)。它的工作:
- 处理通道的 I/O 事件
- 执行任务队列里的任务
- 维护通道的生命周期
EventLoopGroup 配置
EventLoopGroup 是工人的团队,负责管理多个 EventLoop。
// 创建事件循环组
// bossGroup 负责接受连接,通常只需要一个线程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// workerGroup 负责处理业务,默认线程数是 CPU 核心数的 2 倍
EventLoopGroup workerGroup = new NioEventLoopGroup();
线程数配置坑
我一开始没设置 bossGroup 的线程数,结果创建了很多线程,CPU 使用率直接上去了。后来才知道,bossGroup 只需要 1 个线程就够了,因为它只负责接受连接。
教训:bossGroup 线程数设为 1,workerGroup 用默认值就好。
三、ChannelHandler
处理器类型
- ChannelInboundHandler:处理入站事件,比如读数据
- ChannelOutboundHandler:处理出站事件,比如写数据
- ChannelDuplexHandler:同时处理入站和出站
SimpleChannelInboundHandler
这个是个好东西,它会自动释放消息对象,避免内存泄漏。
public class MyHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("Received: " + msg);
}
}
我之前用的是 ChannelInboundHandlerAdapter,结果没释放消息,导致内存泄漏。后来换成 SimpleChannelInboundHandler,问题解决了。
教训:能用 SimpleChannelInboundHandler 就用它,省得手动释放消息。
四、ByteBuf(字节缓冲区)
ByteBuf 是啥?
ByteBuf 是 Netty 用来装字节数据的容器,比 Java 自带的 ByteBuffer 好用多了。
内存类型
- 堆内存:在 JVM 堆里,分配释放快,但 I/O 操作需要拷贝
- 直接内存:在堆外,I/O 操作不需要拷贝,但分配释放慢
我刚开始搞不懂这两种内存的区别,后来查资料才明白:堆内存适合小数据,直接内存适合大数据。
ByteBuf 的创建
// 创建堆内存缓冲区
ByteBuf heapBuf = Unpooled.buffer(1024);
// 创建直接内存缓冲区
ByteBuf directBuf = Unpooled.directBuffer(1024);
// 从字节数组创建
byte[] bytes = {1, 2, 3, 4, 5};
ByteBuf wrappedBuf = Unpooled.wrappedBuffer(bytes);
// 复制字节数组
ByteBuf copiedBuf = Unpooled.copiedBuffer(bytes);
读索引和写索引
ByteBuf 有两个索引:
- readerIndex:当前读取位置
- writerIndex:当前写入位置
我刚开始总搞混这两个索引,导致读数据读错了。后来画了个图,才明白它们的关系。
内存池技术
Netty 4.0 引入了内存池,通过 PooledByteBufAllocator 管理内存,减少内存分配和垃圾回收的开销。
// 获取默认的内存池分配器
ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
// 创建内存池缓冲区
ByteBuf pooledBuf = allocator.buffer(1024);
常用操作
// 写入数据
byteBuf.writeByte(1);
byteBuf.writeInt(100);
byteBuf.writeBytes("Hello".getBytes());
// 读取数据
byte b = byteBuf.readByte();
int i = byteBuf.readInt();
byte[] bytes = new byte[5];
byteBuf.readBytes(bytes);
// 重置读索引
byteBuf.resetReaderIndex();
// 清空缓冲区
byteBuf.clear();
五、实战:自定义 ChannelHandler
我写了个自定义处理器,用来处理字符串消息:
自定义入站处理器
public class CustomInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Client connected: " + ctx.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
String message = buf.toString(CharsetUtil.UTF_8);
System.out.println("Received: " + message);
// 响应客户端
ctx.writeAndFlush(Unpooled.copiedBuffer("Server: " + message, CharsetUtil.UTF_8));
// 记得释放 buf,否则会内存泄漏
buf.release();
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Client disconnected: " + ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
自定义出站处理器
public class CustomOutboundHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
String message = buf.toString(CharsetUtil.UTF_8);
System.out.println("Sending: " + message);
}
super.write(ctx, msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
System.out.println("Flushing buffer");
super.flush(ctx);
}
}
六、我踩过的坑
- 线程数配置:bossGroup 线程数设多了,导致 CPU 使用率高
- 内存泄漏:用 ChannelInboundHandlerAdapter 没释放消息
- 索引混乱:没搞懂 ByteBuf 的读索引和写索引
- 内存类型选择:小数据用了直接内存,性能反而下降
七、最佳实践(我总结的)
- 用 SimpleChannelInboundHandler:自动释放消息,避免内存泄漏
- 合理配置线程数:bossGroup 1 个线程,workerGroup 默认
- 使用内存池:生产环境用 PooledByteBufAllocator
- 正确选择内存类型:小数据用堆内存,大数据用直接内存
- 避免阻塞操作:耗时操作放到业务线程池
验证步骤
1. 测试自定义处理器
// 在 ServerBootstrap 中添加处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new CustomInboundHandler());
ch.pipeline().addLast(new CustomOutboundHandler());
}
});
2. 启动服务端和客户端
# 启动服务端
java CustomServer
# 启动客户端
java CustomClient
3. 测试消息发送
在客户端输入消息,看服务端是否正确处理。
预期结果:服务端显示"Received: 消息内容",客户端收到"Server: 消息内容"。
总结
其实 Netty 的核心组件也没那么难,就是刚开始概念有点多,容易记混。我也是画了好多图,做了好多测试,才慢慢理解的。
现在我对 Channel、EventLoop、ChannelHandler、ByteBuf 这些组件有了点感觉,知道它们是干啥的,怎么用。但要真正掌握,还得继续练习。
肯定有理解不对的地方,欢迎大佬指正。
如果你也是新手,希望这篇笔记能帮到你。
更多推荐




所有评论(0)