# 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核心原理