【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 的核心机制之一

相关推荐
zzu123zsw4 小时前
从臃肿的模板调用到声明式契约:深入 UniHttp 动态代理与多渠道生命周期扩展
java·spring boot·microsoft·unihttp
Yuzhiyuxia5 小时前
【AI Agent面试系列】AI Agent记忆机制知识点1
人工智能·面试·职场和发展
源码集结号5 小时前
云诊所门诊系统源码,基于SpringBoot+Vue.js架构,支持多租户设计与Docker容器化部署
java·spring boot·源码·医院信息系统·门诊系统·诊所系统
论文复现现场5 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
啃瓜子的松鼠6 小时前
泛微OAe9-后端二开异步调用
java
秋天的怀念_David6 小时前
第二十一夜 · 数字黑洞与西西弗斯的惩罚
java·程序人生·算法
外收内放6 小时前
Python基础语法练习题(73)
java·前端·python
Wang's Blog7 小时前
Java 项目实战: 交友项目 - 环信后台发送消息测试
java·交友
用户3721574261357 小时前
Java 操作 Word 脚注和尾注:添加、读取与删除
java
考研保研资料分享7 小时前
2025公卫保研清华专硕:三校考核、英文读译与择校复盘
面试