【Java】NIO 中的多路复用(Selector / Channel)机制

分析这行代码:key.interestOps(SelectionKey.OP_READ);

java 复制代码
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.SocketChannel;

public class NIOClient {
    public static void main(String[] args) {
        try {
            // 打开一个 SocketChannel
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false); // 设置为非阻塞模式

            // 连接到服务器
            socketChannel.connect(new InetSocketAddress("localhost", 8080));

            // 打开 Selector
            Selector selector = Selector.open();
            socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);

            while (true) {
                // 选择已准备好进行 I/O 操作的通道
                selector.select();

                for (SelectionKey key : selector.selectedKeys()) {
                    if (key.isConnectable()) {
                        // 完成连接
                        SocketChannel client = (SocketChannel) key.channel();
                        if (client.isConnectionPending()) {
                            client.finishConnect();
                            System.out.println("Connected to the server.");
                        }
                    }

                    if (key.isWritable()) {
                        // 发送数据
                        SocketChannel client = (SocketChannel) key.channel();
                        String message = "Hello from the client!";
                        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                        client.write(buffer);
                        System.out.println("Message sent to server: " + message);
                        // 发送完后取消可写事件
                        key.interestOps(SelectionKey.OP_READ); // 只关注读事件
                    }

                    if (key.isReadable()) {
                        // 读取服务器响应
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = client.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip(); // 切换为读模式
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            System.out.println("Received from server: " + new String(data));
                        }
                    }
                }
                selector.selectedKeys().clear(); // 清理已处理的键
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这属于 Java NIO 中的多路复用(Selector/Channel)机制。我会逐步解释:


1️⃣ 背景

Java NIO 中,你可以使用 Selector 来监听多个通道(Channel)的事件,比如:

  • OP_READ:通道可以读数据
  • OP_WRITE:通道可以写数据
  • OP_CONNECT:通道完成连接
  • OP_ACCEPT:服务器通道有新连接

这些事件被称为 "兴趣事件(interestOps)"

SelectionKey 代表通道注册到 Selector 后返回的一个 key,它记录了这个通道的兴趣事件、就绪状态以及附加对象。


2️⃣ interestOps 方法

java 复制代码
key.interestOps(int ops)
  • 用来 设置通道的兴趣事件,告诉 Selector 你关心哪些事件。
  • 参数 ops 是一个位掩码(bit mask),可以是:
java 复制代码
SelectionKey.OP_READ      // 读
SelectionKey.OP_WRITE     // 写
SelectionKey.OP_CONNECT   // 连接完成
SelectionKey.OP_ACCEPT    // 接受新连接
  • 可以组合多个事件,例如:
java 复制代码
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

表示你同时关心读和写事件。


3️⃣ 你的代码:key.interestOps(SelectionKey.OP_READ);

意思就是:

给这个 SelectionKey 注册 只关心读事件

当通道 有数据可读 时,Selector 会通知你。

具体效果:

  • 当你调用 selector.select() 时,如果该通道有数据可读,它会被加入 就绪集合(selected set)。
  • 你就可以安全地调用 channel.read(buffer) 来读取数据。

作用:将兴趣集合设置为 仅 OP_READ。

结果

  • 原来的兴趣集合(比如 OP_WRITE)会被替换掉。
  • Selector 不再监听通道的原先事件,只有读事件仍然被监听。

保留原有事件的方法

如果我想在保留原有事件的基础上添加一个事件 ,不要直接覆盖,而要用位操作

java 复制代码
key.interestOps(key.interestOps() | SelectionKey.OP_READ);

解释:

  • key.interestOps():获取原来的兴趣集合
  • | SelectionKey.OP_READ:使用按位或,添加读事件
  • 结果:保留原有事件,同时添加 OP_READ

同理,如果想移除某个事件:

key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);

  • ~SelectionKey.OP_WRITE:按位与取反,移除写事件。

总结

  • 直接调用 key.interestOps(OP_X)->替换原有兴趣事件
  • 保留原有事件 ->用key.interestOps(key.interestOps() | OP_X)
  • 移除事件 ->用key.interestOps(key.interestOps() & ~OP_X)

4️⃣ 举个例子

假设你写一个聊天服务器,处理客户端消息:

java 复制代码
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
  • 客户端一旦发送消息,Selector 就会检测到通道可读。
  • 服务器就可以安全读取客户端发送的数据。
  • 如果你还想写数据,可以之后:
java 复制代码
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);

这样同时关心读和写。


✅ 总结

  • key.interestOps(SelectionKey.OP_READ);
    • 设置该通道 只关心读事件
    • 当通道有数据可读时,Selector 会通知你
    • 是 NIO 非阻塞 I/O 的核心机制之一

相关推荐
wefly201713 分钟前
纯前端架构深度解析:jsontop.cn,JSON 格式化与全栈开发效率平台
java·前端·python·架构·正则表达式·json·php
nbwenren1 小时前
node.js内置模块之---crypto 模块
java
weyyhdke2 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
chools2 小时前
Java后端拥抱AI开发之个人学习路线 - - Spring AI【第一期】
java·人工智能·学习·spring·ai
jeCA EURG3 小时前
Spring Boot 2.7.x 至 2.7.18 及更旧的版本,漏洞说明
java·spring boot·后端
BduL OWED3 小时前
Redis之Redis事务
java·数据库·redis
FastBean3 小时前
BizAssert:一个轻量级、生产就绪的 Java 业务断言工具类
java·后端
zhuiyisuifeng4 小时前
Node.js使用教程
java
李庆政3704 小时前
Reactor-core 响应式编程 spring-boot-starter-webflux
java·spring boot·reactor·响应式编程·reactor-core
是Smoky呢4 小时前
springAI+向量数据库+RAG入门案例
java·开发语言·ai编程