0
0
0

Netty:BIO、NIO 与 IO 多路复用底层原理

2026-10-30
2026-10-30
文章摘要
|

一、Netty 是什么

Netty 是基于事件驱动的异步网络应用框架,常用 Java NIO 传输,也支持特定操作系统的原生传输。它封装了网络线程管理、Channel、事件传播、编解码和连接管理等通用能力。

  • 适用:TCP 长连接、物联网设备接入、即时通讯、游戏服务、RPC 等。

  • 与 Spring Boot 不冲突:Spring Boot 管理应用与业务组件,Netty 负责网络通信。

  • Netty 不属于 Java 8 自带类库;java.net、java.nio、java.nio.channels 属于 JDK 自带。

一句话:NIO 解决如何非阻塞地管理 IO;Netty 解决如何组织高性能、可维护的网络通信服务。

二、BIO、NIO 与 Netty 的区别

维度

BIO

原生 Java NIO

Netty

主要 API

Socket、InputStream

SocketChannel、Selector、ByteBuffer

Bootstrap、EventLoop、Channel、Pipeline

线程等待

典型实现一连接一阻塞读取线程

少量线程监听多个 Channel

在 NIO 等传输能力上进一步封装

编解码、心跳

自己实现

自己实现

内置基础设施与可扩展 Handler

适用

连接少、实现简单

需要底层控制

长连接、大量设备、复杂协议

纠错:Netty 不是 BIO、NIO 之外的第三种 IO 模型,它是网络框架。

2.1 BIO 的阻塞位置

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket socket = serverSocket.accept(); // 无新连接时阻塞
    new Thread(() -> {
        try (Socket s = socket; InputStream in = s.getInputStream()) {
            byte[] bytes = new byte[1024];
            int n;
            while ((n = in.read(bytes)) != -1) { // 无数据时通常阻塞
                System.out.println("读到字节数:" + n);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }).start();
}

这只是教学代码,未处理线程数量上限与优雅关闭。在一连接一线程方案里,大量空闲连接也可能占用大量线程资源。

2.2 NIO 的优势

少量线程可等待大量 Channel 的 IO 就绪事件,空闲连接不必独占一个阻塞线程。但非阻塞不代表不消耗资源,也不代表业务代码自动并行。

三、Buffer:内存缓冲区

常用 ByteBuffer,属于 Java 8 自带的 java.nio。

  • capacity:总容量。

  • position:下次操作的位置。

  • limit:当前允许读写的边界。

import java.nio.ByteBuffer;

public class BufferDemo {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put((byte) 10);
        buffer.put((byte) 20);
        buffer.put((byte) 30);
        // 写模式:position=3, limit=10, capacity=10

        buffer.flip();
        // 读模式:position=0, limit=3
        System.out.println(buffer.get()); // 10
        System.out.println(buffer.get()); // 20
        // 此时 position=2, limit=3

        buffer.compact();
        // 未读的 30 移到开头,进入写模式:position=1, limit=10
        buffer.put((byte) 40);
        buffer.flip();
        System.out.println(buffer.get()); // 30
        System.out.println(buffer.get()); // 40
    }
}

方法

作用

注意

put()

写入字节

position 前进

flip()

写模式切到读模式

limit=旧 position,position=0

get()

读取字节

position 前进

clear()

重置为写模式

不是真的清零数据

compact()

保留未读内容并切回写模式

未读数据会移到开头

生产注意:ByteBuffer 容量不足可能触发 BufferOverflowException;TCP 接收时一次 read() 不保证收到一条完整业务消息。

四、Channel:读写通道

  • ServerSocketChannel:监听 TCP 连接。

  • SocketChannel:TCP 连接的读写。

  • DatagramChannel:UDP 通信。

  • FileChannel:文件读写。

SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
// 非阻塞连接需要检查 finishConnect();以下只演示读取 API
ByteBuffer buffer = ByteBuffer.allocate(1024);
int n = channel.read(buffer);
// n > 0:读取到 n 字节
// n == 0:当前没有可读取的数据
// n == -1:对端已到达流结束(通常需关闭连接)

Channel 负责传输,Buffer 负责装数据。

五、Selector:IO 就绪事件通知中心

Selector 让一个线程监听多个非阻塞 Channel。常见兴趣事件:

  • OP_ACCEPT:服务端通道可接受新连接。

  • OP_CONNECT:非阻塞连接操作就绪。

  • OP_READ:可以尝试读取。

  • OP_WRITE:可以尝试写入。

5.1 核心循环(可运行的简化 TCP 服务端)

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NioServer {
    public static void main(String[] args) throws Exception {
        try (Selector selector = Selector.open();
             ServerSocketChannel server = ServerSocketChannel.open()) {
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(8080));
            server.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("NIO listening on 8080");
            while (true) {
                selector.select(); // 可以阻塞等待“多个连接”的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove(); // 必须从 selectedKeys 中移除已处理的键
                    if (!key.isValid()) continue;
                    try {
                        if (key.isAcceptable()) {
                            SocketChannel client = server.accept();
                            if (client != null) {
                                client.configureBlocking(false);
                                client.register(selector, SelectionKey.OP_READ);
                            }
                        } else if (key.isReadable()) {
                            SocketChannel client = (SocketChannel) key.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            int n = client.read(buffer);
                            if (n == -1) {
                                key.cancel();
                                client.close();
                            } else if (n > 0) {
                                buffer.flip();
                                byte[] bytes = new byte[buffer.remaining()];
                                buffer.get(bytes);
                                System.out.println(new String(bytes, "UTF-8"));
                            }
                        }
                    } catch (Exception ex) {
                        key.cancel();
                        key.channel().close();
                    }
                }
            }
        }
    }
}

此例仅演示多路复用,不处理 TCP 分包、业务协议或写缓冲。生产不能把每次 read() 当作一条完整消息。

5.2 select() 会不会阻塞?

会。 Selector.select() 可以阻塞当前线程直到发生就绪事件、被 wakeup() 唤醒或被中断等。NIO 所说的非阻塞主要指 Channel 的 IO 操作可以配置非阻塞模式。

六、Linux IO 多路复用:select / poll / epoll

  • select:调用时传入 FD 集合,返回后通常检查整个集合;存在 fd_set 大小约束。

  • poll:同样每次传入监听集合,但没有 select 同类固定 fd_set 容量限制。

  • epoll:使用 epoll_ctl 注册关注的 FD,内核维护关注集合和就绪信息,使用 epoll_wait 等待就绪事件;大量空闲连接下通常更有优势。

机制

每次重交关注集合

获取就绪 FD 的方式

大量空闲连接

select

是

遍历检查

通常较差

poll

是

遍历检查

通常较差

epoll

通常不需要

返回就绪事件

通常较好

Linux epoll 关键调用:epoll_create1、epoll_ctl、epoll_wait。上述是 Linux 系统调用概念,不属于 Java 8 直接暴露的原生 API。

6.1 LT 与 ET

  • LT(水平触发):缓冲区仍满足就绪条件时,后续还可以继续通知。

  • ET(边缘触发):围绕就绪状态变化通知,非阻塞读写通常需要循环直到返回 EAGAIN,避免漏处理已有数据。

纠错:epoll 不是一定比 select/poll 快,也不是让系统完全不用扫描任何数据结构;性能取决于连接数、活跃比例与处理模式。

七、与 Netty 的关系

典型链路:客户端 Socket → Linux IO 就绪通知 → JDK NIO Selector(或 Netty 原生 epoll 传输)→ Netty EventLoop → ChannelPipeline → Handler。

  • EventLoop 负责监听、分发 IO 以及执行任务。

  • Handler 不应长期阻塞 EventLoop,比如直接执行慢 SQL 或同步调用外部接口。

八、生产场景与踩坑

  1. TCP 是字节流:一次读不等于一条消息,必须设计长度字段、分隔符或固定长度协议。

  2. 不可常驻监听 OP_WRITE:Socket 经常可写,长期开启可能让事件循环频繁空转;仅在待写数据且确有必要时关注。

  3. 区分 read() 返回值:-1 表示流结束,0 表示当前没有数据,正数是实际读取字节数。

  4. 避免单个连接独占线程:大量空闲物联网连接更适合 NIO/Netty。

  5. 不要把非阻塞误解为零等待:selector.select() 本身可以阻塞等待多路事件。

九、问题与标准答案

Q1:为什么 NIO 一个线程能管理多个连接?

A:Channel 配置为非阻塞,多个 Channel 注册到 Selector,线程通过 IO 多路复用等待任意连接的就绪事件,再处理对应 Channel,而不是为每条连接创建阻塞读取线程。

Q2:Selector.select() 会阻塞吗?

A:会。它可以阻塞等待多个 Channel 的就绪事件,与单个 Socket read() 阻塞不是一回事。

Q3:flip()、clear()、compact() 的区别?

A:flip 切换到读模式;clear 重置状态准备写入但不清除底层字节;compact 保留未读部分、移动到开头再进入写模式。

Q4:epoll 为什么适合连接多但活跃比例低的场景?

A:epoll 将关注 FD 持久维护在内核中,并通过等待接口返回就绪事件,减少每次重新提交全部关注集合和应用层扫描所有 FD 的成本。

Q5:可读事件意味着一定有完整业务报文吗?

A:不意味着。它只表示可以尝试读取,也可能读到部分字节或读出 -1,业务必须实现粘包拆包处理。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论