Netty 第二篇:从 NIO 原生到 Netty 实战
在上一篇博客中,我们用 Java NIO 的原生 API 手写了一个最简单的客户端与服务器,跑通了多路复用的基本流程,在本章里将介绍Netty是怎么实现相应功能的。Netty 基于 NIO 封装,屏蔽了底层 Selector、Buffer 的复杂操作,提供了一套优雅的 API,让网络编程变得"人类友好"。本文将用 Netty 重写上一篇的 Demo。
一、回顾:NIO 原生实现有什么问题?
先快速回顾一下上一篇 NIO 服务端的核心痛点:
- Selector 模板代码冗余:打开 Selector、注册 Channel、设置 interestOps,这些步骤每次都要写,且容易出错。
- ByteBuffer 的 flip() 噩梦:写完数据必须 flip 才能读,忘了就读不到,读了多少还要自己记 position。
- 事件分发靠 if/else:
key.isAcceptable()、key.isReadable()一堆判断,Handler 多了代码迅速膨胀。 - 半包/粘包问题:原生 NIO 不提供任何编解码器,TCP 流式传输的拆包粘包需要自己处理。
- 资源释放容易遗漏: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 负责干活。
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 中,我们用sscKey和scKey区分服务端 Channel 和客户端 Channel,这里 Netty 直接通过 API 语义就分开了。ChannelInitializer:每当有一个新的客户端连接进来,Netty 就会调用initChannel方法,在这个方法里配置该连接对应的 Pipeline。这对应 NIO 原生中"accept 之后注册 OP_READ"的那段逻辑。ChannelInboundHandlerAdapter:这是一个适配器类,提供了默认的空实现。我们只需要重写关心的方法即可。这里重写了channelRead,对应 NIO 原生中"if (key.isReadable())"的处理逻辑。ByteBuf:Netty 的缓冲区,替代了 NIO 的ByteBuffer。readBytes(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 的内存池中。
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 的核心价值:
- 屏蔽底层复杂度:Selector 的轮询、ByteBuffer 的 flip、事件的 if/else 分发——全部被封装。
- API 设计优雅:ServerBootstrap / Bootstrap 的链式调用,Pipeline 的责任链模式,用起来非常"流式"。
- 扩展性强:从简单的 echo 服务器到复杂的 RPC 框架(如 Dubbo),Netty 的 Pipeline + Handler 架构都能胜任。
- 生态丰富:内置多种编解码器、拥塞控制、心跳检测等组件,开箱即用。
— 原创文章,转载请注明出处 —

浙公网安备 33010602011771号