⚡ Netty高性能网络编程面试10题

Java后端进阶 · 2026年5月 · 每题含标准答案+代码示例

Q1:Netty是什么?为什么要用Netty而不是直接用Java NIO?

一句话:Netty是异步事件驱动的网络应用框架,封装了Java NIO的复杂性。

为什么不用Java NIO原语:
1. API复杂:ByteBuffer到处flip/clear,Selector的keys处理繁琐
2. epoll空轮询bug:JDK NIO在Linux下Select空转100%CPU——Netty修复了
3. 零拷贝内置:Netty的FileRegion/CompositeByteBuf一行搞定
4. 编解码器生态:HTTP/WebSocket/Protobuf开箱即用

Q2:Netty的线程模型是什么?讲一下Reactor模式。

主从Reactor多线程模型(ServerBootstrap默认):
                      BossGroup (1线程)
                  EventLoop 轮询 Accept 事件
                            ↓
              新连接注册到 WorkerGroup
                            ↓
                    WorkerGroup (N线程 = CPU核数*2)
             EventLoop-1   EventLoop-2   ...   EventLoop-N
             (读→解码→业务→编码→写 全在一个线程)
关键点:一个Channel从创建到销毁,始终绑定在同一个EventLoop线程——无锁化设计。

对比Reactor三种模式:Netty的BossGroup+WorkerGroup就是主从Reactor(MainReactor处理Accept,SubReactor处理IO)。

Q3:BIO、NIO、AIO有什么区别?epoll为什么比select快?

三者的区别:
- BIO(阻塞IO):一个连接一个线程,读写都阻塞。连接超过1000时线程爆炸。
- NIO(非阻塞IO):一个线程管理多个连接。selector.select()只返回就绪的。
- AIO(异步IO):操作系统回调通知。Linux上底层也是epoll,未必更快。

epoll vs select 核心差异:
1. fd数量:select用fd_set,默认1024上限。epoll无限制。
2. 遍历方式:select每次O(n)全量扫描。epoll回调注册,O(1)只拿就绪的。
3. 内存拷贝:select每次把fd_set从用户态拷到内核态。epoll用mmap共享内存。

面试金句:"epoll是事件驱动,select是轮询扫描。Netty在Linux下用epoll边缘触发+水平触发双模。"

Q4:Netty怎么解决TCP粘包/拆包?

粘包原因:TCP是流协议,send 10次100字节,底层可能合并成一次1000字节发出去。

Netty内置5种拆包器:
// 1. 固定长度
ch.pipeline().addLast(new FixedLengthFrameDecoder(100));

// 2. 分隔符(最常用——Redis RESP协议就用\r\n)
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(8192, 
    Delimiters.lineDelimiter()));

// 3. 长度域(自定义协议:前4字节表长度)
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
    65535, 0, 4, 0, 4));

// 4. 行分隔
ch.pipeline().addLast(new LineBasedFrameDecoder(8192));

// 5. 变长自定义
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024*1024, 
    0, 2, -2, 0));

Q5:Netty的零拷贝是什么意思?和操作系统的零拷贝是一回事吗?

两个层面的"零拷贝":

1. Netty层面的零拷贝(减少Java堆内存拷贝):
- CompositeByteBuf:多个ByteBuf合并成一个逻辑Buf,不实际拷贝数据
- Slice:共享同一个byte[],不同视图
- FileRegion:调用transferTo直接文件→Socket,绕过用户态

2. OS层面的零拷贝(减少内核态-用户态拷贝):
- sendfile(Linux):DMA直接磁盘→内核缓冲区→Socket缓冲区(不经过用户态)
- mmap:内核和用户空间共享内存
- splice:两个fd之间管道传输

面试区分:Netty的CompositeByteBuf是JVM堆内优化;FileRegion底层调用OS的sendfile实现真正的零拷贝。

Q6:Netty的ByteBuf和JDK的ByteBuffer有什么区别?

核心对比:
| ByteBuf | ByteBuffer | |---------|-----------| | 双指针 readIndex/writeIndex | 单指针 position,反复flip | | 自动扩容 | 需手动检查capacity | | 池化(PooledByteBufAllocator) | 每次new,GC压力 | | 引用计数(ReferenceCounted) | 无回收机制 | | 支持链式调用 | 不支持 | 生产环境必须用池化:
ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
ByteBuf buf = allocator.buffer(1024);  // 从池中取,用完release归还

Q7:Netty的ChannelPipeline是怎么工作的?

双向链表责任链模式:
I/O Request
    ↓
┌─── ChannelPipeline ───────────┐
│  HeadContext (双向)            │
│  ↓                             │
│  [Encoder]  =====出站====>     │  // 出站Handler
│  [Codec]    ==出/入==>         │  // 编解码器合并
│  [Business]  <====入站====     │  // 业务Handler
│  ↓                             │
│  TailContext (双向)            │
└────────────────────────────────┘
    ↓
I/O Response
入站事件(Inbound):ChannelActive→ChannelRead→ChannelReadComplete 从头到尾顺序执行
出站事件(Outbound):write→flush 从尾到头逆序执行
传播控制:ctx.fireChannelRead()继续传播;不调用则中断。

这个设计的好处:解码、业务处理、编码各司其职,新增功能只需addLast,不修改现有代码——开闭原则。

Q8:Netty的EventLoop和Channel是怎么绑定的?

绑定机制:Channel注册到EventLoop时,通过next()轮询从EventLoopGroup中选一个。

// 默认轮询算法(DefaultEventExecutorChooserFactory)
public EventExecutor next() {
    return executors[idx.getAndIncrement() & executors.length - 1]; // 2的幂次取模
}
关键保证:一个Channel创建后绑定到某个EventLoop → 永久绑定,直到Channel关闭。这意味着:
- ✅ 所有IO操作(读/写/关闭)在同一个线程 → 无锁
- ✅ 不需要synchronized
- ⚠️ 不能在EventLoop中做长耗时操作(如大量计算、阻塞IO)——会阻塞该EventLoop上所有Channel

长耗时任务的正确处理:提交到DefaultEventExecutorGroup(业务线程池):
pipeline.addLast(new DefaultEventExecutorGroup(16), 
    new BusinessHandler());  // BusinessHandler在独立线程池执行

Q9:Netty的心跳机制怎么实现?

IdleStateHandler 三板斧:
ch.pipeline().addLast(new IdleStateHandler(
    60,   // readerIdleTime: 60秒没读到数据 → 触发 READER_IDLE
    30,   // writerIdleTime: 30秒没写出数据 → 触发 WRITER_IDLE
    0     // allIdleTime: 0=禁用
));

// 自定义心跳处理
class HeartbeatHandler extends ChannelInboundHandlerAdapter {
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.READER_IDLE) {
                ctx.close(); // 超过60秒没收到→踢下线
            } else if (state == IdleState.WRITER_IDLE) {
                ctx.writeAndFlush(new PingMessage()); // 30秒→发心跳
            }
        }
    }
}
生产实践:服务端设readerIdle=2*客户端心跳间隔——留一倍容错空间。

Q10:用Netty写一个简单的HTTP服务器,代码怎么搭?

30行核心代码:
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(boss, worker)
     .channel(NioServerSocketChannel.class)
     .childHandler(new ChannelInitializer() {
         protected void initChannel(SocketChannel ch) {
             ch.pipeline()
               .addLast(new HttpServerCodec())       // HTTP编解码
               .addLast(new HttpObjectAggregator(65536)) // 聚合完整请求
               .addLast(new SimpleChannelInboundHandler() {
                   protected void channelRead0(ChannelHandlerContext ctx, 
                       FullHttpRequest req) {
                       String body = "Hello Netty!";
                       FullHttpResponse resp = new DefaultFullHttpResponse(
                           HTTP_1_1, OK, Unpooled.copiedBuffer(body, UTF_8));
                       resp.headers().set(CONTENT_TYPE, "application/json");
                       resp.headers().set(CONTENT_LENGTH, body.length());
                       ctx.writeAndFlush(resp).addListener(CLOSE);
                   }
               });
         }
     });
    ChannelFuture f = b.bind(8080).sync();
    f.channel().closeFuture().sync();
} finally { boss.shutdownGracefully(); worker.shutdownGracefully(); }
追问多说一句:实际项目用Spring WebFlux + Netty,Spring Boot 3默认底层就是Netty。