Netty 第二篇:从 NIO 原生到 Netty 实战

 

📅 2026-05-22    ✍️ 原创    🏷️ Java NIO Netty 网络编程

在上一篇博客中,我们用 Java NIO 的原生 API 手写了一个最简单的客户端与服务器,跑通了多路复用的基本流程,在本章里将介绍Netty是怎么实现相应功能的。Netty 基于 NIO 封装,屏蔽了底层 Selector、Buffer 的复杂操作,提供了一套优雅的 API,让网络编程变得"人类友好"。本文将用 Netty 重写上一篇的 Demo。


一、回顾:NIO 原生实现有什么问题?

先快速回顾一下上一篇 NIO 服务端的核心痛点:

  1. Selector 模板代码冗余:打开 Selector、注册 Channel、设置 interestOps,这些步骤每次都要写,且容易出错。
  2. ByteBuffer 的 flip() 噩梦:写完数据必须 flip 才能读,忘了就读不到,读了多少还要自己记 position。
  3. 事件分发靠 if/elsekey.isAcceptable()key.isReadable() 一堆判断,Handler 多了代码迅速膨胀。
  4. 半包/粘包问题:原生 NIO 不提供任何编解码器,TCP 流式传输的拆包粘包需要自己处理。
  5. 资源释放容易遗漏:Channel 关闭、Key 取消、Buffer 回收,哪一步忘了都可能造成泄漏。
NIO 原生 API 就像"手动挡"——能开,但需要你什么都管。Netty 则是"自动挡",把底层细节全封装好了。

二、Netty 核心概念速览

在动手写代码之前,先快速过一遍 Netty 的几个核心概念,方便后续理解:

Netty 组件对应 NIO 原生作用
EventLoopGroup Selector + Thread 事件循环组,替代原生的 Selector 轮询,自动管理线程
ServerBootstrap 手动创建 ServerSocketChannel 服务端启动引导类,一站式配置 Channel、Handler、端口绑定
Bootstrap 手动创建 SocketChannel 客户端启动引导类,配置 Channel、连接地址、Handler
ChannelPipeline if/else 事件分发 责任链模式,每个 Handler 只关心自己的逻辑
ChannelHandler SelectionKey 事件处理 事件处理器,重写对应方法即可(channelRead、channelActive 等)
ByteBuf ByteBuffer Netty 的缓冲区,读写指针独立,无需 flip

三、Netty 服务端实现

直接上代码,然后用同样的思路逐段拆解:

package demo.nio;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
                .group(new NioEventLoopGroup(1), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf byteBuf = (ByteBuf) msg;
                                byte[] bytes = new byte[5];
                                byteBuf.readBytes(bytes);
                                System.out.println(new String(bytes));
                                super.channelRead(ctx, msg);
                            }
                        });
                    }
                })
                .bind(8080)
                .sync();
    }
}

整个服务端只有 35 行代码,比 NIO 原生版本短了一大截,而且逻辑非常清晰。我们逐段来看:

1️⃣ 创建 ServerBootstrap

ServerBootstrap serverBootstrap = new ServerBootstrap();

ServerBootstrap 是 Netty 服务端的启动引导类,相当于把所有"启动前的配置"都收敛到一个对象里。对比 NIO 原生写法——你需要手动创建 Selector、打开 ServerSocketChannel、configureBlocking、register、bind……每一步都是散落的代码,而这里全部被 ServerBootstrap 封装了。

2️⃣ 配置 EventLoopGroup

serverBootstrap
    .group(new NioEventLoopGroup(1), new NioEventLoopGroup())

这里配置了两个 NioEventLoopGroup

  • 第一个参数(bossGroup):负责接收客户端连接(对应 NIO 中原生的 ServerSocketChannel.accept()),这里只配了 1 个线程,因为接收连接这件事本身不需要太多线程。
  • 第二个参数(workerGroup):负责处理已建立连接的读写事件(对应 NIO 中原生的 SocketChannel.read/write),使用默认线程数(CPU 核心数 × 2),可以充分利用多核。
这就是经典的"老板-员工"模型:boss 负责接客,worker 负责干活。
⚠️ 注意:如果只传一个 EventLoopGroup,Netty 会让它同时承担 boss 和 worker 的角色,在客户端连接数不多时也能用,但生产环境建议分开配置。

3️⃣ 指定 Channel 类型

.channel(NioServerSocketChannel.class)

告诉 Netty 服务端使用 NioServerSocketChannel,底层就是 NIO 的 ServerSocketChannel。如果你用的是 Netty 的 epoll 版本(Linux 专属),可以换成 EpollServerSocketChannel,性能会更好。

4️⃣ 配置 ChildHandler(核心)

.childHandler(new ChannelInitializer<NioSocketChannel>() {
    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf byteBuf = (ByteBuf) msg;
                byte[] bytes = new byte[5];
                byteBuf.readBytes(bytes);
                System.out.println(new String(bytes));
                super.channelRead(ctx, msg);
            }
        });
    }
})

这是整个服务端最核心的配置,我们拆开看:

  • childHandler:注意是 childHandler,不是 handler。handler 是给 bossGroup 用的(处理服务端本身的事件),childHandler 是给 workerGroup 用的(处理每个客户端连接的事件)。上一篇 NIO 中,我们用 sscKeyscKey 区分服务端 Channel 和客户端 Channel,这里 Netty 直接通过 API 语义就分开了。
  • ChannelInitializer:每当有一个新的客户端连接进来,Netty 就会调用 initChannel 方法,在这个方法里配置该连接对应的 Pipeline。这对应 NIO 原生中"accept 之后注册 OP_READ"的那段逻辑。
  • ChannelInboundHandlerAdapter:这是一个适配器类,提供了默认的空实现。我们只需要重写关心的方法即可。这里重写了 channelRead,对应 NIO 原生中"if (key.isReadable())"的处理逻辑。
  • ByteBuf:Netty 的缓冲区,替代了 NIO 的 ByteBufferreadBytes(bytes) 内部自动维护读指针,不需要 flip,也不需要关心 position/limit,用起来直观多了。
⚠️ 注意:这里的 super.channelRead(ctx, msg) 调用了父类的默认实现,父类会释放 msg 的引用计数。如果重写 channelRead 后不调用 super.channelRead,可能会导致 ByteBuf 的引用计数无法正常递减,进而造成内存泄漏。生产环境中更推荐继承 SimpleChannelInboundHandler<ByteBuf>,它会在 channelRead0 执行完后自动释放消息。

5️⃣ 绑定端口并同步等待

.bind(8080)
.sync();

bind(8080) 绑定端口,sync() 阻塞等待绑定完成。对比 NIO 原生写法,你需要手动调用 ssc.bind(new InetSocketAddress(8080)),然后进入 while 循环调用 selector.select()。Netty 把这些全部封装了——你不需要写 while 循环,不需要调 select(),EventLoopGroup 内部已经帮你做好了事件循环


四、Netty 客户端实现

客户端代码同样简洁:

package demo.nio;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {
    public static void main(String[] args) throws Exception {
        Bootstrap bootstrap = new Bootstrap();
        Channel channel = bootstrap
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                    }
                })
                .connect("127.0.0.1", 8080)
                .sync()
                .channel();
        channel.writeAndFlush(Unpooled.copiedBuffer("hello".getBytes()));
        channel.close();
    }
}

逐段解析:

1️⃣ 创建 Bootstrap

Bootstrap bootstrap = new Bootstrap();

Bootstrap 是客户端的启动引导类,对应服务端的 ServerBootstrap。一个用于"被动接受连接",一个用于"主动发起连接"。

2️⃣ 配置 EventLoopGroup

.group(new NioEventLoopGroup())

客户端只需要一个 EventLoopGroup,因为它只需要管理自己这一条(或少数几条)连接的事件循环,不像服务端需要区分 boss 和 worker。

3️⃣ 指定 Channel 类型

.channel(NioSocketChannel.class)

客户端使用 NioSocketChannel,底层对应 NIO 的 SocketChannel

4️⃣ 设置 ChannelOption

.option(ChannelOption.TCP_NODELAY, true)

这里设置了 TCP_NODELAY 为 true,禁用 Nagle 算法,让数据尽可能快地发送出去。在 NIO 原生中,你需要通过 SocketChannel.socket().setTcpNoDelay(true) 来设置,Netty 直接在 Bootstrap 的链式调用中就完成了。

5️⃣ 配置 Handler

.handler(new ChannelInitializer<NioSocketChannel>() {
    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
    }
})

客户端的 handler(注意不是 childHandler,因为客户端没有"子连接"的概念)用于配置该连接的处理器链。在这个 Demo 中,客户端只负责发送数据,不需要接收服务端回复,所以 Handler 是空的。如果后续需要读取服务端的响应,可以在这里添加 ChannelInboundHandlerAdapter 并重写 channelRead

6️⃣ 连接服务器并发送数据

.connect("127.0.0.1", 8080)
.sync()
.channel();
channel.writeAndFlush(Unpooled.copiedBuffer("hello".getBytes()));
channel.close();

connect 发起连接,sync() 阻塞等待连接建立成功,然后通过 channel() 获取 Channel 对象。writeAndFlush 将数据写入并立即刷新到网络中。Unpooled.copiedBuffer 把 byte 数组包装成 ByteBuf,底层会拷贝一份数据到 Netty 的内存池中。

⚠️ 注意:这个 Demo 中客户端发送完数据就立刻 close() 了。在实际场景中,如果服务端有响应需要接收,不应该马上关闭,而是在 channelRead 中处理完响应后再关闭,或者使用 closeFuture().sync() 等待服务端关闭连接。

五、NIO 原生 vs Netty 全方位对比

代码看完了,下面用一张表把 NIO 原生和 Netty 做全方位对比:

对比维度NIO 原生Netty
线程模型 手动创建 Selector + 单线程循环 EventLoopGroup 自动管理,boss/worker 分离
缓冲区 ByteBuffer,需手动 flip() ByteBuf,读写指针独立,无需 flip
事件分发 if/else 判断 SelectionKey 事件类型 ChannelPipeline 责任链,Handler 按需添加
编解码 无,需手动处理半包/粘包 内置 StringDecoder / StringEncoder / 多种 FrameDecoder
连接管理 手动 accept / register / cancel Bootstrap 链式配置,自动管理生命周期
异常处理 try/catch 包裹,容易遗漏 handlerExceptionCaught 统一处理
资源释放 需手动关闭 Channel、取消 Key 引用计数 + 自动释放(SimpleChannelInboundHandler)
代码量 服务端 ~70 行,模板代码多 服务端 ~35 行,逻辑集中,一目了然

六、运行效果

先启动服务端,再启动客户端,服务端控制台输出:

hello

和上一篇 NIO 原生 Demo 的运行效果完全一致。服务端收到客户端发送的 "hello",读取 5 个字节并打印。虽然功能相同,但背后的实现已经从"手动挡"升级到了"自动挡"。


七、总结

通过这一篇的对比,相信你已经感受到了 Netty 的核心价值:

  1. 屏蔽底层复杂度:Selector 的轮询、ByteBuffer 的 flip、事件的 if/else 分发——全部被封装。
  2. API 设计优雅:ServerBootstrap / Bootstrap 的链式调用,Pipeline 的责任链模式,用起来非常"流式"。
  3. 扩展性强:从简单的 echo 服务器到复杂的 RPC 框架(如 Dubbo),Netty 的 Pipeline + Handler 架构都能胜任。
  4. 生态丰富:内置多种编解码器、拥塞控制、心跳检测等组件,开箱即用。

— 原创文章,转载请注明出处 —

posted @ 2026-05-29 14:57  小金虫  阅读(17)  评论(0)    收藏  举报