Netty从0到1系列之NIO

一、核心思想: 为什么需要NIO?

传统的 Java I/O(java.io,又称 BIO - Blocking I/O)是基于阻塞的。

  • :单向的,要么是输入流(InputStream),要么是输出流(OutputStream),不能同时读写。
  • 阻塞:当一个线程调用 read()write() 时,该线程会被阻塞,直到数据被读取到或写入完成。在此期间,线程什么也做不了。

BIO 模型在处理大量连接时非常低效。通常采用“一个连接,一个线程”的方案。当连接数暴涨时,线程数量也随之暴涨,导致巨大的上下文切换开销,最终耗尽系统资源。

NIO (New I/O / Non-blocking I/O) 的出现就是为了解决这些问题:

  1. 非阻塞 I/O:线程可以从通道请求读取数据,但如果尚无数据可用,线程可以立即去做别的事情,而不是被阻塞。
  2. 面向缓冲区:数据被读入或写出一个缓冲区,你可以根据需要前后移动缓冲区,这提供了更大的灵活性。
  3. 多路复用器:使用单个(或少量)线程来管理多个通道(连接),这是高性能的关键。

🎯 目标:用少量线程处理成千上万个连接。

NIO vs BIO

场景BIO(阻塞IO)NIO(非阻塞IO)
10个连接10个线程1个线程即可
1000个连接1000个线程 → 崩溃1~4个线程可支撑
CPU开销高(上下文切换)
编程复杂度简单较复杂(状态管理)
NIO模型
BIO模型
Selector
单线程
连接1: 非阻塞
连接2: 非阻塞
连接3: 非阻塞
连接1: 阻塞读写
线程1
连接2: 阻塞读写
线程2
连接3: 阻塞读写
线程3

二、NIO三大核心组件

Channel、Buffer、Selector

组件说明
Buffer(缓冲区)数据的容器,所有数据都通过 Buffer 读写
Channel(通道)类似流,但可双向读写,支持非阻塞模式
Selector(选择器)实现 I/O 多路复用,监听多个 Channel 的事件

2.1 Channel【通道】

public interface Channel extends Closeable {

	// channel是否已经打开
    public boolean isOpen();
	// 关闭Channel
    public void close() throws IOException;
}

类似于传统的“流”,但是双向的,既可以读,也可以写。它是连接缓冲区和数据源(如文件、套接字)的桥梁。

  • 主要实现
    • FileChannel:用于文件 IO。
    • SocketChannel & ServerSocketChannel:用于 TCP 网络 IO。
    • DatagramChannel:用于 UDP 网络 IO。

在这里插入图片描述

channel vs stream

对比项StreamChannel
方向单向双向
阻塞总是阻塞可设为非阻塞
传输方式逐字节与 Buffer 配合批量传输

2.2 Buffer【缓冲区】

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
	// ...
}

在这里插入图片描述

一个容器对象,所有数据的读写都直接通过缓冲区进行。它本质上是一个内存块数组,并提供了一组方法以便更轻松地使用该内存。

  • 核心属性

    • capacity:容量,缓冲区最大大小,创建后不可变。
    • position:位置,下一个要读取或写入的元素的索引。
    • limit:界限,缓冲区中第一个不应读取或写入的元素的索引。
    • mark:标记,一个备忘位置,通过 mark() 标记,可通过 reset() 恢复到 mark 的位置。
    • 关系:0 <= mark <= position <= limit <= capacity
  • 主要实现ByteBuffer, CharBuffer, IntBuffer 等,最常用的是 ByteBuffer

  • 核心操作

    • allocate(int capacity):分配一块新的缓冲区。
    • put() / get():写入/读取数据。
    • flip():将缓冲区从写模式切换到读模式。limit = position; position = 0;
    • clear():清空缓冲区,准备再次写入。position = 0; limit = capacity;
    • rewind():重读缓冲区。position = 0;

2.3 Selector【选择器】

NIO 的灵魂。它是一个多路复用器,可以监控多个通道的 IO 状态(例如:连接就绪、读就绪、写就绪)。一个单线程的 Selector 可以管理成千上万的通道。

  • SelectionKey:表示一个通道在选择器上的注册 token。它包含:
    • 兴趣集合 (Interest Set):你关心通道的什么事件
      • OP_ACCEPT
      • OP_CONNECT
      • OP_READ
      • OP_WRITE
    • 就绪集合 (Ready Set):通道已就绪的操作集合。
    • 通道 (Channel)选择器 (Selector) 的引用。
    • 附件 (Attachment):可附加一个对象(如一个 Buffer 或会话对象),是强大的扩展机制。

工作流程图
在这里插入图片描述

2.4 工作原理流程图

Selector Thread Selector SelectionKey (ACCEPT) SelectionKey (READ) Client Channel SocketChannel Buffer .select() (阻塞或非阻塞等待) 监控所有注册的通道 返回就绪的Key数量 遍历SelectionKeySet 检查key是否有效 接受连接,创建SocketChannel 配置非阻塞,注册到Selector(OP_READ) alt [isAcceptable()] 获取附加的Buffer .read(ByteBuffer) 处理Buffer中的数据 .clear()/.flip() alt [isReadable()] 从集合中移除已处理的Key loop [处理SelectedKeys] Selector Thread Selector SelectionKey (ACCEPT) SelectionKey (READ) Client Channel SocketChannel Buffer

三、代码示例【NIO echo 服务器】

3.1 示例代码

下面是一个完整的 NIO Echo 服务器实现,它接收客户端的消息并原样返回。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioEchoServer {

    public static void main(String[] args) throws IOException {
        // 1. 创建选择器 (The Multiplexer)
        Selector selector = Selector.open();

        // 2. 创建服务器套接字通道并设置为非阻塞模式
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 必须设置为非阻塞
        serverSocketChannel.bind(new InetSocketAddress(9090)); // 绑定端口

        // 3. 将通道注册到选择器,并指定感兴趣的事件为 ACCEPT(接受连接)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO Echo Server started on port 9090...");

        // 主事件循环
        while (true) {
            // 4. 阻塞等待就绪的通道。参数可设置超时时间。
            selector.select();

            // 5. 获取就绪的 SelectionKey 集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                // 必须先移除,防止下次重复处理
                keyIterator.remove();

                try {
                    if (key.isAcceptable()) {
                        // 6. 处理 ACCEPT 事件:新的客户端连接
                        handleAccept(key, selector);
                    } else if (key.isReadable()) {
                        // 7. 处理 READ 事件:客户端发送了数据
                        handleRead(key);
                    }
                    // 可以处理 isWritable(),但通常只在需要写入大量数据时才注册
                } catch (IOException e) {
                    // 客户端断开连接等异常
                    System.err.println("Error handling client: " + e.getMessage());
                    key.cancel(); // 取消这个键的注册
                    if (key.channel() != null) {
                        key.channel().close(); // 关闭通道
                    }
                }
            }
        }
    }

    private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        // 获取注册的 ServerSocketChannel
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        // 接受连接,获取客户端的 SocketChannel
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false); // 设置为非阻塞模式

        // 欢迎信息
        String welcomeMsg = "Welcome to NIO Echo Server. Type 'exit' to quit.\n";
        ByteBuffer buffer = ByteBuffer.wrap(welcomeMsg.getBytes());
        clientChannel.write(buffer); // 发送欢迎信息

        // 将新客户端通道注册到选择器,对 READ 事件感兴趣
        // 并为每个通道附加一个专属的 Buffer
        clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(256));
        System.out.println("Client connected: " + clientChannel.getRemoteAddress());
    }

    private static void handleRead(SelectionKey key) throws IOException {
        // 获取客户端通道和附加的 Buffer
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        // 清空Buffer,准备读取数据
        buffer.clear();

        int bytesRead = clientChannel.read(buffer);
        if (bytesRead == -1) {
            // 客户端关闭了连接
            System.out.println("Client disconnected: " + clientChannel.getRemoteAddress());
            key.cancel();
            clientChannel.close();
            return;
        }

        // 切换Buffer为读模式
        buffer.flip();
        // 将接收到的数据转换为字符串
        String received = new String(buffer.array(), 0, buffer.limit()).trim();
        System.out.println("Received from " + clientChannel.getRemoteAddress() + ": " + received);

        if ("exit".equalsIgnoreCase(received)) {
            // 如果客户端发送 "exit",则关闭连接
            clientChannel.write(ByteBuffer.wrap("Goodbye!\n".getBytes()));
            System.out.println("Client exited: " + clientChannel.getRemoteAddress());
            key.cancel();
            clientChannel.close();
        } else {
            // Echo 功能:将收到的数据原样发回
            // 注意:write()可能不会一次性写完,需要注册OP_WRITE事件来持续写,
            // 但对于Echo这种小数据量场景,通常一次write就能完成,这里做了简化。
            buffer.rewind(); // 将position重置为0,重新读一遍Buffer
            clientChannel.write(buffer);
        }
    }
}

3.2 客户端测试

客户端测试
你可以使用 telnetnetcat 命令来测试这个服务器:

telnet localhost 9090
# 或
nc localhost 9090

服务器端

在这里插入图片描述

客户端
在这里插入图片描述

四、NIO优缺点与实践经验

4.1 优点

  1. 高性能和高伸缩性:单线程即可管理大量连接,避免了线程创建和上下文切换的巨大开销。这是 NIO 最大的优势。
  2. 更少的资源消耗:与为每个连接创建一个线程相比,线程资源消耗要少得多。

4.2 缺点

  1. API 复杂:相比于 BIO,NIO 的 API 调用复杂得多,开发和调试难度大。
  2. 可靠性编程困难:需要处理许多边缘情况,如 write 不一定能一次写完(需要注册 OP_WRITE 并循环写),TCP 粘包/拆包问题(需要在应用层设计协议,如定长消息、分隔符、长度字段等)。
  3. Bug 与陷阱:如 SelectionKey 必须手动从集合中移除,否则会重复处理;对 ByteBuffer 的状态(position, limit)管理不当会导致错误。

4.3 实践经验总结

  1. 不要阻塞 Selector 线程Selector.select() 所在的线程是核心,其中的所有操作(如 handleRead)都必须快速非阻塞。任何耗时的操作(如数据库查询、复杂计算)都应该交给专门的业务线程池去处理,以免影响其他连接的响应。
  2. 管理好 Buffer
    • 考虑使用 Buffer 池来避免频繁的创建和销毁。
    • 深刻理解 flip(), clear(), rewind() 的含义。
  3. 处理写操作channel.write(buffer) 的返回值表示写入的字节数,它可能无法一次性写完缓冲区中的所有数据。如果需要写入大量数据,应在未写完时注册 OP_WRITE 事件,在 isWritable() 时继续写,写完后再取消注册,以避免 Selector 被不必要的写事件占满。
  4. 使用 Netty 等框架99% 的场景下,你不应该直接使用原生 NIO API 来编写网络应用。 框架如 NettyMina 完美地封装了 NIO 的复杂性,解决了上述所有缺点和陷阱(如粘包拆包、API复杂、线程模型),提供了强大、易用且高性能的抽象。学习 NIO 是为了更好地理解这些框架的原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值