# Java NIO 与 Netty
用生活化的比喻,让你理解 NIO 的三大组件和 Netty 的事件驱动架构
前置知识:第01章 JVM 深度原理 + 第02章 并发编程(线程模型基础)
阅读指南(初学者必看)
为什么你需要学习 Java NIO 与 Netty?
游戏服务器的核心是网络通信——每个玩家都通过 TCP/WebSocket 长连接与服务器交互。不理解 NIO 和 Netty,就无法:
- 理解为什么 BIO 不能支撑游戏服务器(一个连接一个线程 = 资源耗尽)
- 理解 Netty 的事件驱动模型(boss/worker 线程组)
- 开发高性能的游戏网关和协议编解码
学完本章,你能回答:
- NIO 的 Channel、Buffer、Selector 分别是什么?怎么配合工作?
- Netty 的 boss/worker 线程模型是怎么设计的?
- 零拷贝是什么?为什么对游戏服务器重要?
- 如何用 Netty 实现一个游戏网关服务器?
- Protobuf 编解码、心跳、断线重连怎么实现?
本文结构
第一部分:NIO 三大组件(Channel、Buffer、Selector)
第二部分:Reactor 模式(高性能网络编程的核心)
第三部分:Netty 架构(EventLoop、Pipeline、Handler)
第四部分:零拷贝与性能优化
第五部分:Netty 游戏服务器实战(ProtoBuf、心跳、网关)
一、IO 模型对比
1.1 三种 IO 模型
| 模型 | 名称 | 特点 | 类比 |
|---|---|---|---|
| BIO | 阻塞IO | 一个连接一个线程 | 一个客人配一个服务员 |
| NIO | 非阻塞IO | 多路复用,一个线程管多个连接 | 一个服务员轮询多桌 |
| AIO | 异步IO | 操作系统回调通知 | 客人按铃,服务员再过去 |
1.2 BIO 的问题
// 传统BIO服务器
ServerSocket server = new ServerSocket(8080);
while (true) {
// accept()会阻塞,直到有客户端连接
Socket socket = server.accept();
// 为每个连接创建一个线程
new Thread(() -> handle(socket)).start();
}
问题:
- 1000个连接 = 1000个线程
- 线程切换开销大
- 很多连接只是挂着,没有数据发送
BIO模型(10000个连接 = 10000个线程)
┌─────────────────────────────────────────────────────┐
│ 服务器 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 线程池(假设100线程) │ │
│ │ [T1] [T2] [T3] ... [T100] │ │
│ └─────────────────────────────────────────────┘ │
│ ↑ ↑ ↑ ↑ │
│ [连接1] [连接2] [连接3] ... [连接9900+] │
│ │
│ 问题: │
│ 1. 连接数 >> 线程数,线程不够用 │
│ 2. 线程占用内存,每个线程约1MB栈空间 │
│ 3. 线程切换有开销 │
└─────────────────────────────────────────────────────┘
二、NIO 三大组件
// Channel:双向通道,可以读也可以写
// Buffer:数据容器,读写都经过缓冲区
// Selector:多路复用器,一个线程管理多个 Channel
// NIO vs BIO
// BIO:一个连接一个线程(浪费)
// NIO:一个线程管理多个连接(高效)
生活类比
BIO:一家餐厅,每个顾客配一个服务员,顾客不吃的时候服务员也守着。
NIO:一家餐厅,一个服务员用对讲机管理多桌顾客,谁需要服务就响应谁。
Channel 详解
Channel 类型:
├── FileChannel ── 文件读写(不支持非阻塞)
├── SocketChannel ── TCP 客户端
├── ServerSocketChannel ── TCP 服务端
└── DatagramChannel ── UDP 通信
Buffer 详解
生活类比:Buffer 就像一个快递箱——数据先进箱子,再从箱子取。
Buffer 核心属性:
capacity ── 箱子总容量
position ── 当前读写位置
limit ── 读/写上限
写模式:position 从 0 开始,写一个数据 position+1
翻转(flip):position→0,limit→之前的 position
读模式:从 position 读到 limit
// ByteBuffer是NIO数据读写的容器
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 三种状态:position(当前位置)、limit(限制)、capacity(容量)
// 写模式:position=0, limit=capacity
buffer.put("Hello".getBytes());
// 切换到读模式:flip()
// position=0, limit=原来position的位置
buffer.flip();
// 读取数据
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
// 清空缓冲区,回到写模式
buffer.clear();
| Buffer 类型 | 存储内容 | 游戏场景 |
|---|---|---|
| ByteBuffer | 字节 | 网络通信最常用 |
| CharBuffer | 字符 | 文本处理 |
| IntBuffer | int | 游戏数值 |
| ShortBuffer | short | 协议字段 |
直接内存 vs 堆内存:
// 堆内存(普通Buffer)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 直接内存(堆外内存,不受GC管理,IO操作更快)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
// 区别:
// - 堆内存:有GC,开辟和释放快
// - 直接内存:无GC,但分配释放慢,适合长期存在的Buffer
Selector 详解
// Selector 核心用法
Selector selector = Selector.open();
// 注册 Channel 到 Selector,监听事件
channel.register(selector, SelectionKey.OP_ACCEPT); // 接受连接
channel.register(selector, SelectionKey.OP_READ); // 读取数据
channel.register(selector, SelectionKey.OP_WRITE); // 写入数据
// 轮询就绪的 Channel
while (true) {
int readyChannels = selector.select(); // 阻塞直到有事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
if (key.isAcceptable()) { /* 处理新连接 */ }
if (key.isReadable()) { /* 读取数据 */ }
if (key.isWritable()) { /* 写入数据 */ }
}
}
| 事件 | 含义 | 生活类比 |
|---|---|---|
| OP_ACCEPT | 有新连接来了 | 门铃响了 |
| OP_CONNECT | 连接建立成功 | 电话打通了 |
| OP_READ | 有数据可读 | 信箱有新信 |
| OP_WRITE | 可以写数据 | 邮筒有空位 |
NIO 完整服务器示例
public class NioServer {
private Selector selector;
private ByteBuffer buffer = ByteBuffer.allocate(1024);
public void start() throws IOException {
selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务器启动,端口8080...");
while (true) {
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (!key.isValid()) continue;
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("新连接:" + client.getRemoteAddress());
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
buffer.clear();
int read = client.read(buffer);
if (read == -1) {
key.cancel();
client.close();
return;
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String msg = new String(data);
System.out.println("收到:" + msg);
String response = "Echo: " + msg;
buffer.clear();
buffer.put(response.getBytes());
buffer.flip();
client.write(buffer);
}
}
三、Reactor 模式
为什么需要 Reactor 模式?
NIO虽然是非阻塞的,但代码写起来很复杂:
- 需要手动管理多个Channel
- 需要手动处理各种事件
- 很难处理业务逻辑
Reactor模式是NIO的升华:
- 将IO操作和业务处理分离
- 让开发者只关注业务逻辑
Reactor 模式原理
Reactor模式结构:
┌─────────────────────────────────────────────────────────────┐
│ Reactor │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Acceptor(分发器) │ │
│ │ 监听端口,接受新连接 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Handler(处理器) │ │
│ │ 处理读写、业务逻辑 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
单线程Reactor:
┌─────────┐
│Acceptor │ ← 单线程处理所有事件
└────┬────┘
↓
┌────┴────┐
│ Handler │
└─────────┘
多线程Reactor(Netty默认):
┌───────────────────┐
│ Boss Group (1个) │ 只处理accept
│ NioEventLoop │
└───────────────────┘
│
v
┌───────────────────┐
│ Worker Group(N个)│ 处理read、decode、encode、write
│ NioEventLoop │ 业务逻辑可以交给业务线程池
└───────────────────┘
Netty 的 Reactor 实现
Netty是对NIO和Reactor的封装,比手写NIO更方便:
// Netty版服务器(比手写NIO简洁很多!)
public class NettyServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 主Reactor
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 从Reactor
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new GameDecoder()); // 解码
pipeline.addLast(new GameEncoder()); // 编码
pipeline.addLast(new GameHandler()); // 业务处理
}
});
ChannelFuture f = bootstrap.bind(8080).sync();
System.out.println("Netty服务器启动...");
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
四、Netty 架构
生活类比:Netty 就像一个高效的快递分拣中心。
EventLoopGroup(boss 组)── 接收连接
↓
EventLoopGroup(worker 组)── 处理读写
↓
Pipeline(处理链)── 一系列 Handler 依次处理
├── Decoder(解码)
├── BusinessHandler(业务逻辑)
└── Encoder(编码)
游戏服务器为什么用 Netty?
- 高性能 NIO 框架,处理 WebSocket / TCP 长连接
- 零拷贝:减少内存拷贝(和 GPU 纹理上传的零拷贝概念类似)
- 线程模型:boss/worker 分离,和网络 I/O 分离
- 丰富的编解码器:Protobuf、HTTP、WebSocket
Netty 线程模型
┌──────────────────────┐
│ Boss EventLoop │
│ (接收新连接) │
└──────────┬───────────┘
│ 注册连接到 Worker
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ Worker 4 │
│ EventLoop│ │ EventLoop│ │ EventLoop│ │ EventLoop│
│ ┌──────┐ │ │ │ │ │ │ │
│ │Ch-1 │ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │
│ │Ch-2 │ │ │ │Ch-3 │ │ │ │Ch-4 │ │ │ ┌──────┐ │
│ │Ch-5 │ │ │ │Ch-6 │ │ │ │Ch-7 │ │ │ │Ch-8 │ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
每个Channel绑定一个EventLoop,EventLoop内部单线程串行执行
关键设计:EventLoop 内部是单线程,Channel 绑定到固定的 EventLoop,避免锁竞争。
Pipeline 和 Handler
// Pipeline 是 Handler 的链表
// 每个 Channel 有自己的 Pipeline
// Inbound(入站):数据从网络流向应用
// HeadContext → Decoder → BusinessHandler → TailContext
// Outbound(出站):数据从应用流向网络
// TailContext → Encoder → HeadContext
// 典型游戏服务器 Pipeline 配置
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
pipeline.addLast("protobufDecoder", new ProtobufDecoder(GameMessage.getDefaultInstance()));
pipeline.addLast("frameEncoder", new LengthFieldPrepender(2));
pipeline.addLast("protobufEncoder", new ProtobufEncoder());
pipeline.addLast("idleHandler", new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("gameHandler", new GameMessageHandler());
核心组件
| 组件 | 说明 |
|---|---|
| EventLoop | 事件循环(处理连接、读写) |
| Channel | 连接 |
| ChannelPipeline | 管道(多个处理Handler的容器) |
| ChannelHandler | 处理逻辑(入站/出站) |
五、零拷贝与性能优化
零拷贝是什么?
生活类比:传统数据传输像搬家——先把东西搬到走廊(内核缓冲区),再搬到新家(用户缓冲区),再搬回走廊,再搬到目的地。零拷贝就是直接从旧家搬到新家,省去中间步骤。
传统数据传输(4次拷贝):
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket缓冲区 → 网卡
零拷贝(2次拷贝):
磁盘 → 内核缓冲区 → 网卡(直接从内核缓冲区发送)
Netty 的零拷贝优化
| 技术 | 说明 | 生活类比 |
|---|---|---|
| CompositeByteBuf | 逻辑合并多个 ByteBuf,不拷贝 | 把几个箱子并排放,不合并内容 |
| Unpooled.wrappedBuffer | 包装已有数组,不拷贝 | 给已有箱子贴标签 |
| ByteBuf.slice | 切片共享底层数组 | 从大箱子里划出一块区域 |
| FileRegion | 文件传输零拷贝 | 直接从仓库发货 |
六、编解码器设计
解决粘包/拆包
// LengthFieldBasedFrameDecoder:基于长度字段的拆包
// 协议格式:[长度(4字节)][消息类型(2字节)][payload]
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
65535, // 最大帧长度
0, // 长度字段偏移
4, // 长度字段字节数
6, // 长度调整(跳过消息类型)
0 // 跳过的字节数
));
自定义编解码器
public class GameMessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) return; // 长度不够,等待
in.markReaderIndex();
int length = in.readInt();
if (in.readableBytes() < length) {
in.resetReaderIndex(); // 数据不够,重置
return;
}
byte[] data = new byte[length];
in.readBytes(data);
GameMessage msg = parseMessage(data);
out.add(msg);
}
}
七、Netty 游戏服务器实战
ProtoBuf 协议
为什么游戏用 ProtoBuf?
| 协议 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON | 调试方便、可读 | 体积大、解析慢 | Web API |
| ProtoBuf | 体积小、解析快 | 不可读、需要schema | 游戏(高频消息) |
| 自定义二进制 | 最灵活、可定制 | 需要自己写编解码 | 特殊需求 |
// game.proto
syntax = "proto3";
package game;
message Player {
int64 id = 1;
string name = 2;
int32 level = 3;
int64 exp = 4;
}
message MoveMsg {
int64 player_id = 1;
float x = 2;
float y = 3;
int32 timestamp = 4;
}
message LoginRequest {
string token = 1;
}
message LoginResponse {
int32 code = 1;
string msg = 2;
Player player = 3;
}
心跳与断线重连
// 心跳处理器
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
private static final int READ_IDLE_TIMEOUT = 30;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
// 读空闲,说明客户端太久没发消息
System.out.println("读空闲,客户端可能挂了,关闭连接");
ctx.close();
}
}
}
}
游戏网关服务器核心代码
// 游戏网关服务器核心代码(简化)
public class GameGatewayServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS))
.addLast(new GameMessageDecoder())
.addLast(new GameMessageEncoder())
.addLast(new GameGatewayHandler());
}
});
ChannelFuture future = bootstrap.bind(8080).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
Netty WebSocket 服务器
// GameServer.java
public class GameServer {
private int port;
public GameServer(int port) {
this.port = port;
}
public void start() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
protected void initChannel(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new GameHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("Game Server started on port " + port);
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
ChannelGroup 管理所有连接
ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 玩家加入
@Override
public void channelActive(ChannelHandlerContext ctx) {
group.add(ctx.channel());
}
// 广播消息给所有人
void broadcast(TextWebSocketFrame msg) {
group.writeAndFlush(msg);
}
八、性能优化与安全
TCP 参数优化
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.option(ChannelOption.SO_BACKLOG, 1024) // 连接队列
.option(ChannelOption.SO_REUSEADDR, true) // 地址复用
.childOption(ChannelOption.SO_KEEPALIVE, true) // TCP保活
.childOption(ChannelOption.TCP_NODELAY, true) // 禁用Nagle
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024) // 发送缓冲
.childOption(ChannelOption.SO_RCVBUF, 32 * 1024); // 接收缓冲
内存池化
// 使用内存池,减少GC压力
bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
抗 DDoS
// 1. IP限流
public class IpRateLimiter extends ChannelInboundHandlerAdapter {
private static Map<String, Counter> ipCounters = new ConcurrentHashMap<>();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String ip = ctx.channel().remoteAddress().toString();
Counter counter = ipCounters.computeIfAbsent(ip, k -> new Counter());
if (counter.increment() > 1000) {
ctx.close();
return;
}
ctx.fireChannelRead(msg);
}
}
// 2. 连接数限制
public class ConnectionLimiter extends ChannelInboundHandlerAdapter {
private static AtomicInteger connections = new AtomicInteger();
private static final int MAX_CONNECTIONS = 100000;
@Override
public void channelActive(ChannelHandlerContext ctx) {
if (connections.incrementAndGet() > MAX_CONNECTIONS) {
ctx.close();
return;
}
ctx.fireChannelActive();
}
}
九、自问自答
Q:NIO 一定比 BIO 好吗? A:不一定。BIO 代码简单,连接数少(<1000)时性能差别不大。NIO 适合连接数多、每个连接数据量少的场景(如游戏服务器)。连接数少但数据量大的场景,BIO 反而更简单有效。
Q:Netty 为什么不用 AIO(异步IO)? A:Netty 曾支持 AIO,但在 Linux 上 AIO 底层仍然用 epoll 模拟,没有真正的性能优势,反而增加了代码复杂度。所以 Netty 4.x 统一用 NIO,在所有平台表现一致。
Q:boss 线程组为什么只需要 1 个线程? A:boss 线程只负责 accept 新连接,这是非常轻量的操作。一个线程每秒可以 accept 数万连接。除非服务端需要同时监听多个端口,否则 1 个 boss 线程足够。
Q:游戏服务器中 Netty 的 EventLoop 数量怎么定? A:一般 worker EventLoop 数量 = CPU 核心数 * 2。因为是 IO 密集型,需要足够的线程来处理网络 IO 等待。但如果业务逻辑也在 EventLoop 中执行,需要更多线程,或者把业务逻辑提交到独立的业务线程池。
Q:零拷贝真的是0次拷贝吗? A:不是。FileRegion从文件到网卡还是2次拷贝(DMA从磁盘到内核,DMA从内核到网卡),只是不经过用户空间。
Q:Netty 的 EventLoop 为什么不要阻塞? A:一个EventLoop管理多个Channel,如果阻塞了,这些Channel的读写都会卡住。
实践任务
- 任务1:用原生 NIO(Channel + Buffer + Selector)实现一个简单的 Echo 服务器
- 任务2:用 Netty 实现同样的 Echo 服务器,对比代码量和性能
- 任务3:实现一个 Protobuf 编解码的 Netty 服务器,发送/接收自定义消息
- 任务4:用 Netty 实现 WebSocket 服务器,支持浏览器客户端连接
- 任务5:给 Netty 服务器添加心跳机制,30 秒无数据自动断开连接
- 任务6:实现 IP 限流和连接数限制,防止 DDoS 攻击
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| NIO 线程模型 | 第02章 并发编程 | EventLoop 本质是线程池 |
| Netty Pipeline | 第04章 Spring 原理 | Pipeline 和 Filter Chain 类似 |
| 零拷贝 | 2_2_h5-rendering-mastery | GPU 纹理上传也有零拷贝概念 |
| 网络通信 | 第11章 游戏服务器架构 | 游戏网关基于 Netty 构建 |
| Protobuf | 第12章 游戏协议设计 | Netty 原生支持 Protobuf 编解码 |
上一章:02-Java并发编程深度 | 下一章:04-Spring核心原理