Netty 核心组件学习记:小白也能看懂的内部机制

这是一个小白的学习记录
边学边练,把踩过的坑都记下来


为啥要学核心组件?

上回写了个 Echo 服务器,虽然跑起来了,但我心里没底啊。那些 Channel、EventLoop 都是啥玩意儿?就知道照着代码写,根本不理解原理。

要想真正用好 Netty,必须理解它的核心组件。于是我又开始啃这些概念,边学边记,把踩过的坑都写下来。

一、Channel 和 ChannelPipeline

Channel 是啥?

我理解的 Channel 就是一根水管,数据在里面流。Netty 提供了好几种 Channel:

  • NioSocketChannel:TCP 客户端用的
  • NioServerSocketChannel:TCP 服务端用的
  • NioDatagramChannel:UDP 用的
  • EpollSocketChannel:Linux 系统用的,据说性能更好

刚开始我还纳闷,为啥搞这么多 Channel?后来才明白,不同的场景用不同的实现嘛。

ChannelPipeline 是啥?

ChannelPipeline 就像一串过滤器,数据要经过多个处理器。用 Mermaid 图表表示更清楚:

出站流程

入站流程

入站

处理

处理

处理

出站

处理

处理

处理

网络数据

处理器1

处理器2

处理器3

应用程序

处理器3

处理器2

处理器1

网络数据

入站和出站

  • 入站:数据从网络到应用,比如 channelReadchannelActive
  • 出站:数据从应用到网络,比如 writeflush

我刚开始搞反了,以为出站是从网络到应用,后来测试了才明白。

二、EventLoop 和 EventLoopGroup

EventLoop 是啥?

EventLoop 就像一个工人,负责处理水管里的水流(I/O 事件)。它的工作:

  1. 处理通道的 I/O 事件
  2. 执行任务队列里的任务
  3. 维护通道的生命周期

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:当前写入位置

索引关系

0

readerIndex

writerIndex

capacity

ByteBuf内存布局

可丢弃的字节

可读的字节

可写的字节

内容

我刚开始总搞混这两个索引,导致读数据读错了。后来画了个图,才明白它们的关系。

内存池技术

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

六、我踩过的坑

  1. 线程数配置:bossGroup 线程数设多了,导致 CPU 使用率高
  2. 内存泄漏:用 ChannelInboundHandlerAdapter 没释放消息
  3. 索引混乱:没搞懂 ByteBuf 的读索引和写索引
  4. 内存类型选择:小数据用了直接内存,性能反而下降

七、最佳实践(我总结的)

  1. 用 SimpleChannelInboundHandler:自动释放消息,避免内存泄漏
  2. 合理配置线程数:bossGroup 1 个线程,workerGroup 默认
  3. 使用内存池:生产环境用 PooledByteBufAllocator
  4. 正确选择内存类型:小数据用堆内存,大数据用直接内存
  5. 避免阻塞操作:耗时操作放到业务线程池

验证步骤

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 这些组件有了点感觉,知道它们是干啥的,怎么用。但要真正掌握,还得继续练习。

肯定有理解不对的地方,欢迎大佬指正。

如果你也是新手,希望这篇笔记能帮到你。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐