第 32 章:IO 流进阶
学习目标
- 理解 BIO / NIO / AIO 的区别
- 掌握 Buffer 与 Channel
- 了解零拷贝与 Netty 的基础
一、三种 IO 模型
| 模型 | 线程模型 | 适用 |
|---|---|---|
| BIO | 1 连接 : 1 线程 | 连接数少(< 1000)、逻辑简单 |
| NIO | 1 线程 : N 连接 | 高并发(Netty / Tomcat NIO / Redis) |
| AIO | 系统回调 | Windows 上可用,Linux 底层仍是 epoll |
BIO 的问题
java
// BIO 服务端:每个连接开一个线程
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept(); // ① 阻塞,直到有连接进来
new Thread(() -> {
InputStream in = socket.getInputStream();
in.read(buffer); // ② 阻塞,直到有数据
}).start(); // ③ 1 万连接 = 1 万线程 → OOM
}为什么线程多就慢? 每个线程栈默认 1MB,1 万线程要 10GB 内存;且 CPU 大量时间花在线程上下文切换上。
二、NIO 三大核心
| 组件 | 作用 |
|---|---|
Channel | 类似流,但双向(可读可写) |
Buffer | 数据必须先进 Buffer,不能直接读写 Channel |
Selector | 一个线程监听多个 Channel 的就绪事件 |
三、Buffer 的四个指针
java
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello".getBytes()); // ① 写模式:position 移到 5
buffer.flip(); // ② 切换读模式:limit=5, position=0
byte b = buffer.get(); // ③ 读一个字节,position=1
buffer.clear(); // ④ 重置为写模式(数据未擦除,但可覆盖)| 指针 | 含义 |
|---|---|
capacity | 总容量,创建后不变 |
position | 下一个读/写的位置 |
limit | 能读/写的上界 |
mark | 标记位置,reset() 回到这里 |
新手必踩坑:写完不
flip()直接读 → 读到的是空数据(position 还在末尾)。
四、Channel 复制文件
java
try (FileChannel in = FileChannel.open(Path.of("src.bin"), StandardOpenOption.READ);
FileChannel out = FileChannel.open(Path.of("dst.bin"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
// ✅ 零拷贝:数据不经过 JVM 堆,直接内核态传输
in.transferTo(0, in.size(), out);
}零拷贝原理
应用:Kafka 高吞吐的秘密之一就是零拷贝(
sendfile系统调用)。
五、Selector 多路复用
java
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // ① 必须设为非阻塞
server.register(selector, SelectionKey.OP_ACCEPT); // ② 注册关心的事件
while (true) {
selector.select(); // ③ 阻塞直到有事件就绪
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // ④ 必须移除,否则重复处理
if (key.isAcceptable()) {
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
client.read(buf);
buf.flip();
System.out.println(StandardCharsets.UTF_8.decode(buf));
}
}
}一个线程就能扛住上万连接——这就是 Netty / Redis / Nginx 高并发的底层模型(epoll)。
六、实际开发怎么选?
| 需求 | 选择 |
|---|---|
| 读写本地文件 | Files.readString / Files.lines(够用) |
| 大文件复制 | FileChannel.transferTo(零拷贝) |
| 写网络服务器 | 不要手写 NIO,用 Netty |
| Web 应用 | Spring Boot 内嵌 Tomcat 已用 NIO,无需操心 |
规约:手写 NIO 极易出 bug(半包、粘包、空轮询 CPU 100%)。生产环境一律用 Netty。
七、本章小结
| 要点 | 关键 |
|---|---|
| BIO | 1 连接 1 线程,连接多就崩 |
| NIO | Channel + Buffer + Selector |
| Buffer | 写完必须 flip() 才能读 |
| 零拷贝 | transferTo,不经过 JVM 堆 |
| Selector | 一线程管万连接 |
| 生产建议 | 网络编程用 Netty |
动手练习
练习 1:基础题
用 ByteBuffer 写入 "hello world",再完整读出来打印。注意 flip() 的时机。
练习 2:进阶题
用 NIO Selector 写一个简易 Echo 服务器:客户端发什么,服务端原样返回。用 telnet 测试。
下一章:第 33 章:反射基础 →