Netty 架构师面试题集锦

Netty 架构师面试题集锦

目录

  1. 基础概念
  2. 核心组件
  3. 线程模型
  4. 内存管理
  5. 编解码器
  6. 高性能原理
  7. 实战问题
  8. 架构设计

基础概念

1. 什么是 Netty?为什么要使用 Netty?

答案:

Netty 是一个异步事件驱动的网络应用框架,用于快速开发高性能、高可靠性的网络服务器和客户端程序。

使用 Netty 的原因:

  1. API 简单易用

    • 封装了 NIO 的复杂性
    • 提供统一的 API,支持多种传输类型(NIO、OIO、Epoll)
  2. 高性能

    • 零拷贝技术(Zero-Copy)
    • 内存池设计
    • 高效的 Reactor 线程模型
    • 无锁化串行设计
  3. 稳定性强

    • 修复了 JDK NIO 的已知 Bug(如 epoll bug)
    • 成熟的断线重连、心跳检测机制
  4. 社区活跃

    • 大量开源项目使用(Dubbo、RocketMQ、Elasticsearch、gRPC)
    • 文档完善,社区支持好

对比原生 NIO:

// 原生 NIO 复杂度高
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// Netty 简化开发
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<>() {
        // 处理逻辑
    });

2. Netty 的应用场景有哪些?

答案:

  1. RPC 框架

    • Dubbo 的网络通信层
    • gRPC 的底层实现
    • Spring Cloud Gateway
  2. 消息中间件

    • RocketMQ 的 Remoting 模块
    • Kafka 的网络层(Scala 实现,但思想类似)
  3. 分布式协调

    • Elasticsearch 的节点通信
    • ZooKeeper 的网络层(虽然原生实现)
  4. 游戏服务器

    • 长连接推送
    • 实时通信
  5. IM 即时通讯

    • WebSocket 聊天室
    • 消息推送系统
  6. 大数据传输

    • Hadoop 的 RPC
    • Spark 的 Shuffle 服务

核心组件

3. 请详细解释 Netty 的核心组件及其作用

答案:

1. Channel(通道)

  • 代表一个网络连接,类似于 Socket
  • 提供异步的网络 I/O 操作
// 主要实现类
NioSocketChannel      // 客户端 TCP Channel
NioServerSocketChannel // 服务端 TCP Channel
NioDatagramChannel    // UDP Channel
EpollSocketChannel    // Linux Epoll 优化

2. EventLoop(事件循环)

  • 处理 I/O 操作的线程
  • 一个 EventLoop 可以服务多个 Channel
  • 一个 Channel 只会绑定一个 EventLoop
EventLoopGroup bossGroup = new NioEventLoopGroup(1);      // 接收连接
EventLoopGroup workerGroup = new NioEventLoopGroup(8);    // 处理 I/O

关系图:

EventLoopGroup (线程池)
    ├── EventLoop (线程1) ──> Channel1, Channel2, Channel3
    ├── EventLoop (线程2) ──> Channel4, Channel5
    └── EventLoop (线程3) ──> Channel6

3. ChannelHandler(处理器)

  • 处理 I/O 事件的核心接口
  • 分为入站(Inbound)和出站(Outbound)
public class MyHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 处理接收到的数据
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 异常处理
        cause.printStackTrace();
        ctx.close();
    }
}

4. ChannelPipeline(管道)

  • Handler 的容器,维护 Handler 链
  • 采用责任链模式
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new MyBusinessHandler());

执行流程:

入站(读数据):head -> handler1 -> handler2 -> tail
出站(写数据):tail -> handler2 -> handler1 -> head

5. ChannelFuture(异步结果)

  • 异步操作的结果占位符
  • 可以添加监听器获取结果
ChannelFuture future = channel.writeAndFlush(msg);
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) {
        if (future.isSuccess()) {
            System.out.println("发送成功");
        } else {
            System.out.println("发送失败");
        }
    }
});

6. ByteBuf(字节缓冲区)

  • Netty 的数据容器,比 NIO 的 ByteBuffer 更强大
  • 支持零拷贝、引用计数
ByteBuf buf = Unpooled.buffer(10);
buf.writeInt(100);
buf.writeBytes("Hello".getBytes());

4. ChannelHandler 的生命周期方法有哪些?执行顺序是什么?

答案:

public class LifecycleHandler extends ChannelInboundHandlerAdapter {
    
    // 1. Handler 被添加到 Pipeline
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        System.out.println("1. handlerAdded");
    }
    
    // 2. Channel 注册到 EventLoop
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) {
        System.out.println("2. channelRegistered");
    }
    
    // 3. Channel 激活(连接建立)
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("3. channelActive");
    }
    
    // 4. 读取数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("4. channelRead");
    }
    
    // 5. 数据读取完成
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        System.out.println("5. channelReadComplete");
    }
    
    // 6. Channel 不活跃(连接断开)
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("6. channelInactive");
    }
    
    // 7. Channel 从 EventLoop 注销
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) {
        System.out.println("7. channelUnregistered");
    }
    
    // 8. Handler 从 Pipeline 移除
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        System.out.println("8. handlerRemoved");
    }
    
    // 异常处理(任何阶段都可能触发)
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.out.println("exceptionCaught: " + cause.getMessage());
    }
}

完整生命周期顺序:

handlerAdded -> channelRegistered -> channelActive 
-> [channelRead -> channelReadComplete]* 
-> channelInactive -> channelUnregistered -> handlerRemoved

5. ctx.write() 和 ctx.channel().write() 有什么区别?

答案:

这是一个非常重要的面试点,涉及到消息传播的起点。

public class DemoHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 方式1:从当前 Handler 的下一个 Handler 开始传播(向前)
        ctx.write(msg);
        
        // 方式2:从 Pipeline 的尾部开始传播(完整链路)
        ctx.channel().write(msg);
    }
}

区别:

方法 起点 适用场景
ctx.write() 当前 Handler 的前一个 Handler 不需要经过前面的 Handler,性能更好
ctx.channel().write() Pipeline 的 tail 需要完整的出站流程

示例:

pipeline.addLast("h1", handler1);
pipeline.addLast("h2", handler2);  // 当前 Handler
pipeline.addLast("h3", handler3);

// 在 handler2 中:
ctx.write(msg);              // 只经过 h1 -> head
ctx.channel().write(msg);    // 经过 h1 -> head

最佳实践:

  • 使用 ctx.write() 性能更好,避免不必要的 Handler 调用
  • 使用 ctx.channel().write() 保证完整性

线程模型

6. 详细说明 Netty 的线程模型(Reactor 模式)

答案:

Netty 采用主从 Reactor 多线程模型,这是目前最成熟的高并发网络模型。

模型演进:

1. 单 Reactor 单线程

EventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(group)  // Boss 和 Worker 使用同一个

缺点:

  • 单线程处理所有操作,性能瓶颈
  • 一个慢操作会阻塞所有连接

2. 单 Reactor 多线程

EventLoopGroup group = new NioEventLoopGroup(); // 默认 CPU*2
ServerBootstrap b = new ServerBootstrap();
b.group(group)

缺点:

  • 一个线程处理 Accept,高并发下仍有瓶颈

3. 主从 Reactor 多线程(推荐)

// Boss 线程池:负责接收连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);

// Worker 线程池:负责 I/O 读写
EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) {
            ch.pipeline().addLast(new YourHandler());
        }
    });

架构图:

                     Client
                       |
                       v
            [Boss EventLoopGroup (1线程)]
                  MainReactor
                       |
            接收连接并分配给 Worker
                       |
        +-------+------+------+-------+
        |       |             |       |
    [Worker EventLoopGroup (N线程)]
    SubReactor1  SR2     SR3    SR4
        |        |       |      |
      多个      多个     多个    多个
    Channel   Channel Channel Channel

工作流程:

  1. BossGroup 职责:

    • 监听端口
    • 接收新连接(Accept)
    • 将连接注册到 WorkerGroup
  2. WorkerGroup 职责:

    • 从 Channel 读取数据
    • 业务处理
    • 将结果写回 Channel
  3. 线程分配原则:

    • 一个 Channel 绑定一个 EventLoop(线程)
    • 一个 EventLoop 可以管理多个 Channel
    • 同一个 Channel 的所有操作都在同一个线程,保证线程安全

7. Netty 如何保证线程安全?

答案:

Netty 通过串行化设计无锁化保证线程安全。

1. Channel 与 EventLoop 的绑定关系

// 核心原则:一个 Channel 只绑定一个 EventLoop
Channel <---绑定---> EventLoop(一个线程)

保证:

  • 同一个 Channel 的所有事件都在同一个线程处理
  • 避免了多线程竞争

2. Handler 的线程安全

// 不安全的写法:多个 Channel 共享同一个 Handler 实例
@ChannelHandler.Sharable  // 标记可共享,但要自己保证线程安全
public class UnsafeHandler extends ChannelInboundHandlerAdapter {
    private int count = 0;  // 共享变量,线程不安全
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        count++;  // 多个线程同时修改
    }
}

// 安全的写法1:不共享 Handler
pipeline.addLast(new SafeHandler());  // 每个 Channel 独立实例

// 安全的写法2:使用 @Sharable + 线程安全措施
@ChannelHandler.Sharable
public class SafeSharedHandler extends ChannelInboundHandlerAdapter {
    private AtomicInteger count = new AtomicInteger(0);  // 原子类
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        count.incrementAndGet();
    }
}

3. 判断是否在 EventLoop 线程

if (ctx.channel().eventLoop().inEventLoop()) {
    // 当前就在 EventLoop 线程,直接执行
    doSomething();
} else {
    // 不在 EventLoop 线程,提交任务
    ctx.channel().eventLoop().execute(() -> {
        doSomething();
    });
}

4. 异步任务提交

// 提交普通任务
ctx.executor().execute(() -> {
    // 耗时操作
});

// 提交定时任务
ctx.executor().schedule(() -> {
    // 定时操作
}, 10, TimeUnit.SECONDS);

总结:

  • 串行无锁:同一 Channel 的操作串行化
  • 空间换时间:每个 Channel 独立状态
  • 异步化:耗时操作异步执行

8. EventLoop 的执行流程是怎样的?

答案:

EventLoop 是 Netty 的核心,实现了事件循环模型

核心代码逻辑:

public final class NioEventLoop extends SingleThreadEventLoop {
    
    @Override
    protected void run() {
        for (;;) {  // 无限循环
            try {
                // 1. 轮询 I/O 事件(阻塞)
                select(wakenUp.getAndSet(false));
                
                // 2. 处理 I/O 事件
                processSelectedKeys();
                
                // 3. 处理异步任务队列
                runAllTasks();
                
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }
}

三大步骤详解:

1. select() - 轮询 I/O 事件

// 阻塞等待事件,但会定期唤醒
// 解决了 JDK epoll 空轮询 Bug
int selectedKeys = selector.select(timeoutMillis);

2. processSelectedKeys() - 处理 I/O 事件

private void processSelectedKeys() {
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    for (SelectionKey key : selectedKeys) {
        if (key.isAcceptable()) {
            // 处理连接事件
        } else if (key.isReadable()) {
            // 处理读事件
        } else if (key.isWritable()) {
            // 处理写事件
        }
    }
}

3. runAllTasks() - 处理任务队列

protected boolean runAllTasks(long timeoutNanos) {
    // 从定时任务队列取出到期任务
    fetchFromScheduledTaskQueue();
    
    // 执行普通任务队列
    Runnable task;
    while ((task = pollTask()) != null) {
        task.run();
        
        // 防止任务执行时间过长
        if (System.nanoTime() - lastExecutionTime >= timeoutNanos) {
            break;
        }
    }
    return true;
}

I/O 事件与任务队列的时间分配:

// ioRatio:I/O 时间占比(默认 50%)
private volatile int ioRatio = 50;

// 如果 I/O 耗时 100ms,那么任务队列也分配 100ms
long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);

流程图:

    ┌─────────────────────┐
    │   EventLoop 启动    │
    └──────────┬──────────┘
               │
               v
    ┌─────────────────────┐
    │   select() 轮询     │ ◄───────┐
    │   等待 I/O 事件     │         │
    └──────────┬──────────┘         │
               │                    │
               v                    │
    ┌─────────────────────┐         │
    │ processSelectedKeys │         │
    │   处理 I/O 事件     │         │
    └──────────┬──────────┘         │
               │                    │
               v                    │
    ┌─────────────────────┐         │
    │   runAllTasks()     │         │
    │   处理任务队列      │         │
    └──────────┬──────────┘         │
               │                    │
               └────────────────────┘

内存管理

9. Netty 的零拷贝(Zero-Copy)是如何实现的?

答案:

Netty 的零拷贝是多维度的优化,不仅仅是操作系统层面的零拷贝。

1. 操作系统层面的零拷贝

传统方式(4次拷贝):

磁盘 -> 内核缓冲区 -> 用户缓冲区 -> Socket 缓冲区 -> 网卡

零拷贝(2次拷贝):

// 使用 FileChannel.transferTo()
FileChannel fileChannel = new FileInputStream(file).getChannel();
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

// 在 Netty 中
DefaultFileRegion fileRegion = new DefaultFileRegion(
    fileChannel, 0, fileChannel.size()
);
ctx.writeAndFlush(fileRegion);

原理:

磁盘 -> 内核缓冲区 -> 网卡(DMA 直接传输)

2. Netty 层面的零拷贝

① CompositeByteBuf - 组合缓冲区

// 传统方式:需要复制数据
ByteBuf header = ...;
ByteBuf body = ...;
ByteBuf merged = Unpooled.buffer(header.readableBytes() + body.readableBytes());
merged.writeBytes(header);  // 拷贝
merged.writeBytes(body);    // 拷贝

// Netty 零拷贝:只是组合引用
CompositeByteBuf composite = Unpooled.compositeBuffer();
composite.addComponents(true, header, body);  // 不拷贝,只是组合

② Slice - 切片

ByteBuf buffer = ...;

// 切分缓冲区,共享底层数据,不拷贝
ByteBuf header = buffer.slice(0, 10);
ByteBuf body = buffer.slice(10, buffer.readableBytes() - 10);

③ Unpooled.wrappedBuffer() - 包装

byte[] bytes = ...;

// 传统方式:拷贝数据
ByteBuf copied = Unpooled.buffer(bytes.length);
copied.writeBytes(bytes);  // 拷贝

// 零拷贝:直接包装
ByteBuf wrapped = Unpooled.wrappedBuffer(bytes);  // 不拷贝

④ 直接内存(Direct Memory)

// 分配堆外内存,减少内核态和用户态的拷贝
ByteBuf directBuffer = Unpooled.directBuffer(1024);

10. ByteBuf 相比 NIO 的 ByteBuffer 有哪些优势?

答案:

特性 ByteBuffer ByteBuf
读写索引 单一 position readerIndex / writerIndex 独立
容量扩展 固定容量 自动扩容
API 设计 flip()/clear() 易错 清晰直观
内存池 不支持 支持池化
引用计数 不支持 支持
零拷贝 不支持 支持多种零拷贝

1. 读写索引分离

// ByteBuffer(复杂)
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("Hello".getBytes());  // 写模式
buffer.flip();                    // 切换到读模式
byte[] bytes = new byte[5];
buffer.get(bytes);                // 读取
buffer.clear();                   // 清空,准备再次写

// ByteBuf(简单)
ByteBuf buf = Unpooled.buffer(10);
buf.writeBytes("Hello".getBytes());  // 写
byte[] bytes = new byte[5];
buf.readBytes(bytes);                // 读,不需要 flip
buf.clear();                         // 清空

ByteBuf 的索引结构:

      +-------------------+------------------+------------------+
      | discardable bytes | readable bytes  |  writable bytes  |
      |    已读数据        |    可读数据      |    可写空间      |
      +-------------------+------------------+------------------+
      |                   |                  |                  |
      0      <=    readerIndex   <=   writerIndex    <=    capacity

2. 自动扩容

ByteBuf buf = Unpooled.buffer(10);  // 初始容量 10
buf.writeBytes(new byte[20]);        // 自动扩容到 20+

3. 引用计数

ByteBuf buf = Unpooled.buffer(10);
buf.retain();         // 引用计数 +1
buf.release();        // 引用计数 -1
buf.release();        // 引用计数 = 0,释放内存

// 检查是否已释放
if (buf.refCnt() == 0) {
    // 已释放
}

为什么需要引用计数?

  • 控制 ByteBuf 的生命周期
  • 及时回收内存(特别是直接内存)
  • 避免内存泄漏

4. 丰富的操作方法

ByteBuf buf = Unpooled.buffer(100);

// 标记和重置
buf.markReaderIndex();
buf.readInt();
buf.resetReaderIndex();  // 回到标记位置

// 查找
int index = buf.indexOf(0, 50, (byte)'\n');

// 派生缓冲区
ByteBuf slice = buf.slice(0, 10);      // 切片(共享数据)
ByteBuf duplicate = buf.duplicate();   // 复制(共享数据,独立索引)
ByteBuf copy = buf.copy();             // 深拷贝(独立数据)

11. Netty 的内存池是如何设计的?

答案:

Netty 的内存池(PooledByteBufAllocator)是一个高性能的内存管理器,借鉴了 jemalloc 的设计思想。

核心概念:

1. 内存规格分类

Tiny:   < 512B      (16B, 32B, 48B, ..., 496B)
Small:  512B - 8KB  (512B, 1KB, 2KB, 4KB, 8KB)
Normal: 8KB - 16MB  (8KB, 16KB, 32KB, ..., 16MB)
Huge:   > 16MB      (不池化,直接分配)

2. 内存块结构

Arena(竞技场)
  ├── PoolChunkList(块链表)
  │     ├── PoolChunk(16MB 大块)
  │     │     ├── Page(8KB 页)
  │     │     └── Subpage(< 8KB 子页)
  │     └── ...
  └── PoolThreadCache(线程缓存)
        ├── Tiny 缓存
        ├── Small 缓存
        └── Normal 缓存

3. 分配流程

// 开启池化
ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
ByteBuf buf = allocator.buffer(1024);  // 分配 1KB

// 关闭池化
ByteBufAllocator allocator = UnpooledByteBufAllocator.DEFAULT;

分配策略:

1. 先从线程缓存(ThreadCache)获取
2. 线程缓存未命中,从 Arena 分配
3. Arena 按内存规格找到对应的 PoolChunkList
4. 从 PoolChunk 中分配内存
5. 分配成功后,缓存一部分到 ThreadCache

为什么使用内存池?

优点:

  1. 减少 GC 压力 - 复用对象,减少频繁创建
  2. 减少内存碎片 - 按规格分配
  3. 提高分配速度 - 线程缓存,无锁化

缺点:

  1. 内存占用更大(预分配)
  2. 代码复杂度高
  3. 需要手动管理引用计数

使用建议:

// 高并发场景,推荐使用池化
ServerBootstrap b = new ServerBootstrap();
b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

// 低并发场景,可以不池化
b.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);

12. Netty 中的内存泄漏如何检测和避免?

答案:

内存泄漏是 Netty 使用中最常见的问题,特别是 Direct Memory 泄漏。

1. 内存泄漏的原因

// 错误示例1:没有释放 ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    // 处理数据...
    // 忘记释放!!!
}

// 错误示例2:提前返回,未释放
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    if (someCondition) {
        return;  // 提前返回,未释放
    }
    buf.release();
}

// 错误示例3:异常时未释放
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    processData(buf);  // 可能抛异常
    buf.release();     // 异常时不会执行
}

2. 正确的释放方式

方式1:手动释放

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    try {
        // 处理数据
        processData(buf);
    } finally {
        buf.release();  // 确保释放
    }
}

方式2:使用 SimpleChannelInboundHandler

// 自动释放
public class AutoReleaseHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
        // 处理数据,框架自动释放 msg
        processData(msg);
    }
}

方式3:使用 ReferenceCountUtil

import io.netty.util.ReferenceCountUtil;

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        // 处理数据
    } finally {
        ReferenceCountUtil.release(msg);  // 安全释放
    }
}

3. 内存泄漏检测

开启泄漏检测:

// 方式1:代码设置
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);

// 方式2:JVM 参数
-Dio.netty.leakDetection.level=paranoid

检测级别:

DISABLED  // 关闭(生产环境)
SIMPLE    // 1% 采样,默认
ADVANCED  // 1% 采样 + 详细信息
PARANOID  // 100% 采样(性能影响大,仅用于测试)

泄漏日志示例:

LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records:
#1:
  io.netty.buffer.AdvancedLeakAwareByteBuf.writeBytes(AdvancedLeakAwareByteBuf.java:100)
  com.example.MyHandler.channelRead(MyHandler.java:50)

4. 释放规则

黄金法则:谁创建谁释放,谁传递谁负责

// 规则1:不传递,需要释放
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    // 没有传递给下一个 Handler
    buf.release();  // 必须释放
}

// 规则2:传递了,不需要释放
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // 传递给下一个 Handler,由下一个负责释放
    ctx.fireChannelRead(msg);
}

// 规则3:转换后传递,原消息需要释放
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf in = (ByteBuf) msg;
    ByteBuf out = ctx.alloc().buffer();
    
    // 转换数据
    out.writeBytes(transform(in));
    
    in.release();   // 释放原消息
    ctx.fireChannelRead(out);  // 传递新消息
}

编解码器

13. Netty 中常用的编解码器有哪些?如何自定义?

答案:

1. 内置编解码器

① 字符串编解码

pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));

② 行分隔符

// 按行分割,解决粘包问题
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder());

③ 固定长度

// 每次读取固定长度
pipeline.addLast(new FixedLengthFrameDecoder(20));

④ 分隔符

// 自定义分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("$$".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, delimiter));

⑤ 长度字段

// 最常用:基于长度字段的解码器
// 消息格式:[长度][数据]
pipeline.addLast(new LengthFieldBasedFrameDecoder(
    1024,    // 最大帧长度
    0,       // 长度字段偏移量
    4,       // 长度字段长度(4字节=int)
    0,       // 长度调整值
    4        // 跳过的字节数(跳过长度字段本身)
));
pipeline.addLast(new LengthFieldPrepender(4));  // 编码器

⑥ 对象序列化(不推荐)

// JDK 序列化(性能差,不推荐)
pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
pipeline.addLast(new ObjectEncoder());

⑦ Protobuf

pipeline.addLast(new ProtobufVarint32FrameDecoder());
pipeline.addLast(new ProtobufDecoder(MyMessage.getDefaultInstance()));
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
pipeline.addLast(new ProtobufEncoder());

2. 自定义编解码器

方式1:继承 ByteToMessageDecoder

/**
 * 自定义协议:
 * +--------+--------+----------+
 * | 魔数   | 长度   |   数据    |
 * | 2字节  | 4字节  | N字节     |
 * +--------+--------+----------+
 */
public class MyProtocolDecoder extends ByteToMessageDecoder {
    
    private static final int HEADER_SIZE = 6;  // 2 + 4
    private static final short MAGIC = (short) 0xCAFE;
    
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        // 1. 检查可读字节数
        if (in.readableBytes() < HEADER_SIZE) {
            return;  // 数据不够,等待更多数据
        }
        
        // 2. 标记读指针
        in.markReaderIndex();
        
        // 3. 读取魔数
        short magic = in.readShort();
        if (magic != MAGIC) {
            throw new IllegalStateException("Invalid magic: " + magic);
        }
        
        // 4. 读取长度
        int length = in.readInt();
        
        // 5. 检查数据是否完整
        if (in.readableBytes() < length) {
            in.resetReaderIndex();  // 重置读指针
            return;  // 数据不完整,等待
        }
        
        // 6. 读取数据
        byte[] data = new byte[length];
        in.readBytes(data);
        
        // 7. 解码完成,添加到结果
        MyMessage message = new MyMessage(data);
        out.add(message);
    }
}

方式2:继承 MessageToByteEncoder

public class MyProtocolEncoder extends MessageToByteEncoder<MyMessage> {
    
    private static final short MAGIC = (short) 0xCAFE;
    
    @Override
    protected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) {
        byte[] data = msg.getData();
        
        // 1. 写入魔数
        out.writeShort(MAGIC);
        
        // 2. 写入长度
        out.writeInt(data.length);
        
        // 3. 写入数据
        out.writeBytes(data);
    }
}

方式3:继承 MessageToMessageDecoder(消息到消息)

// 将 HTTP 请求转换为自定义对象
public class HttpToCustomDecoder extends MessageToMessageDecoder<FullHttpRequest> {
    
    @Override
    protected void decode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) {
        String uri = msg.uri();
        ByteBuf content = msg.content();
        
        MyCustomMessage custom = new MyCustomMessage(uri, content);
        out.add(custom);
    }
}

3. 编解码器最佳实践

① 解决粘包拆包

/**
 * TCP 粘包拆包问题:
 * 
 * 粘包:多个消息粘在一起
 * [msg1][msg2] -> [msg1msg2]
 * 
 * 拆包:一个消息被拆成多个
 * [msg1] -> [ms][g1]
 */

// 解决方案1:固定长度
pipeline.addLast(new FixedLengthFrameDecoder(100));

// 解决方案2:分隔符
pipeline.addLast(new LineBasedFrameDecoder(1024));

// 解决方案3:长度字段(推荐)
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));

② 编解码器顺序

// 正确的顺序
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(...));
pipeline.addLast("protobufDecoder", new ProtobufDecoder(...));
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("protobufEncoder", new ProtobufEncoder());
pipeline.addLast("businessHandler", new BusinessHandler());

顺序原则:

  • 解码:字节 -> 帧 -> 消息 -> 业务对象
  • 编码:业务对象 -> 消息 -> 帧 -> 字节

14. 如何处理半包和粘包问题?

答案:

TCP 是流式协议,没有消息边界,需要应用层自己处理。

问题演示:

// 发送方发送
send("Hello");
send("World");

// 接收方可能收到
"HelloWorld"       // 粘包
"Hel" + "loWorld"  // 拆包

解决方案:

方案1:固定长度

// 每条消息固定 100 字节,不足补空格
pipeline.addLast(new FixedLengthFrameDecoder(100));

// 优点:实现简单
// 缺点:浪费带宽

方案2:分隔符

// 使用 \n 或自定义分隔符
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder());

// 优点:节省带宽
// 缺点:数据中不能包含分隔符,需要转义

方案3:长度字段(最常用)

/**
 * 协议格式:
 * +--------+----------+
 * | Length |   Data   |
 * | 4 bytes| N bytes  |
 * +--------+----------+
 */
pipeline.addLast(new LengthFieldBasedFrameDecoder(
    65535,  // maxFrameLength: 最大帧长度
    0,      // lengthFieldOffset: 长度字段偏移量
    4,      // lengthFieldLength: 长度字段的字节数
    0,      // lengthAdjustment: 长度调整值
    4       // initialBytesToStrip: 跳过的字节数
));

// 发送时自动添加长度
pipeline.addLast(new LengthFieldPrepender(4));

lengthFieldOffset 示例:

// 场景:长度字段不在开头
/**
 * +--------+--------+----------+
 * | Magic  | Length |   Data   |
 * | 2 bytes| 4 bytes| N bytes  |
 * +--------+--------+----------+
 */
new LengthFieldBasedFrameDecoder(
    65535,
    2,      // lengthFieldOffset: 长度字段从第2个字节开始
    4,
    0,
    6       // 跳过魔数和长度字段
);

lengthAdjustment 示例:

// 场景:长度字段的值 = 整个消息的长度
/**
 * +--------+----------+
 * | Length |   Data   |
 * | 4 bytes| N bytes  |
 * +--------+----------+
 * Length 的值 = 4 + N (包含自己)
 */
new LengthFieldBasedFrameDecoder(
    65535,
    0,
    4,
    -4,     // lengthAdjustment: 长度字段值需要减去 4
    4
);

高性能原理

15. Netty 高性能的核心原理有哪些?

答案:

1. I/O 模型

  • Reactor 模式:主从多线程模型
  • NIO Selector:多路复用,单线程管理多连接
  • Epoll 优化(Linux):更高效的事件通知
// Linux 下使用 Epoll
EventLoopGroup group = new EpollEventLoopGroup();
bootstrap.channel(EpollServerSocketChannel.class);

2. 零拷贝

  • Direct Memory:减少内核态和用户态拷贝
  • CompositeByteBuf:逻辑组合,避免物理拷贝
  • FileRegion:sendfile 系统调用

3. 内存池

  • PooledByteBufAllocator:减少 GC,提高分配速度
  • 线程缓存:无锁分配
  • Slab 算法:减少内存碎片

4. 无锁化设计

  • 串行化:Channel 绑定 EventLoop
  • ThreadLocal:线程隔离
  • CAS 操作:原子操作
// 同一个 Channel 的操作都在同一线程,无需加锁
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // 这里不需要加锁,线程安全
    this.count++;
}

5. 高效的数据结构

  • FastThreadLocal:比 JDK ThreadLocal 更快
  • Recycler:对象池,复用对象
  • MpscQueue:无锁队列(多生产者单消费者)

6. 减少系统调用

  • 批量 flush:减少 write 系统调用
  • WriteBufferWaterMark:流控
// 不是每次都 flush
ctx.write(msg1);
ctx.write(msg2);
ctx.writeAndFlush(msg3);  // 最后一次才 flush

7. JVM 优化

  • 避免频繁 GC:对象池、内存池
  • 逃逸分析:栈上分配
  • 方法内联:简化调用栈

性能数据对比:

           QPS       延迟(ms)    CPU(%)
BIO        1万       100         80%
NIO        5万       20          60%
Netty      10万+     5           40%

16. Netty 如何解决 JDK NIO 的 Epoll Bug?

答案:

问题描述:

JDK NIO 在 Linux 下有一个臭名昭著的 Epoll Bug:Selector 会空轮询,导致 CPU 100%。

// 正常情况:没有事件时,select() 会阻塞
int n = selector.select();  // 阻塞等待

// Bug 情况:没有事件,但 select() 立即返回 0
// 导致死循环,CPU 100%
while (true) {
    int n = selector.select();  // 立即返回 0
    // 处理事件...
}

Bug 原因:

  • Linux 内核 Bug,某些情况下 epoll 会错误唤醒
  • 发生在连接断开、网络闪断等场景

Netty 的解决方案:

检测机制:

public final class NioEventLoop extends SingleThreadEventLoop {
    
    private int selectCnt = 0;  // 记录连续空轮询次数
    private static final int SELECTOR_AUTO_REBUILD_THRESHOLD = 512;
    
    @Override
    protected void run() {
        for (;;) {
            int selectedKeys = selector.select(timeoutMillis);
            selectCnt++;
            
            if (selectedKeys != 0 || hasTask()) {
                // 有事件或任务,重置计数
                selectCnt = 0;
            } else if (unexpectedSelectorWakeup(selectCnt)) {
                // 发生空轮询 Bug,重建 Selector
                rebuildSelector();
                selectCnt = 0;
            }
            
            // 处理事件...
        }
    }
    
    private boolean unexpectedSelectorWakeup(int selectCnt) {
        // 连续 512 次空轮询,判定为 Bug
        return selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD;
    }
    
    private void rebuildSelector() {
        // 1. 创建新的 Selector
        Selector newSelector = Selector.open();
        
        // 2. 将所有 Channel 注册到新 Selector
        for (SelectionKey key : oldSelector.keys()) {
            Channel channel = (Channel) key.attachment();
            channel.register(newSelector, key.interestOps());
        }
        
        // 3. 替换旧 Selector
        oldSelector.close();
        this.selector = newSelector;
        
        logger.info("Rebuilt Selector to fix epoll bug");
    }
}

解决思路:

  1. 记录连续空轮询次数
  2. 超过阈值(512次),判定为 Bug
  3. 重建 Selector
  4. 迁移所有注册的 Channel

17. Netty 的 FastThreadLocal 比 JDK ThreadLocal 快在哪里?

答案:

JDK ThreadLocal 的实现:

// JDK ThreadLocal 使用 ThreadLocalMap 存储
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("value");

// 底层结构
Thread {
    ThreadLocalMap threadLocals;  // Map<ThreadLocal, Object>
}

// 查找过程:需要哈希
int hash = threadLocal.hashCode();
Entry entry = table[hash & (table.length - 1)];
while (entry.get() != threadLocal) {
    entry = table[nextIndex(i, len)];  // 线性探测
}

性能问题:

  1. 哈希计算
  2. 哈希冲突时线性探测
  3. 弱引用导致的清理开销

Netty FastThreadLocal 的实现:

// FastThreadLocal 使用数组 + 索引
FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<>();
fastThreadLocal.set("value");

// 底层结构
FastThreadLocalThread {
    Object[] threadLocalMap;  // 直接用数组
}

// 查找过程:O(1) 直接访问
int index = fastThreadLocal.index;  // 每个 FTL 有唯一索引
return threadLocalMap[index];

优化点:

1. 数组代替哈希表

// JDK:哈希查找 O(1) ~ O(n)
map.get(key);

// Netty:数组索引 O(1)
array[index];

2. 避免哈希冲突

public class FastThreadLocal<V> {
    private final int index;  // 全局唯一索引
    
    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }
}

3. 避免弱引用

// JDK ThreadLocal 使用弱引用,需要清理
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
}

// FastThreadLocal 使用强引用,不需要清理
Object[] array;  // 直接存储值

性能对比:

操作          ThreadLocal    FastThreadLocal
set()         ~100ns        ~10ns
get()         ~50ns         ~5ns
remove()      ~100ns        ~10ns

使用条件:

// 必须使用 FastThreadLocalThread
Thread thread = new FastThreadLocalThread(() -> {
    FastThreadLocal<String> ftl = new FastThreadLocal<>();
    ftl.set("value");
});

// Netty 的 EventLoop 默认使用 FastThreadLocalThread

实战问题

18. Netty 中如何实现心跳检测和断线重连?

答案:

1. 心跳检测

服务端实现:

public class HeartbeatServer {
    
    public void start() throws Exception {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ChannelPipeline pipeline = ch.pipeline();
                    
                    // 核心:IdleStateHandler
                    // readerIdleTime: 读超时时间
                    // writerIdleTime: 写超时时间
                    // allIdleTime: 读写超时时间
                    pipeline.addLast(new IdleStateHandler(
                        60,  // 60秒没收到数据,触发读空闲
                        0,   // 不检测写空闲
                        0,   // 不检测读写空闲
                        TimeUnit.SECONDS
                    ));
                    
                    // 心跳处理器
                    pipeline.addLast(new HeartbeatServerHandler());
                }
            });
    }
}

public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {
    
    private static final ByteBuf HEARTBEAT_RESPONSE = 
        Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("PONG", CharsetUtil.UTF_8));
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        String message = buf.toString(CharsetUtil.UTF_8);
        
        // 收到心跳请求,回复 PONG
        if ("PING".equals(message)) {
            ctx.writeAndFlush(HEARTBEAT_RESPONSE.duplicate());
        } else {
            // 业务消息
            ctx.fireChannelRead(msg);
        }
    }
    
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            
            if (event.state() == IdleState.READER_IDLE) {
                // 60秒没收到客户端数据,认为客户端死了
                System.out.println("客户端心跳超时,关闭连接: " + ctx.channel().remoteAddress());
                ctx.close();
            }
        }
    }
}

客户端实现:

public class HeartbeatClient {
    
    public void connect() throws Exception {
        Bootstrap b = new Bootstrap();
        b.group(group)
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ChannelPipeline pipeline = ch.pipeline();
                    
                    // 30秒没写数据,发送心跳
                    pipeline.addLast(new IdleStateHandler(
                        0,   // 不检测读空闲
                        30,  // 30秒没发送数据,触发写空闲
                        0,
                        TimeUnit.SECONDS
                    ));
                    
                    pipeline.addLast(new HeartbeatClientHandler());
                }
            });
    }
}

public class HeartbeatClientHandler extends ChannelInboundHandlerAdapter {
    
    private static final ByteBuf HEARTBEAT_REQUEST = 
        Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("PING", CharsetUtil.UTF_8));
    
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            
            if (event.state() == IdleState.WRITER_IDLE) {
                // 30秒没发送数据,发送心跳
                ctx.writeAndFlush(HEARTBEAT_REQUEST.duplicate());
                System.out.println("发送心跳: PING");
            }
        }
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        String message = buf.toString(CharsetUtil.UTF_8);
        
        if ("PONG".equals(message)) {
            System.out.println("收到心跳响应: PONG");
        } else {
            // 业务消息
            ctx.fireChannelRead(msg);
        }
    }
}

2. 断线重连

public class ReconnectClient {
    
    private Bootstrap bootstrap;
    private EventLoopGroup group;
    private volatile Channel channel;
    private final String host;
    private final int port;
    
    public ReconnectClient(String host, int port) {
        this.host = host;
        this.port = port;
    }
    
    public void start() {
        group = new NioEventLoopGroup();
        bootstrap = new Bootstrap();
        bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.SO_KEEPALIVE, true)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(new ReconnectHandler(ReconnectClient.this));
                    ch.pipeline().addLast(new BusinessHandler());
                }
            });
        
        connect();
    }
    
    public void connect() {
        ChannelFuture future = bootstrap.connect(host, port);
        future.addListener((ChannelFutureListener) f -> {
            if (f.isSuccess()) {
                channel = f.channel();
                System.out.println("连接成功");
            } else {
                System.out.println("连接失败,5秒后重连...");
                scheduleReconnect();
            }
        });
    }
    
    private void scheduleReconnect() {
        group.schedule(() -> {
            System.out.println("重新连接...");
            connect();
        }, 5, TimeUnit.SECONDS);
    }
    
    public void send(String message) {
        if (channel != null && channel.isActive()) {
            channel.writeAndFlush(message);
        } else {
            System.out.println("连接未建立,消息发送失败");
        }
    }
}

/**
 * 重连处理器
 */
@ChannelHandler.Sharable
public class ReconnectHandler extends ChannelInboundHandlerAdapter {
    
    private final ReconnectClient client;
    private int retryCount = 0;
    private static final int MAX_RETRY = 10;
    
    public ReconnectHandler(ReconnectClient client) {
        this.client = client;
    }
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("连接建立,重置重试次数");
        retryCount = 0;
        ctx.fireChannelActive();
    }
    
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("连接断开");
        
        if (retryCount < MAX_RETRY) {
            retryCount++;
            int delay = Math.min(retryCount * 2, 60);  // 指数退避,最大60秒
            System.out.println("第" + retryCount + "次重连," + delay + "秒后...");
            
            ctx.channel().eventLoop().schedule(() -> {
                client.connect();
            }, delay, TimeUnit.SECONDS);
        } else {
            System.out.println("达到最大重试次数,放弃重连");
        }
        
        ctx.fireChannelInactive();
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.out.println("发生异常: " + cause.getMessage());
        ctx.close();  // 关闭连接,触发 channelInactive
    }
}

重连策略:

  1. 固定延迟:每次间隔固定时间(如 5 秒)
  2. 指数退避:延迟时间指数增长(2s, 4s, 8s, 16s...)
  3. 限制次数:超过最大次数后放弃
  4. 抖动:添加随机延迟,避免雷鸣羊群效应

19. Netty 中如何实现流控(背压)?

答案:

流控(Flow Control)用于防止生产者速度过快,导致消费者处理不过来。

1. 自动流控:WriteBufferWaterMark

ServerBootstrap b = new ServerBootstrap();
b.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, 
    new WriteBufferWaterMark(
        32 * 1024,   // 低水位:32KB
        64 * 1024    // 高水位:64KB
    )
);

工作原理:

写缓冲区大小
    ^
    |
64KB├─────────────────────  高水位(不可写)
    |  ▲ setAutoRead(false)
    |  │
    |  │ 暂停读取
    |  │
32KB├─────────────────────  低水位(可写)
    |  │ setAutoRead(true)
    |  ▼
    └─────────────────────>

示例:

public class FlowControlHandler extends ChannelInboundHandlerAdapter {
    
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) {
        if (ctx.channel().isWritable()) {
            System.out.println("缓冲区可写,恢复读取");
            ctx.channel().config().setAutoRead(true);
        } else {
            System.out.println("缓冲区满,暂停读取");
            ctx.channel().config().setAutoRead(false);
        }
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 发送数据
        ChannelFuture future = ctx.writeAndFlush(msg);
        
        // 检查是否可写
        if (!ctx.channel().isWritable()) {
            System.out.println("缓冲区满,暂停读取");
            ctx.channel().config().setAutoRead(false);
            
            // 等待缓冲区可写
            future.addListener((ChannelFutureListener) f -> {
                if (ctx.channel().isWritable()) {
                    System.out.println("缓冲区可写,恢复读取");
                    ctx.channel().config().setAutoRead(true);
                }
            });
        }
    }
}

2. 手动流控:流量整形

// 限流:每秒最多 1MB
GlobalTrafficShapingHandler trafficHandler = new GlobalTrafficShapingHandler(
    group,
    1024 * 1024,  // 写限速:1MB/s
    1024 * 1024   // 读限速:1MB/s
);

pipeline.addFirst("traffic", trafficHandler);

流量整形类型:

  1. ChannelTrafficShapingHandler:单 Channel 限流
  2. GlobalTrafficShapingHandler:全局限流
  3. GlobalChannelTrafficShapingHandler:全局 + 单 Channel 限流

20. 如何在 Netty 中实现 WebSocket?

答案:

public class WebSocketServer {
    
    public void start(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        
                        // 1. HTTP 编解码器
                        pipeline.addLast(new HttpServerCodec());
                        
                        // 2. HTTP 聚合器(将 HTTP 消息聚合成 FullHttpRequest)
                        pipeline.addLast(new HttpObjectAggregator(65536));
                        
                        // 3. HTTP 压缩
                        pipeline.addLast(new HttpContentCompressor());
                        
                        // 4. WebSocket 协议处理器
                        pipeline.addLast(new WebSocketServerProtocolHandler(
                            "/ws",        // WebSocket 路径
                            null,         // 子协议
                            true,         // 允许扩展
                            65536,        // 最大帧大小
                            false,        // 允许 Mask
                            true,         // 发送 Close 帧
                            10000L        // 握手超时时间
                        ));
                        
                        // 5. 业务处理器
                        pipeline.addLast(new WebSocketHandler());
                    }
                });
            
            ChannelFuture f = b.bind(port).sync();
            System.out.println("WebSocket 服务启动: ws://localhost:" + port + "/ws");
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 文本消息
        if (frame instanceof TextWebSocketFrame) {
            String text = ((TextWebSocketFrame) frame).text();
            System.out.println("收到文本消息: " + text);
            
            // 回复消息
            ctx.channel().writeAndFlush(
                new TextWebSocketFrame("服务器收到: " + text)
            );
        }
        // 二进制消息
        else if (frame instanceof BinaryWebSocketFrame) {
            ByteBuf content = frame.content();
            System.out.println("收到二进制消息: " + content.readableBytes() + " 字节");
            
            ctx.channel().writeAndFlush(
                new BinaryWebSocketFrame(content.retain())
            );
        }
        // Ping 消息
        else if (frame instanceof PingWebSocketFrame) {
            ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
        }
        // Pong 消息
        else if (frame instanceof PongWebSocketFrame) {
            System.out.println("收到 Pong");
        }
        // Close 消息
        else if (frame instanceof CloseWebSocketFrame) {
            System.out.println("收到关闭帧");
            ctx.channel().close();
        }
        else {
            throw new UnsupportedOperationException("不支持的帧类型: " + frame.getClass().getName());
        }
    }
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        System.out.println("客户端连接: " + ctx.channel().id());
    }
    
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        System.out.println("客户端断开: " + ctx.channel().id());
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端(JavaScript):

const ws = new WebSocket('ws://localhost:8080/ws');

ws.onopen = () => {
    console.log('连接成功');
    ws.send('Hello Server');
};

ws.onmessage = (event) => {
    console.log('收到消息:', event.data);
};

ws.onerror = (error) => {
    console.error('发生错误:', error);
};

ws.onclose = () => {
    console.log('连接关闭');
};

架构设计

21. 如何设计一个基于 Netty 的 IM 系统?

答案:

系统架构:

客户端
  │
  ├─ WebSocket/TCP
  │
  ▼
┌─────────────────┐
│  接入层 (Gateway)│  ◄─ Netty 服务器
│  - 连接管理      │
│  - 协议转换      │
│  - 负载均衡      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   业务层 (Logic) │
│  - 消息路由      │
│  - 在线状态      │
│  - 消息存储      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  存储层 (Storage)│
│  - Redis: 在线   │
│  - MySQL: 消息   │
│  - MongoDB: 历史 │
└─────────────────┘

核心组件设计:

1. 连接管理器

/**
 * 管理所有在线用户的 Channel
 */
public class UserChannelManager {
    
    // userId -> Channel 映射
    private static final ConcurrentHashMap<String, Channel> USER_CHANNELS = 
        new ConcurrentHashMap<>();
    
    // Channel -> userId 映射
    private static final ConcurrentHashMap<Channel, String> CHANNEL_USERS = 
        new ConcurrentHashMap<>();
    
    /**
     * 用户上线
     */
    public static void online(String userId, Channel channel) {
        // 旧连接下线
        Channel oldChannel = USER_CHANNELS.get(userId);
        if (oldChannel != null && oldChannel.isActive()) {
            oldChannel.close();
        }
        
        USER_CHANNELS.put(userId, channel);
        CHANNEL_USERS.put(channel, userId);
        
        System.out.println("用户上线: " + userId);
    }
    
    /**
     * 用户下线
     */
    public static void offline(Channel channel) {
        String userId = CHANNEL_USERS.remove(channel);
        if (userId != null) {
            USER_CHANNELS.remove(userId);
            System.out.println("用户下线: " + userId);
        }
    }
    
    /**
     * 获取用户的 Channel
     */
    public static Channel getChannel(String userId) {
        return USER_CHANNELS.get(userId);
    }
    
    /**
     * 判断用户是否在线
     */
    public static boolean isOnline(String userId) {
        Channel channel = USER_CHANNELS.get(userId);
        return channel != null && channel.isActive();
    }
    
    /**
     * 获取在线用户数
     */
    public static int getOnlineCount() {
        return USER_CHANNELS.size();
    }
}

2. 消息协议

/**
 * 消息协议
 * +------+------+------+--------+
 * | 魔数 | 版本 | 类型 | 长度   | 数据
 * | 2B   | 1B   | 1B   | 4B     | N字节
 * +------+------+------+--------+
 */
public class IMMessage {
    private short magic = 0xCAFE;       // 魔数
    private byte version = 1;            // 版本
    private MessageType type;            // 消息类型
    private int length;                  // 数据长度
    private byte[] data;                 // 消息体
    
    public enum MessageType {
        LOGIN(1),           // 登录
        LOGOUT(2),          // 登出
        PING(3),            // 心跳
        PONG(4),            // 心跳响应
        P2P_MESSAGE(5),     // 单聊消息
        GROUP_MESSAGE(6),   // 群聊消息
        ACK(7);             // 消息确认
        
        private final byte code;
        MessageType(int code) {
            this.code = (byte) code;
        }
    }
}

/**
 * 消息体
 */
public class ChatMessage {
    private String msgId;        // 消息ID
    private String from;         // 发送者
    private String to;           // 接收者(用户ID或群ID)
    private String content;      // 消息内容
    private long timestamp;      // 时间戳
    private MessageContentType contentType;  // 内容类型
    
    public enum MessageContentType {
        TEXT,      // 文本
        IMAGE,     // 图片
        VIDEO,     // 视频
        FILE       // 文件
    }
}

3. 消息处理器

public class IMServerHandler extends SimpleChannelInboundHandler<IMMessage> {
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, IMMessage msg) {
        MessageType type = msg.getType();
        
        switch (type) {
            case LOGIN:
                handleLogin(ctx, msg);
                break;
            case LOGOUT:
                handleLogout(ctx, msg);
                break;
            case PING:
                handlePing(ctx, msg);
                break;
            case P2P_MESSAGE:
                handleP2PMessage(ctx, msg);
                break;
            case GROUP_MESSAGE:
                handleGroupMessage(ctx, msg);
                break;
            case ACK:
                handleAck(ctx, msg);
                break;
            default:
                System.out.println("未知消息类型: " + type);
        }
    }
    
    /**
     * 处理登录
     */
    private void handleLogin(ChannelHandlerContext ctx, IMMessage msg) {
        // 解析登录数据
        LoginRequest request = JSON.parseObject(msg.getData(), LoginRequest.class);
        
        // 验证 Token
        if (!validateToken(request.getToken())) {
            ctx.close();
            return;
        }
        
        // 用户上线
        String userId = request.getUserId();
        UserChannelManager.online(userId, ctx.channel());
        
        // 发送登录响应
        LoginResponse response = new LoginResponse(true, "登录成功");
        IMMessage respMsg = new IMMessage(MessageType.LOGIN, JSON.toJSONString(response));
        ctx.writeAndFlush(respMsg);
        
        // 拉取离线消息
        pullOfflineMessages(userId, ctx.channel());
    }
    
    /**
     * 处理单聊消息
     */
    private void handleP2PMessage(ChannelHandlerContext ctx, IMMessage msg) {
        ChatMessage chatMsg = JSON.parseObject(msg.getData(), ChatMessage.class);
        String toUserId = chatMsg.getTo();
        
        // 1. 保存消息到数据库
        saveMessage(chatMsg);
        
        // 2. 发送 ACK 给发送者
        sendAck(ctx.channel(), chatMsg.getMsgId());
        
        // 3. 判断接收者是否在线
        Channel toChannel = UserChannelManager.getChannel(toUserId);
        if (toChannel != null && toChannel.isActive()) {
            // 在线,直接推送
            IMMessage pushMsg = new IMMessage(MessageType.P2P_MESSAGE, msg.getData());
            toChannel.writeAndFlush(pushMsg);
        } else {
            // 离线,存储为离线消息
            saveOfflineMessage(toUserId, chatMsg);
        }
    }
    
    /**
     * 处理群聊消息
     */
    private void handleGroupMessage(ChannelHandlerContext ctx, IMMessage msg) {
        ChatMessage chatMsg = JSON.parseObject(msg.getData(), ChatMessage.class);
        String groupId = chatMsg.getTo();
        
        // 1. 保存消息
        saveMessage(chatMsg);
        
        // 2. 发送 ACK
        sendAck(ctx.channel(), chatMsg.getMsgId());
        
        // 3. 获取群成员
        List<String> members = getGroupMembers(groupId);
        
        // 4. 推送给所有在线成员
        IMMessage pushMsg = new IMMessage(MessageType.GROUP_MESSAGE, msg.getData());
        for (String memberId : members) {
            if (memberId.equals(chatMsg.getFrom())) {
                continue;  // 跳过发送者
            }
            
            Channel memberChannel = UserChannelManager.getChannel(memberId);
            if (memberChannel != null && memberChannel.isActive()) {
                memberChannel.writeAndFlush(pushMsg);
            } else {
                // 离线,存储为离线消息
                saveOfflineMessage(memberId, chatMsg);
            }
        }
    }
    
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        // 用户断开连接
        UserChannelManager.offline(ctx.channel());
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

4. 消息可靠性保证

/**
 * 消息确认机制
 */
public class MessageAckManager {
    
    // 等待 ACK 的消息(msgId -> Message)
    private static final ConcurrentHashMap<String, PendingMessage> PENDING_MESSAGES = 
        new ConcurrentHashMap<>();
    
    /**
     * 发送消息,等待 ACK
     */
    public static void sendWithAck(Channel channel, IMMessage message, 
                                   int retryTimes, long timeoutMs) {
        String msgId = message.getMsgId();
        
        // 记录待确认消息
        PendingMessage pending = new PendingMessage(channel, message, retryTimes);
        PENDING_MESSAGES.put(msgId, pending);
        
        // 发送消息
        channel.writeAndFlush(message);
        
        // 设置超时重发
        scheduleRetry(msgId, timeoutMs);
    }
    
    /**
     * 收到 ACK
     */
    public static void onAck(String msgId) {
        PendingMessage pending = PENDING_MESSAGES.remove(msgId);
        if (pending != null) {
            pending.cancelRetry();
            System.out.println("消息已确认: " + msgId);
        }
    }
    
    /**
     * 超时重发
     */
    private static void scheduleRetry(String msgId, long timeoutMs) {
        // 延迟任务
        ScheduledFuture<?> future = scheduler.schedule(() -> {
            PendingMessage pending = PENDING_MESSAGES.get(msgId);
            if (pending == null) {
                return;  // 已确认
            }
            
            if (pending.decrementRetry() > 0) {
                // 重发
                System.out.println("消息超时,重发: " + msgId);
                pending.getChannel().writeAndFlush(pending.getMessage());
                
                // 再次调度
                scheduleRetry(msgId, timeoutMs);
            } else {
                // 重试次数用尽
                System.out.println("消息发送失败: " + msgId);
                PENDING_MESSAGES.remove(msgId);
                
                // 通知业务层
                notifyFailed(msgId);
            }
        }, timeoutMs, TimeUnit.MILLISECONDS);
        
        pending.setRetryFuture(future);
    }
}

5. 集群部署

/**
 * 多机部署时,使用 Redis 共享在线状态
 */
public class ClusterUserManager {
    
    private static final String ONLINE_KEY_PREFIX = "im:online:";
    private static final String SERVER_KEY_PREFIX = "im:server:";
    
    /**
     * 用户上线
     */
    public static void online(String userId, String serverId, Channel channel) {
        // 1. 本地管理
        UserChannelManager.online(userId, channel);
        
        // 2. Redis 记录
        redisTemplate.opsForValue().set(
            ONLINE_KEY_PREFIX + userId, 
            serverId,
            30, TimeUnit.MINUTES  // 30分钟过期
        );
    }
    
    /**
     * 查找用户所在服务器
     */
    public static String findUserServer(String userId) {
        return redisTemplate.opsForValue().get(ONLINE_KEY_PREFIX + userId);
    }
    
    /**
     * 跨服务器推送消息
     */
    public static void pushMessage(String userId, IMMessage message) {
        // 1. 查找用户所在服务器
        String serverId = findUserServer(userId);
        if (serverId == null) {
            // 用户不在线
            return;
        }
        
        // 2. 判断是否在本服务器
        if (serverId.equals(currentServerId)) {
            // 本地推送
            Channel channel = UserChannelManager.getChannel(userId);
            if (channel != null) {
                channel.writeAndFlush(message);
            }
        } else {
            // 跨服务器推送,使用 MQ
            String topic = SERVER_KEY_PREFIX + serverId;
            mqProducer.send(topic, message);
        }
    }
}

/**
 * 监听其他服务器的消息
 */
@Component
public class IMMessageConsumer {
    
    @RocketMQMessageListener(
        topic = "im:server:" + "${server.id}",
        consumerGroup = "im-push-group"
    )
    public void onMessage(IMMessage message) {
        String userId = message.getToUserId();
        
        // 推送给本地用户
        Channel channel = UserChannelManager.getChannel(userId);
        if (channel != null && channel.isActive()) {
            channel.writeAndFlush(message);
        }
    }
}

22. 在压力测试中,Netty 应用应该关注哪些指标?

答案:

关键性能指标:

1. QPS / TPS

// 统计每秒请求数
public class QpsCounter {
    private AtomicLong counter = new AtomicLong(0);
    
    public void count() {
        counter.incrementAndGet();
    }
    
    // 定时统计
    @Scheduled(fixedRate = 1000)
    public void report() {
        long qps = counter.getAndSet(0);
        System.out.println("QPS: " + qps);
    }
}

2. 响应时间(RT)

  • P50:50% 的请求响应时间
  • P95:95% 的请求响应时间
  • P99:99% 的请求响应时间
  • P999:99.9% 的请求响应时间
// 记录响应时间
public class LatencyRecorder {
    private List<Long> latencies = new CopyOnWriteArrayList<>();
    
    public void record(long latency) {
        latencies.add(latency);
    }
    
    public void report() {
        Collections.sort(latencies);
        int size = latencies.size();
        
        System.out.println("P50: " + latencies.get((int)(size * 0.5)));
        System.out.println("P95: " + latencies.get((int)(size * 0.95)));
        System.out.println("P99: " + latencies.get((int)(size * 0.99)));
    }
}

3. 连接数

// 监控连接数
public class ConnectionMonitor {
    private AtomicInteger connectionCount = new AtomicInteger(0);
    
    public void onConnect() {
        int count = connectionCount.incrementAndGet();
        System.out.println("当前连接数: " + count);
    }
    
    public void onDisconnect() {
        connectionCount.decrementAndGet();
    }
}

4. 内存使用

# 监控堆内存
-Xms4g -Xmx4g -XX:+PrintGCDetails

# 监控直接内存
-XX:MaxDirectMemorySize=2g
-Dio.netty.maxDirectMemory=2147483648

# 内存泄漏检测
-Dio.netty.leakDetection.level=simple

5. CPU 使用率

# 查看线程 CPU 使用
top -H -p <pid>

# 线程栈分析
jstack <pid> > thread.dump

6. 网络 I/O

# 网络流量监控
iftop -i eth0

# TCP 连接状态
netstat -ant | awk '{print $6}' | sort | uniq -c

7. GC 情况

# GC 日志
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log

# 查看 GC
jstat -gcutil <pid> 1000

性能优化建议:

1. JVM 参数调优

# 示例配置(8GB 内存)
-Xms4g -Xmx4g                    # 堆内存 4G
-Xmn2g                            # 年轻代 2G
-XX:MetaspaceSize=256m            # 元空间
-XX:MaxDirectMemorySize=2g        # 直接内存 2G
-XX:+UseG1GC                      # 使用 G1 GC
-XX:MaxGCPauseMillis=200          # GC 停顿目标
-XX:+ParallelRefProcEnabled       # 并行处理引用
-XX:+UnlockExperimentalVMOptions
-XX:+AggressiveOpts               # 激进优化

2. Netty 参数调优

ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024)              // 连接队列长度
 .option(ChannelOption.SO_REUSEADDR, true)            // 地址复用
 .childOption(ChannelOption.SO_KEEPALIVE, true)       // 保持连接
 .childOption(ChannelOption.TCP_NODELAY, true)        // 禁用 Nagle 算法
 .childOption(ChannelOption.SO_SNDBUF, 32 * 1024)     // 发送缓冲区
 .childOption(ChannelOption.SO_RCVBUF, 32 * 1024)     // 接收缓冲区
 .childOption(ChannelOption.ALLOCATOR, 
              PooledByteBufAllocator.DEFAULT)         // 使用内存池
 .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
              new WriteBufferWaterMark(32 * 1024, 64 * 1024));  // 流控

3. 线程数调优

// Boss 线程:1-2 个即可
EventLoopGroup bossGroup = new NioEventLoopGroup(1);

// Worker 线程:CPU 核心数 * 2
int threads = Runtime.getRuntime().availableProcessors() * 2;
EventLoopGroup workerGroup = new NioEventLoopGroup(threads);

// 业务线程池:独立的线程池处理耗时业务
ExecutorService businessExecutor = new ThreadPoolExecutor(
    threads, 
    threads * 2,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10000),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

4. 监控和告警

// 使用 Prometheus + Grafana 监控
// 使用 Micrometer 暴露指标
@Component
public class NettyMetrics {
    
    private Counter requestCounter;
    private Timer requestTimer;
    
    public NettyMetrics(MeterRegistry registry) {
        requestCounter = Counter.builder("netty.requests.total")
            .description("Total requests")
            .register(registry);
            
        requestTimer = Timer.builder("netty.request.duration")
            .description("Request duration")
            .register(registry);
    }
    
    public void recordRequest(long duration) {
        requestCounter.increment();
        requestTimer.record(duration, TimeUnit.MILLISECONDS);
    }
}

总结

以上是 Netty 架构师面试的核心问题和详细答案,涵盖了:

  1. 基础概念:Netty 是什么、为什么使用
  2. 核心组件:Channel、EventLoop、Handler、Pipeline 等
  3. 线程模型:Reactor 模式、线程安全、EventLoop 原理
  4. 内存管理:零拷贝、ByteBuf、内存池、内存泄漏
  5. 编解码器:内置编解码器、自定义协议、粘包拆包
  6. 高性能原理:I/O 模型、无锁化、数据结构优化
  7. 实战问题:心跳检测、断线重连、流控、WebSocket
  8. 架构设计:IM 系统设计、集群部署、性能优化

这些问题不仅考察理论知识,更注重实践经验和架构设计能力。建议结合实际项目经验回答,展示对 Netty 的深入理解。

posted @ 2026-01-26 22:04  菜鸟~风  阅读(0)  评论(0)    收藏  举报