Netty:BIO、NIO 与 IO 多路复用底层原理
一、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 的区别
纠错: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
}
}生产注意: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等待就绪事件;大量空闲连接下通常更有优势。
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 或同步调用外部接口。
八、生产场景与踩坑
TCP 是字节流:一次读不等于一条消息,必须设计长度字段、分隔符或固定长度协议。
不可常驻监听 OP_WRITE:Socket 经常可写,长期开启可能让事件循环频繁空转;仅在待写数据且确有必要时关注。
区分
read()返回值:-1 表示流结束,0 表示当前没有数据,正数是实际读取字节数。避免单个连接独占线程:大量空闲物联网连接更适合 NIO/Netty。
不要把非阻塞误解为零等待:
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,业务必须实现粘包拆包处理。
