Netty 架构师面试题集锦
Netty 架构师面试题集锦
目录
基础概念
1. 什么是 Netty?为什么要使用 Netty?
答案:
Netty 是一个异步事件驱动的网络应用框架,用于快速开发高性能、高可靠性的网络服务器和客户端程序。
使用 Netty 的原因:
-
API 简单易用
- 封装了 NIO 的复杂性
- 提供统一的 API,支持多种传输类型(NIO、OIO、Epoll)
-
高性能
- 零拷贝技术(Zero-Copy)
- 内存池设计
- 高效的 Reactor 线程模型
- 无锁化串行设计
-
稳定性强
- 修复了 JDK NIO 的已知 Bug(如 epoll bug)
- 成熟的断线重连、心跳检测机制
-
社区活跃
- 大量开源项目使用(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 的应用场景有哪些?
答案:
-
RPC 框架
- Dubbo 的网络通信层
- gRPC 的底层实现
- Spring Cloud Gateway
-
消息中间件
- RocketMQ 的 Remoting 模块
- Kafka 的网络层(Scala 实现,但思想类似)
-
分布式协调
- Elasticsearch 的节点通信
- ZooKeeper 的网络层(虽然原生实现)
-
游戏服务器
- 长连接推送
- 实时通信
-
IM 即时通讯
- WebSocket 聊天室
- 消息推送系统
-
大数据传输
- 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
工作流程:
-
BossGroup 职责:
- 监听端口
- 接收新连接(Accept)
- 将连接注册到 WorkerGroup
-
WorkerGroup 职责:
- 从 Channel 读取数据
- 业务处理
- 将结果写回 Channel
-
线程分配原则:
- 一个 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
为什么使用内存池?
优点:
- 减少 GC 压力 - 复用对象,减少频繁创建
- 减少内存碎片 - 按规格分配
- 提高分配速度 - 线程缓存,无锁化
缺点:
- 内存占用更大(预分配)
- 代码复杂度高
- 需要手动管理引用计数
使用建议:
// 高并发场景,推荐使用池化
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");
}
}
解决思路:
- 记录连续空轮询次数
- 超过阈值(512次),判定为 Bug
- 重建 Selector
- 迁移所有注册的 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)]; // 线性探测
}
性能问题:
- 哈希计算
- 哈希冲突时线性探测
- 弱引用导致的清理开销
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
}
}
重连策略:
- 固定延迟:每次间隔固定时间(如 5 秒)
- 指数退避:延迟时间指数增长(2s, 4s, 8s, 16s...)
- 限制次数:超过最大次数后放弃
- 抖动:添加随机延迟,避免雷鸣羊群效应
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);
流量整形类型:
- ChannelTrafficShapingHandler:单 Channel 限流
- GlobalTrafficShapingHandler:全局限流
- 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 架构师面试的核心问题和详细答案,涵盖了:
- 基础概念:Netty 是什么、为什么使用
- 核心组件:Channel、EventLoop、Handler、Pipeline 等
- 线程模型:Reactor 模式、线程安全、EventLoop 原理
- 内存管理:零拷贝、ByteBuf、内存池、内存泄漏
- 编解码器:内置编解码器、自定义协议、粘包拆包
- 高性能原理:I/O 模型、无锁化、数据结构优化
- 实战问题:心跳检测、断线重连、流控、WebSocket
- 架构设计:IM 系统设计、集群部署、性能优化
这些问题不仅考察理论知识,更注重实践经验和架构设计能力。建议结合实际项目经验回答,展示对 Netty 的深入理解。

浙公网安备 33010602011771号