乐于分享
好东西不私藏

Netty源码分析(2) — 从BIO到NIO再到Netty,为什么我们需要Netty?

Netty源码分析(2) — 从BIO到NIO再到Netty,为什么我们需要Netty?

Netty源码分析(2) — 从BIO到NIO再到Netty,为什么我们需要Netty?

一、先看一段"祖传代码"

很多老项目里都能看到这样的代码——用 BIO(Blocking I/O) 写一个Socket服务端:

// 你肯定见过的"祖传代码"——BIO 多线程版
public class BioServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO Server started on port 8080");
        
        // 一个线程池来处理客户端连接
        ExecutorService threadPool = Executors.newFixedThreadPool(100);
        
        while (true) {
            // accept() 是阻塞的——没有连接来就卡在这
            Socket socket = serverSocket.accept();
            System.out.println("新客户端连接: " + socket.getRemoteSocketAddress());
            
            // 每个连接扔到一个线程里去处理
            threadPool.execute(() -> {
                handleConnection(socket);
            });
        }
    }
    
    private static void handleConnection(Socket socket) {
        try (
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
        ) {
            String line;
            // readLine() 也是阻塞的——没数据可读就卡在这
            while ((line = in.readLine()) != null) {
                System.out.println("收到: " + line);
                out.println("已收到: " + line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这段代码的问题在哪?

本质问题:一个连接需要一个线程。

如果 1000 个客户端连上来,线程池 100 个就满了,第 101 个连接只能排队等着。这叫 C10K 问题——一万个并发连接,一万个线程?操作系统直接崩溃。

Java 1.4 引入了 NIO(New I/O / Non-blocking I/O),试图解决这个问题。理论上一个线程可以管理成千上万个连接。但现实是……

二、为什么原生 NIO 也难用?

先看一段原生 NIO 的代码:

public class NioServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建 Selector(事件轮询器)
        Selector selector = Selector.open();
        
        // 2. 创建 ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress(8080));
        serverSocket.configureBlocking(false);  // 非阻塞模式!
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        
        System.out.println("NIO Server started on port 8080");
        
        while (true) {
            // 3. 阻塞等待事件发生
            selector.select();
            
            // 4. 处理所有就绪的事件
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> it = selectedKeys.iterator();
            
            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();  // 必须手动移除!
                
                try {
                    if (key.isAcceptable()) {
                        // 有新连接进来
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_READ);
                        System.out.println("新连接: " + client.getRemoteAddress());
                        
                    } else if (key.isReadable()) {
                        // 有数据可读
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int read = client.read(buffer);
                        
                        if (read > 0) {
                            buffer.flip();
                            byte[] bytes = new byte[buffer.remaining()];
                            buffer.get(bytes);
                            String msg = new String(bytes);
                            System.out.println("收到: " + msg);
                            
                            // 写回去——还要处理写半包
                            ByteBuffer writeBuf = ByteBuffer.wrap(
                                ("已收到: " + msg).getBytes());
                            client.write(writeBuf);
                        } else if (read == -1) {
                            // 客户端关闭了
                            client.close();
                        }
                    }
                } catch (IOException e) {
                    // 任何一个连接出异常要把key取消掉
                    key.cancel();
                    key.channel().close();
                }
            }
        }
    }
}

看到没?原生 NIO 有这几个痛点:

1. 代码复杂到离谱 一个简单的 Echo Server 写了几十行,各种 try-catch 满天飞,任何一步出错都要手动 cleanup。真实线上代码比这还要复杂 5 倍。

2. 粘包半包问题 TCP 是流式协议,没有消息边界。你发 "Hello" + "World",底层可能一次收到 "HelloWorld",也可能分三次收到 "Hel" + "loW" + "orld"。原生 NIO 要你自己去拼缓冲区、判断消息边界。

3. 断线重连、心跳、空闲检测 这些在原生 NIO 里全得手写,而且很容易写出 Bug。

4. 多线程处理复杂 selector 线程和 worker 线程怎么配合?读写 buffer 要不要加锁?这些都是坑。

5. 一个著名的 Bug——JDK 的 epoll 空轮询 JDK NIO 在 Linux 上有个经典 Bug:Selector.select() 在 epoll 模式下即使没有事件也会立即返回,导致 CPU 飙到 100%。虽然 JDK 在后续版本修复了这个问题,但修复方式……emmm,是用"如果空轮询超过一定次数就重建 Selector"这种 Hack(后来 JDK 6u17 才真正修好)。

所以,原生 NIO ≈ 自己做轮子,而且轮子可能是方的。

三、Netty 来了——把复杂留给自己,简单留给开发者

同样一个 Echo Server,用 Netty 写:

public class NettyServer {
    public static void main(String[] args) {
        // 1. 两个 EventLoopGroup——一个接连接,一个处理读写
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            // 2. 启动器——帮我们做配置的"脚手架"
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new ChannelInitializer<SocketChannel>() {
                         @Override
                         protected void initChannel(SocketChannel ch) {
                             // 3. 用 Pipeline 组装处理器——像搭积木一样
                             ch.pipeline().addLast(
                                 new StringDecoder(),
                                 new StringEncoder(),
                                 new EchoServerHandler()
                             );
                         }
                     });
            
            // 4. 绑定端口启动
            ChannelFuture future = bootstrap.bind(8080).sync();
            System.out.println("Netty Server started on port 8080");
            
            // 5. 优雅关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

// 业务逻辑——只关心 "数据来了怎么办"
class EchoServerHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        System.out.println("收到: " + msg);
        ctx.writeAndFlush("已收到: " + msg);
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

不用管 Selector,不用管 ByteBuffer 的 flip/compact,不用管事件注册,不用处理粘包……五行代码就把一个 Echo Server 写完了。

底层 Netty 帮你做了这些:

3.1 Reactor 线程模型——拒绝开线程地狱

Netty 的线程模型是多 Reactor 模式:

                    ┌──────────────────┐
                    │   Boss Group     │ (通常1个线程)
                    │  (Acceptor)      │
                    └────────┬─────────┘
                             │ 接受新连接
                             ▼
             ┌─────────────────────────────┐
             │       Worker Group          │ (通常 CPU 核数 * 2 个线程)
             │  ┌──────┐ ┌──────┐ ┌──────┐ │
             │  │Event │ │Event │ │Event │ │
             │  │Loop1 │ │Loop2 │ │Loop3 │ │
             │  └──┬───┘ └──┬───┘ └──┬───┘ │
             └─────┼────────┼────────┼─────┘
                   │        │        │
                channel1 channel2 channel3
  • Boss Group:只负责 accept 新连接,然后分发给 Worker
  • Worker Group:负责连接上的读写操作
  • 每个 EventLoop 绑定一个 Selector,管理多个 Channel
  • 一个 Channel 的生命周期绑定在同一个 EventLoop 上——天然无锁!

3.2 Pipeline 机制——把复杂业务拆成积木

Netty 把数据处理设计成责任链模式:

I/O Request
    │
    ▼
┌─────────────┐
│ Head Handler│  (内置,处理底层 I/O)
├─────────────┤
│ Decoder     │  ← Netty 自带的分隔符解码器、长度域解码器(解决粘包!)
├─────────────┤
│ Encoder     │  ← 对象序列化
├─────────────┤
│ Business    │  ← 你只需要写这个!
├─────────────┤
│ Tail Handler│  (内置,处理异常)
└─────────────┘

每个处理器只干一件事,职责单一,想加功能就加一个 Handler,想改就换一个 Handler。

3.3 零拷贝——省掉无谓的数据复制

Netty 的 ByteBuf 有很多骚操作:

// 合成两个 ByteBuf——不用复制数据
CompositeByteBuf composite = Unpooled.compositeBuffer();
composite.addComponents(true, header, body);
// header 和 body 的数据还在原来的内存里,不会发生拷贝

// 零拷贝切片
ByteBuf slice = buffer.slice(0, 10);
// slice 和 buffer 共享同一块内存

还有 FileRegion 配合操作系统 sendfile,文件传输直接在内核空间完成,用户空间完全不沾手。

3.4 内存池——告别频繁 GC

每个 new ByteBuffer.allocate(1024) 都在堆上分配内存,高并发下 GC 压力爆炸。

Netty 的 PooledByteBufAllocator 做了池化:

  • 预先分配一大块内存,切成小片(PoolChunk → PoolSubpage)
  • 用完归还到池中,下次复用
  • 还支持堆外内存(DirectBuffer),减少在垃圾回收上的停顿

这部分后面专门开一篇详细分析,这里先知道 Netty 在内存管理上有多变态就够了。

四、所以,Netty 到底解决了什么?

用一张表说清楚:

问题 原生 NIO Netty
代码复杂度 复杂,大量模板代码 简洁,关注业务逻辑
粘包半包 手写缓冲区拼装 内置 DelimiterBasedFrameDecoder 等
线程模型 自己管理 Reactor 模型,EventLoop 无锁设计
断线重连 手写 IdleStateHandler + 自定义重连
零拷贝 不支持 ByteBuf 零拷贝 + FileRegion
内存管理 GC 频繁 内存池 + 堆外内存
epoll 空轮询 JDK 自身有 Bug Netty 有自动防御机制
协议扩展 手写编解码 编解码器链式组合
异常处理 各处 try-catch exceptionCaught 统一处理

一句话总结:Netty 把 Java NIO 封装到了极致,让开发者只需要关注"数据来了怎么处理"这一个问题。其他所有底层细节——事件循环、线程调度、内存分配、缓冲区管理——全被 Netty 消化了。

五、一个完整的示例——Netty 客户端

刚才写了服务端,再来个对应的客户端:

public class NettyClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                     .channel(NioSocketChannel.class)
                     .handler(new ChannelInitializer<SocketChannel>() {
                         @Override
                         protected void initChannel(SocketChannel ch) {
                             ch.pipeline().addLast(
                                 new StringDecoder(),
                                 new StringEncoder(),
                                 new SimpleChannelInboundHandler<String>() {
                                     @Override
                                     protected void channelRead0(
                                             ChannelHandlerContext ctx, String msg)
 {
                                         System.out.println("服务端回复: " + msg);
                                     }
                                 }
                             );
                         }
                     });
            
            // 连接服务端
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).sync();
            
            // 发送消息
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(System.in));
            while (true) {
                String line = reader.readLine();
                if ("quit".equals(line)) break;
                future.channel().writeAndFlush(line);
            }
            
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

先启动服务端,再启动客户端,输入文字就能看到服务端回复了。

六、Netty 的适用场景

Netty 不只是一个网络框架,它是一个基础设施。看看哪些主流项目在用 Netty:

  • Apache Dubbo / gRPC — RPC 框架底层通信
  • Apache RocketMQ / Kafka — 消息中间件
  • Spring WebFlux — 响应式 Web 框架
  • Elasticsearch — 分布式搜索引擎节点通信
  • ZooKeeper — 分布式协调服务(3.6+ 可选 Netty)
  • Hadoop / Spark — 大数据框架的部分组件

基本上,只要是用 Java 做高并发网络通信的中间件,底层大概率就是 Netty。

七、下一篇预告

Netty 的核心灵魂是 Reactor 线程模型。下一篇我们深入 EventLoopGroup 和 EventLoop 的源码,看看 Netty 到底是怎么做到"一个线程管理上千个连接还不用加锁"的。

这是整个 Netty 源码分析中最关键的一篇,懂了线程模型,后面看 Pipeline、ByteBuf、内存分配都会轻松很多。

下次见!

相关学习资料