Skip to content
第 32 / 250 章后端⏱ 10 分钟阅读

第 32 章:IO 流进阶

学习目标

  • 理解 BIO / NIO / AIO 的区别
  • 掌握 Buffer 与 Channel
  • 了解零拷贝与 Netty 的基础

一、三种 IO 模型

模型线程模型适用
BIO1 连接 : 1 线程连接数少(< 1000)、逻辑简单
NIO1 线程 : 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。

七、本章小结

要点关键
BIO1 连接 1 线程,连接多就崩
NIOChannel + Buffer + Selector
Buffer写完必须 flip() 才能读
零拷贝transferTo,不经过 JVM 堆
Selector一线程管万连接
生产建议网络编程用 Netty

动手练习

练习 1:基础题

ByteBuffer 写入 "hello world",再完整读出来打印。注意 flip() 的时机。

练习 2:进阶题

用 NIO Selector 写一个简易 Echo 服务器:客户端发什么,服务端原样返回。用 telnet 测试。


下一章第 33 章:反射基础

本站基于 VitePress 构建 · 由 Codebook 团队维护