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

相关推荐
七夜zippoe2 小时前
Docker容器化实战:核心概念、镜像制作与多阶段构建全解析
java·jvm·数据库·docker·oracle·容器化
TimberWill2 小时前
优化if else过多的方案(含设计模式处理方式)
java·设计模式
东离与糖宝2 小时前
GraalVM+Project Leyden实战:Spring Boot应用原生编译,Serverless冷启动自由
java·人工智能
今天你TLE了吗3 小时前
JVM学习笔记:第七章——对象实例化、内存布局&访问定位
java·jvm·笔记·学习
w_t_y_y3 小时前
知识体系——MCP(三)io.modelcontextprotocol.sdk(1)开发mcp server
java
亚马逊云开发者3 小时前
人人都能写 OpenClaw Skill!手把手带你做一个自动日报技能
java
weixin_399380693 小时前
Prometheus(普罗米修斯)+grafana 监控Tongweb80909(by lqw)
java·grafana·prometheus
9527出列3 小时前
结合拦截器描述mybatis启动流程
java·mybatis
blues92573 小时前
MySQL 篇 - Java 连接 MySQL 数据库并实现数据交互
java·数据库·mysql