【Netty源码解读和权威指南】第02篇:Java IO进化史——从BIO到NIO再到AIO,一文搞懂网络IO
上一篇【第01篇】Netty是什么——从NIO地狱到网络编程天堂的蜕变
下一篇【第03篇】手把手搭建Netty开发环境——5分钟从零到HelloWorld
摘要
Java IO模型经历了四次重大进化:JDK 1.0的BIO(阻塞IO)让服务器被连接"卡死";JDK 1.4的NIO(非阻塞IO)通过Selector实现多路复用,一个线程管成百上千连接;JDK 1.7的AIO(异步IO)让操作系统帮我们完成IO后主动通知。但NIO的API复杂难用,这正是Netty登场的原因。
本文详解Linux五种IO模型(阻塞/非阻塞/IO多路复用/信号驱动/异步),对比BIO/NIO/AIO四种Java模式的代码实现,并给出生产环境的IO模型选型建议。读完这篇,您将彻底搞懂Java网络IO的来龙去脉。
一、Linux五种IO模型——网络编程的底层逻辑
要理解Java IO,必须先理解操作系统层面的IO模型。Linux提供了五种IO模型,它们的本质区别在于:应用程序何时知道"数据准备好了"。
【Linux五种IO模型对比】
模型 等待数据 数据从内核→用户空间 特点
────────────────────────────────────────────────────────────
阻塞IO(BIO) 阻塞 阻塞 一个连接一个线程,简单但浪费
非阻塞IO(NIO) 轮询 阻塞 不阻塞等待,但要不断轮询,CPU空转
IO多路复用(NIO) select/epoll阻塞 阻塞 一个线程管多个连接,最高效
信号驱动IO 不阻塞 阻塞 较少使用,信号处理复杂
异步IO(AIO) 不阻塞 不阻塞 操作系统完成后通知,最理想
1.1 阻塞IO(Blocking IO)——最朴素的IO模型
【阻塞IO模型图解】
应用程序线程 内核
| |
|-- recvfrom() ------>| (1) 系统调用,线程阻塞
| |----> (2) 等待数据到达网卡
| |----> (3) 数据从网卡拷贝到内核缓冲区
| |<---- (4) 数据从内核拷贝到用户空间
|<-- 数据返回 --------| (5) 系统调用返回,线程恢复执行
| |
💤 阻塞等待中... 内核在忙
特点:
- 优点:编程简单,适合连接数少的场景
- 缺点:一个连接一个线程,C10K问题(1万连接=1万线程,系统崩溃)
Java BIO代码:
// 服务器端(一个连接一个线程)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 💤 阻塞等待连接
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
int len;
byte[] buf = new byte[1024];
while ((len = in.read(buf)) != -1) { // 💤 阻塞等待数据
System.out.println(new String(buf, 0, len));
}
} catch (Exception e) { }
}).start();
}
1.2 非阻塞IO(Non-blocking IO)——不断"问"内核
【非阻塞IO模型图解】
应用程序线程 内核
| |
|-- recvfrom() ------>| (1) 系统调用,立即返回(数据未准备好返回EAGAIN)
|<-- EAGAIN ---------| (2) 应用程序知道数据还没好
| 去做其他事... |
|-- recvfrom() ------>| (3) 再次询问
|<-- EAGAIN ---------| (4) 还是没好
| 继续做其他事... |
|-- recvfrom() ------>| (5) N次询问后,数据终于准备好了!
|<-- 数据返回 --------| (6) 这一次真的有数据了
特点:
- 优点:不用阻塞等待,可以去干其他事
- 缺点:要不断轮询(polling),CPU空转浪费严重
1.3 IO多路复用(IO Multiplexing)——一个线程管所有连接
【IO多路复用模型图解(select/epoll)】
应用程序线程 内核
| |
|-- select() --------->| (1) 告诉内核:帮我监视这些连接(阻塞在此)
| |----> (2) 内核监视所有连接
| |----> (3) 某个连接有数据到达了!
|<-- 有数据了! -------| (4) select返回,告诉应用哪个连接有数据
| |
|-- recvfrom() ------>| (5) 应用发起真正的读取(此时不会阻塞太久)
|<-- 数据返回 --------| (6) 读取数据
核心思想:把"监视连接"和"读取数据"两件事分开。一个线程通过select/epoll监视成百上千个连接,哪个连接有数据了才去读哪个。
select vs epoll:
| 特性 | select | poll | epoll(Linux特有) |
|---|---|---|---|
| 最大连接数 | 1024(硬编码) | 无限制 | 无限制 |
| 性能 | O(N),随连接数下降 | O(N) | O(1),只返回活跃连接 |
| 数据结构 | 位图(bitmap) | 数组 | 红黑树 + 就绪链表 |
为什么Netty在Linux上用Epoll传输层比NIO快? 因为JDK NIO在Linux上用的是epoll,但JDK的实现有额外开销;Netty的Epoll传输层直接调用Linux原生epoll API,减少了JDK层的开销,性能提升20-30%。
1.4 异步IO(Asynchronous IO)——最理想的IO模型
【异步IO模型图解】
应用程序线程 内核
| |
|-- aio_read() ------>| (1) 告诉内核:数据好了叫我(立即返回,不阻塞)
|<-- 立即返回 --------| (2) 应用程序可以去干其他事
| |
| 做其他事情... |----> (3) 数据到达网卡
| |----> (4) 数据拷贝到用户空间
| |
|<-- 信号/回调 -------| (5) 内核主动通知应用:数据已经好了!
| |
特点:
- 优点:真正异步,应用完全不阻塞
- 缺点:Linux对AIO支持不完善,Java AIO在实际生产中使用较少
二、Java IO模型的四次进化
Java的IO模型随着JDK版本不断进化,每一次进化都是为了解决上一代的痛点。
【Java IO进化时间线】
JDK 1.0 (1996) JDK 1.4 (2002) JDK 1.7 (2011)
| | |
v v v
BIO NIO AIO
(阻塞IO) (非阻塞IO) (异步IO)
| | |
v v v
一个连接 多路复用 回调函数
一个线程 Selector CompletionHandler
2.1 第一代:BIO(Blocking IO)——一个连接一个线程
// BIO时间服务器(传统阻塞模式)
public class TimeServerBIO {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8080);
System.out.println("BIO服务器启动,监听8080...");
while (true) {
Socket socket = server.accept(); // 💤 阻塞,等待客户端连接
System.out.println("客户端连接:" + socket.getRemoteSocketAddress());
// 每个连接开一个新线程处理(这就是C10K问题的根源)
new Thread(() -> handleSocket(socket)).start();
}
}
private static void handleSocket(Socket socket) {
try (InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream()) {
byte[] buf = new byte[1024];
int len = in.read(buf); // 💤 阻塞,等待客户端发数据
if (len > 0) {
String msg = new String(buf, 0, len);
System.out.println("收到:" + msg);
out.write(("时间:" + new Date() + "\n").getBytes());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
BIO的致命缺陷:
- 连接数 = 线程数,1万连接 = 1万线程
- 线程切换开销巨大(上下文切换)
- 线程栈占用内存(默认1MB/线程),内存撑不住
2.2 第二代:伪异步IO——用线程池"缓一缓"
// 伪异步IO(用线程池限制线程数量)
public class TimeServerPseudoAsync {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8080);
// 用线程池代替"一个连接一个线程"
ExecutorService pool = Executors.newFixedThreadPool(10);
System.out.println("伪异步IO服务器启动...");
while (true) {
Socket socket = server.accept(); // 💤 还是阻塞
pool.execute(() -> handleSocket(socket)); // 交给线程池
}
}
}
伪异步IO的评价:
- 优点:控制了线程数量,不会无限创建线程
- 缺点:并没有解决阻塞问题!如果10个连接都在等待数据,第11个连接就无法被处理(线程池满了)
2.3 第三代:NIO(Non-blocking IO)——多路复用的王者
// NIO时间服务器(非阻塞多路复用模式)
public class TimeServerNIO {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 🔑 设置为非阻塞模式
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT); // 注册"接受连接"事件
System.out.println("NIO服务器启动,监听8080...");
while (true) {
selector.select(); // 💤 阻塞,但阻塞在Selector上(不是阻塞在某个连接上!)
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
// 有新连接进来
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接:" + sc.getRemoteAddress());
} else if (key.isReadable()) {
// 有连接发来数据
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = sc.read(buf); // 非阻塞读取
if (len > 0) {
buf.flip();
System.out.println("收到:" + new String(buf.array(), 0, len));
sc.write(ByteBuffer.wrap(("时间:" + new Date() + "\n").getBytes()));
}
}
}
}
}
}
NIO的核心思想:
- 一个Selector线程监视所有连接
- 只有真正有数据的连接才会被处理
- 完美解决C10K问题!
但NIO的痛点:
- API复杂(ByteBuffer的flip/compact让人头秃)
- 要自己处理TCP粘包/拆包
- 要自己实现协议(HTTP/WebSocket等)
- JDK的epoll Bug(空轮询导致CPU 100%)
2.4 第四代:AIO(Asynchronous IO)——回调函数的世界
// AIO时间服务器(真正的异步IO)
public class TimeServerAIO {
public static void main(String[] args) throws Exception {
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
System.out.println("AIO服务器启动,监听8080...");
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketSocketChannel client, Void attachment) {
server.accept(null, this); // 继续接受下一个连接
ByteBuffer buf = ByteBuffer.allocate(1024);
client.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
buf.flip();
System.out.println("收到:" + new String(buf.array(), 0, result));
client.write(ByteBuffer.wrap(("时间:" + new Date() + "\n").getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 主线程不退出(AIO是回调模式,主线程退出程序就结束了)
Thread.currentThread().join();
}
}
AIO的评价:
- 优点:真正异步,编程模型更简洁
- 缺点:Linux对AIO支持不完善;Windows的IOCP实现较好,但服务端很少用Windows;实际生产中很少使用AIO
为什么生产环境很少用AIO? 因为NIO的多路复用模型已经足够高效(epoll的O(1)复杂度),AIO带来的性能提升有限,反而增加了编程复杂度(回调函数嵌套)。所以,Netty、Dubbo、Elasticsearch等框架都选择NIO,而不是AIO。
三、Java NIO三大核心组件详解
Java NIO提供了三大核心组件:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。理解这三大组件,就理解了Java NIO的半壁江山。
【Java NIO三大核心组件关系图】
+---------------------+ +---------------------+
| Selector | | Channel |
| (选择器/多路 |<------| (通道/连接) |
| 复用器) | | |
| | | ServerSocketChannel |
| 监视多个Channel | | SocketChannel |
| 的IO事件 | | DatagramChannel |
+----------+----------+ +----------+----------+
| |
| 注册(register) | read/write
v v
+----------+----------+ +---------------------+
| SelectionKey | | Buffer |
| (事件类型) | | (缓冲区) |
| | | |
| OP_ACCEPT: 接受连接 | | HeapByteBuffer |
| OP_CONNECT: 连接完成| | DirectByteBuffer |
| OP_READ: 可读 | | |
| OP_WRITE: 可写 | | 数据的中转站 |
+---------------------+ +---------------------+
3.1 Channel(通道)——数据的"高速公路"
Channel是Java NIO中的数据通道,类似于BIO中的Socket,但有重要区别:
| 特性 | BIO的Socket | NIO的Channel |
|---|---|---|
| 方向 | 输入/输出分开(InputStream/OutputStream) | 双向(可读可写) |
| 阻塞 | 阻塞 | 可设置为非阻塞 |
| 数据 | 直接读写 | 通过Buffer读写 |
常用Channel:
ServerSocketChannel:服务器通道,用于监听新连接(对应OP_ACCEPT)SocketChannel:客户端/服务端连接通道,用于读写数据(对应OP_READ/OP_WRITE)DatagramChannel:UDP通道
3.2 Buffer(缓冲区)——数据的"中转站"
Buffer是NIO中数据读写的中转站。所有的数据都通过Buffer来处理。
Buffer的核心属性:
【ByteBuffer内部结构图解】
position limit capacity
| | |
v v v
+----------+---------+------------+
| 已读/已写 | 未读数据 | 空闲区域 |
+----------+---------+------------+
0 position limit capacity
- capacity: 缓冲区的总容量(创建时指定,不可变)
- position: 当前读写位置(写模式下=已写字节数;读模式下=已读字节数)
- limit: 读写限制(写模式下=capacity;读模式下=已写字节数)
Buffer的四大操作:
// 1. 写入数据到Buffer
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("Hello".getBytes());
// 2. 切换为读模式(flip是关键!)
buf.flip(); // position=0, limit=position(5)
// 3. 从Buffer读取数据
byte[] dst = new byte[buf.remaining()];
buf.get(dst);
System.out.println(new String(dst)); // 输出:Hello
// 4. 清空Buffer(准备下一次写入)
buf.clear(); // position=0, limit=capacity
NIO编程第一大坑:忘记调用
flip(),导致读不到数据!Netty的ByteBuf通过读写两个指针完美解决了这个问题。
3.3 Selector(选择器)——NIO的"大脑"
Selector是Java NIO多路复用的核心。一个Selector可以监视多个Channel的IO事件。
Selector的使用流程:
// 1. 创建Selector
Selector selector = Selector.open();
// 2. 创建Channel并设置为非阻塞
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
// 3. 将Channel注册到Selector,并指定要监听的事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 4. 循环调用select()(阻塞,直到有事件到达)
while (true) {
selector.select(); // 阻塞,直到有Channel就绪
// 5. 获取就绪的SelectionKey
Set<SelectionKey> keys = selector.selectedKeys();
// 6. 处理每个就绪的Channel
for (SelectionKey key : keys) {
if (key.isAcceptable()) { /* 处理新连接 */ }
if (key.isReadable()) { /* 处理读事件 */ }
if (key.isWritable()) { /* 处理写事件 */ }
}
// 7. 清空已处理的key(重要!)
keys.clear();
}
四、Java NIO的"九九八十一难"
虽然Java NIO功能强大,但在实际使用中存在大量"坑"。这也是为什么我们需要Netty。
【Java NIO常见坑点一览】
坑点 表现 Netty的解决方案
────────────────────────────────────────────────────────────────────
epoll空轮询Bug CPU 100%,Selector死循环 重建Selector(rebuildSelector)
ByteBuffer的flip 经常忘记调用flip() ByteBuf有独立的读写指针
TCP粘包/拆包 数据不完整 自带多种Decoder解决
直接内存泄漏 OOM Buffer池化 + 引用计数
跨平台兼容性 Linux/Windows行为不一致 Epoll/KQueue原生传输层
没有HTTP/WS支持 要自己实现协议 内置HttpServerCodec等
五、如何选择合适的IO模型?
不同场景适合不同的IO模型,以下是选型建议:
【Java IO模型选型指南】
场景 推荐模型 理由
────────────────────────────────────────────────────────────
连接数少(<1000) BIO/NIO 简单够用,没必要上Netty
连接数多(>10000) Netty(NIO) 多路复用,一个线程管所有连接
低延迟、高吞吐 Netty(NIO) 零拷贝+内存池,性能极致
HTTP/WebSocket服务 Netty 内置协议支持,开箱即用
简单客户端程序 NIO/AIO 直接用JDK NIO也够用
总结
- Linux五种IO模型:阻塞IO → 非阻塞IO → IO多路复用 → 信号驱动IO → 异步IO,效率依次提升
- Java IO四次进化:BIO(一个连接一个线程)→ 伪异步IO(线程池缓冲)→ NIO(多路复用)→ AIO(异步回调)
- Java NIO三大核心:Channel(通道)、Buffer(缓冲区)、Selector(多路复用器)
- NIO的痛点:API复杂、TCP粘包要自己处理、epoll空轮询Bug、跨平台兼容性问题
- 为什么需要Netty:Netty在JDK NIO之上提供了生产级的封装,解决了NIO的所有痛点,是Java网络编程的最优选择
上一篇【第01篇】Netty是什么——从NIO地狱到网络编程天堂的蜕变
下一篇【第03篇】手把手搭建Netty开发环境——5分钟从零到HelloWorld
更多推荐



所有评论(0)