Netty源码分析---SelectionKey与位运算详解

SpringBoot+Vue+MySQL 高校学科竞赛平台 完整可运行版(源码+文档) 本项目基于SpringBoot+Vue+MySQL构建高校学科竞赛管理平台,覆盖学生、教师、领队教师、管理员四大角色,实现竞赛发布、报名、刷题、评分、公示等全流程闭环管理。系统架构清晰,技术主流,配套完整源码、数据库脚本、万字论文、开题报告、答辩PPT及部署文档,支持一键运行,适用于毕业设计、课程设计及院系二次开发,真正实现“开箱即用”。 阅读详情

SelectionKey 与位运算详解

本文详细讲解 Java NIO 中 SelectionKey 的工作原理以及位运算的基础知识

目录


一、SelectionKey 是什么?

核心定义

SelectionKey 是 Channel 和 Selector 之间关系的"纽带"或"令牌"。

生活化比喻

你去银行办业务,拿到一个号码牌:

  • 这个号码牌记录了:
    • 你是谁(Channel)
    • 在哪个窗口办理(Selector)
    • 要办什么业务(感兴趣的事件)
    • 现在可以办哪些业务了(就绪的事件)

SelectionKey 就是这个"号码牌"!


二、SelectionKey 的核心作用

1. 建立 Channel 和 Selector 的关联

// 注册时返回 SelectionKey
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

// 通过 SelectionKey 可以获取 Channel 和 Selector
Channel channel = key.channel();      // 获取关联的通道
Selector selector = key.selector();   // 获取关联的选择器

2. 管理感兴趣的事件(Interest Set)

// 获取当前感兴趣的事件
int interestSet = key.interestOps();

// 判断是否对某个事件感兴趣
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) != 0;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) != 0;

// 修改感兴趣的事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

Interest Set(感兴趣的事件):你想要监听什么(主动设置)

3. 查询就绪的事件(Ready Set)

// 获取已经就绪的事件集合
int readySet = key.readyOps();

// 或者用更方便的方法判断
if (key.isReadable()) {
    // 有数据可以读了
}
if (key.isWritable()) {
    // 可以写数据了
}
if (key.isAcceptable()) {
    // 有新连接可以接受了(服务端)
}
if (key.isConnectable()) {
    // 连接已建立(客户端)
}

Ready Set(就绪的事件):实际发生了什么(被动通知)

关键区别示例:

// 你登记了关心【收快递】和【收信件】
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

// 但实际上只有【收快递】的事情发生了
// 此时 readyOps() 只包含 OP_READ
if (key.isReadable()) {
    System.out.println("有快递到了!");
}

4. 附加自定义对象(Attachment)

// 方式1:注册时附加对象
Object myObject = new MyData();
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, myObject);

// 方式2:注册后附加对象
key.attach(myObject);

// 获取附加的对象
Object attached = key.attachment();
MyData data = (MyData) key.attachment();

实际应用场景:

// 场景1:附加一个ByteBuffer,避免每次都创建
ByteBuffer buffer = ByteBuffer.allocate(1024);
key.attach(buffer);

// 使用时
ByteBuffer buf = (ByteBuffer) key.attachment();
channel.read(buf);

// 场景2:附加业务数据
class ClientInfo {
    String username;
    long loginTime;
}

ClientInfo info = new ClientInfo();
info.username = "张三";
info.loginTime = System.currentTimeMillis();
key.attach(info);

// 后续处理时可以获取
ClientInfo clientInfo = (ClientInfo) key.attachment();
System.out.println("用户:" + clientInfo.username);

5. 取消注册

// 取消这个 Channel 在 Selector 上的注册
key.cancel();

// 取消后,这个 Channel 不再被 Selector 监听
// 下次调用 select() 时,这个 key 会被从 Selector 中移除

SelectionKey 的核心属性和方法总结

核心属性
属性说明获取方法
Channel关联的通道key.channel()
Selector关联的选择器key.selector()
Interest Set感兴趣的事件集合key.interestOps()
Ready Set就绪的事件集合key.readyOps()
Attachment附加的对象key.attachment()
常用方法
// 1. 获取关联对象
Channel channel = key.channel();
Selector selector = key.selector();

// 2. 事件操作
int interestOps = key.interestOps();           // 获取感兴趣的事件
key.interestOps(SelectionKey.OP_READ);         // 设置感兴趣的事件
int readyOps = key.readyOps();                 // 获取就绪的事件

// 3. 判断就绪状态
boolean isReadable = key.isReadable();         // 是否可读
boolean isWritable = key.isWritable();         // 是否可写
boolean isAcceptable = key.isAcceptable();     // 是否可接受连接
boolean isConnectable = key.isConnectable();   // 是否连接完成

// 4. 附加对象
key.attach(object);                            // 附加对象
Object obj = key.attachment();                 // 获取附加对象

// 5. 状态检查
boolean isValid = key.isValid();               // key 是否有效

// 6. 取消注册
key.cancel();                                  // 取消注册

三、位运算基础

什么是位运算?

位运算就是直接对二进制数字的每一位(0 或 1)进行操作的运算。

先理解二进制

我们平时用的是十进制(0-9),计算机用的是二进制(0-1):

十进制    二进制
0    →   0000
1    →   0001
2    →   0010
3    →   0011
4    →   0100
5    →   0101
6    →   0110
7    →   0111
8    →   1000

转换规则:从右往左,每一位代表 2 的 n 次方

二进制: 1001
       ↓↓↓↓
位置:   3210  (从右往左数,从0开始)

计算: 1×2³ + 0×2² + 0×2¹ + 1×2⁰
    = 1×8  + 0×4  + 0×2  + 1×1
    = 8 + 0 + 0 + 1
    = 9 (十进制)

1. 左移运算符 <<

基本概念

<< 表示左移,就是把二进制数字向左移动若干位。

语法:数字 << 移动位数

图解左移
原始数字: 1 (二进制: 0001)

1 << 0  →  0001  (向左移0位,不动)  = 1
1 << 1  →  0010  (向左移1位)       = 2
1 << 2  →  0100  (向左移2位)       = 4
1 << 3  →  1000  (向左移3位)       = 8
1 << 4  → 10000  (向左移4位)       = 16

规律:1 << n 等于 2^n(2 的 n 次方)

1 << 0 = 2⁰ = 1
1 << 1 = 2¹ = 2
1 << 2 = 2² = 4
1 << 3 = 2³ = 8
1 << 4 = 2⁴ = 16
为什么左移一位等于乘以2?
十进制类比:
123 向左移一位(补0) = 1230 = 123 × 10

二进制同理:
101 (5) 向左移一位 = 1010 (10) = 5 × 2

2. 位或运算 |

基本概念

| 表示位或运算,规则:只要有一个是 1,结果就是 1

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
组合多个事件
// 同时关心 READ 和 WRITE
int events = OP_READ | OP_WRITE;

// 计算过程:
OP_READ  = 0001
OP_WRITE = 0100
         ------  (对齐每一位进行或运算)
结果     = 0101  (十进制: 5)

// 验证:
第0位: 0 | 0 = 0
第1位: 0 | 1 = 1  ← OP_READ 的位
第2位: 1 | 0 = 1  ← OP_WRITE 的位
第3位: 0 | 0 = 0
结果: 0101
更多组合示例
// 示例1: READ + WRITE + ACCEPT
int events = OP_READ | OP_WRITE | OP_ACCEPT;

OP_READ   = 00001
OP_WRITE  = 00100
OP_ACCEPT = 10000
          -------
结果      = 10101  (十进制: 21)

// 示例2: 所有事件
int events = OP_READ | OP_WRITE | OP_CONNECT | OP_ACCEPT;

OP_READ    = 00001
OP_WRITE   = 00100
OP_CONNECT = 01000
OP_ACCEPT  = 10000
           -------
结果       = 11101  (十进制: 29)

3. 位与运算 &

基本概念

& 表示位与运算,规则:两个都是 1,结果才是 1

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
判断是否包含某个事件
// 假设 interestOps = 5 (二进制: 0101)
// 表示同时关心 READ 和 WRITE

int interestOps = 5;  // 0101

// 判断是否包含 READ
(interestOps & OP_READ) != 0

// 计算过程:
interestOps = 0101
OP_READ     = 0001
            ------
结果        = 0001  (不等于0,说明包含 READ)

// 判断是否包含 WRITE
(interestOps & OP_WRITE) != 0

// 计算过程:
interestOps = 0101
OP_WRITE    = 0100
            ------
结果        = 0100  (不等于0,说明包含 WRITE)

// 判断是否包含 ACCEPT
(interestOps & OP_ACCEPT) != 0

// 计算过程:
interestOps = 0101
OP_ACCEPT   = 10000
            -------
结果        = 00000  (等于0,说明不包含 ACCEPT)

核心位运算符总结

运算符名称作用示例
<<左移向左移动若干位1 << 2 = 4
|位或组合多个标志OP_READ | OP_WRITE
&位与判断是否包含某个标志(ops & OP_READ) != 0
~位非取反~OP_WRITE

四、SelectionKey 中的位运算

事件常量定义

public static final int OP_READ    = 1 << 0;  // 二进制: 0001 (十进制: 1)
public static final int OP_WRITE   = 1 << 2;  // 二进制: 0100 (十进制: 4)
public static final int OP_CONNECT = 1 << 3;  // 二进制: 1000 (十进制: 8)
public static final int OP_ACCEPT  = 1 << 4;  // 二进制: 10000 (十进制: 16)

详细计算过程

// OP_READ = 1 << 0
原始:  0001  (1)
左移0位: 0001  (不动)
结果:  1 (十进制)

// OP_WRITE = 1 << 2
原始:  0001  (1)
左移2位: 0100  (向左移2位,右边补0)
结果:  4 (十进制)

// OP_CONNECT = 1 << 3
原始:  0001  (1)
左移3位: 1000  (向左移3位,右边补0)
结果:  8 (十进制)

// OP_ACCEPT = 1 << 4
原始:   0001  (1)
左移4位: 10000  (向左移4位,右边补0)
结果:   16 (十进制)

为什么要这样设计?

关键点:每个事件占用不同的二进制位!

OP_READ    = 0001   (第0位是1)
OP_WRITE   = 0100   (第2位是1)
OP_CONNECT = 1000   (第3位是1)
OP_ACCEPT  = 10000  (第4位是1)

这样设计的好处:可以用一个数字同时表示多个事件!

interestOps() 方法返回值详解

返回值类型:int

返回值含义:一个整数,用二进制位表示当前关心的所有事件。

// 假设只关心 READ 事件
key.interestOps(SelectionKey.OP_READ);
int interestSet = key.interestOps();
System.out.println(interestSet);  // 输出: 1
System.out.println(Integer.toBinaryString(interestSet));  // 输出: 1 (二进制: 0001)

// 假设关心 READ 和 WRITE 事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
int interestSet = key.interestOps();
System.out.println(interestSet);  // 输出: 5
System.out.println(Integer.toBinaryString(interestSet));  // 输出: 101 (二进制: 0101)
// 解释: 0001 (READ) | 0100 (WRITE) = 0101 (5)

// 假设关心所有事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_ACCEPT);
int interestSet = key.interestOps();
System.out.println(interestSet);  // 输出: 21
System.out.println(Integer.toBinaryString(interestSet));  // 输出: 10101 (二进制: 010101)
// 解释: 0001 (READ) | 0100 (WRITE) | 10000 (ACCEPT) = 10101 (21)

如何判断返回值中包含哪些事件?

使用位与运算(&):

int interestSet = key.interestOps();

// 使用位与运算判断
if ((interestSet & SelectionKey.OP_READ) != 0) {
    System.out.println("关心 READ 事件");
}

if ((interestSet & SelectionKey.OP_WRITE) != 0) {
    System.out.println("关心 WRITE 事件");
}

if ((interestSet & SelectionKey.OP_ACCEPT) != 0) {
    System.out.println("关心 ACCEPT 事件");
}

if ((interestSet & SelectionKey.OP_CONNECT) != 0) {
    System.out.println("关心 CONNECT 事件");
}

设置多个事件

可以! 使用**位或运算(|)**组合多个事件。

// 关心单个事件
key.interestOps(SelectionKey.OP_READ);

// 关心两个事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

// 关心三个事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_ACCEPT);

// 关心所有事件(不常用)
key.interestOps(
    SelectionKey.OP_READ | 
    SelectionKey.OP_WRITE | 
    SelectionKey.OP_ACCEPT | 
    SelectionKey.OP_CONNECT
);

动态修改关心的事件

// 初始只关心读
key.interestOps(SelectionKey.OP_READ);

// 添加写事件(保留原有的读事件)
int currentOps = key.interestOps();
key.interestOps(currentOps | SelectionKey.OP_WRITE);
// 现在同时关心 READ 和 WRITE

// 移除写事件(只保留读事件)
currentOps = key.interestOps();
key.interestOps(currentOps & ~SelectionKey.OP_WRITE);
// 现在只关心 READ

常见组合场景

// 1. 服务端:只关心接受新连接
serverKey.interestOps(SelectionKey.OP_ACCEPT);

// 2. 客户端连接中:关心连接是否完成
clientKey.interestOps(SelectionKey.OP_CONNECT);

// 3. 正常通信:同时关心读和写
clientKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

// 4. 只读模式:只关心读
clientKey.interestOps(SelectionKey.OP_READ);

// 5. 只写模式:只关心写(缓冲区满时常用)
clientKey.interestOps(SelectionKey.OP_WRITE);

为什么 SelectionKey 用位运算?

  1. 节省空间:一个 int (4字节) 可以表示 32 种事件
  2. 高效:位运算是 CPU 最快的运算
  3. 方便组合:用 | 轻松组合多个事件
  4. 方便判断:用 & 快速判断是否包含某个事件

五、完整示例代码

示例1:位运算演示

public class BitOperationDemo {
    // 定义事件常量
    public static final int OP_READ    = 1 << 0;  // 1
    public static final int OP_WRITE   = 1 << 2;  // 4
    public static final int OP_CONNECT = 1 << 3;  // 8
    public static final int OP_ACCEPT  = 1 << 4;  // 16
    
    public static void main(String[] args) {
        System.out.println("=== 事件常量值 ===");
        System.out.println("OP_READ    = " + OP_READ + 
            " (二进制: " + toBinaryString(OP_READ) + ")");
        System.out.println("OP_WRITE   = " + OP_WRITE + 
            " (二进制: " + toBinaryString(OP_WRITE) + ")");
        System.out.println("OP_CONNECT = " + OP_CONNECT + 
            " (二进制: " + toBinaryString(OP_CONNECT) + ")");
        System.out.println("OP_ACCEPT  = " + OP_ACCEPT + 
            " (二进制: " + toBinaryString(OP_ACCEPT) + ")");
        
        System.out.println("\n=== 组合多个事件(位或运算 |)===");
        
        // 组合 READ 和 WRITE
        int events1 = OP_READ | OP_WRITE;
        System.out.println("READ | WRITE = " + events1 + 
            " (二进制: " + toBinaryString(events1) + ")");
        
        // 组合 READ、WRITE 和 ACCEPT
        int events2 = OP_READ | OP_WRITE | OP_ACCEPT;
        System.out.println("READ | WRITE | ACCEPT = " + events2 + 
            " (二进制: " + toBinaryString(events2) + ")");
        
        System.out.println("\n=== 判断是否包含某个事件(位与运算 &)===");
        
        int interestOps = OP_READ | OP_WRITE;  // 5
        System.out.println("当前关心的事件: " + interestOps + 
            " (二进制: " + toBinaryString(interestOps) + ")");
        
        // 判断是否包含各个事件
        System.out.println("包含 READ?    " + 
            ((interestOps & OP_READ) != 0));
        System.out.println("包含 WRITE?   " + 
            ((interestOps & OP_WRITE) != 0));
        System.out.println("包含 CONNECT? " + 
            ((interestOps & OP_CONNECT) != 0));
        System.out.println("包含 ACCEPT?  " + 
            ((interestOps & OP_ACCEPT) != 0));
        
        System.out.println("\n=== 添加事件 ===");
        int ops = OP_READ;
        System.out.println("初始: " + ops + 
            " (二进制: " + toBinaryString(ops) + ")");
        
        ops = ops | OP_WRITE;  // 添加 WRITE
        System.out.println("添加 WRITE 后: " + ops + 
            " (二进制: " + toBinaryString(ops) + ")");
        
        ops = ops | OP_ACCEPT;  // 添加 ACCEPT
        System.out.println("添加 ACCEPT 后: " + ops + 
            " (二进制: " + toBinaryString(ops) + ")");
        
        System.out.println("\n=== 移除事件(位与非运算 & ~)===");
        ops = ops & ~OP_WRITE;  // 移除 WRITE
        System.out.println("移除 WRITE 后: " + ops + 
            " (二进制: " + toBinaryString(ops) + ")");
    }
    
    // 工具方法:转换为固定长度的二进制字符串
    private static String toBinaryString(int num) {
        String binary = Integer.toBinaryString(num);
        // 补齐到5位
        return String.format("%5s", binary).replace(' ', '0');
    }
}

运行结果:

=== 事件常量值 ===
OP_READ    = 1 (二进制: 00001)
OP_WRITE   = 4 (二进制: 00100)
OP_CONNECT = 8 (二进制: 01000)
OP_ACCEPT  = 16 (二进制: 10000)

=== 组合多个事件(位或运算 |)===
READ | WRITE = 5 (二进制: 00101)
READ | WRITE | ACCEPT = 21 (二进制: 10101)

=== 判断是否包含某个事件(位与运算 &)===
当前关心的事件: 5 (二进制: 00101)
包含 READ?    true
包含 WRITE?   true
包含 CONNECT? false
包含 ACCEPT?  false

=== 添加事件 ===
初始: 1 (二进制: 00001)
添加 WRITE 后: 5 (二进制: 00101)
添加 ACCEPT 后: 21 (二进制: 10101)

=== 移除事件(位与非运算 & ~)===
移除 WRITE 后: 17 (二进制: 10001)

示例2:SelectionKey 完整生命周期

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SelectionKeyLifecycleDemo {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(7397));
        serverChannel.configureBlocking(false);
        
        // 1. 创建 SelectionKey(注册时返回)
        SelectionKey serverKey = serverChannel.register(
            selector, 
            SelectionKey.OP_ACCEPT
        );
        
        System.out.println("=== SelectionKey 创建成功 ===");
        System.out.println("关联的 Channel: " + serverKey.channel());
        System.out.println("关联的 Selector: " + serverKey.selector());
        System.out.println("感兴趣的事件: " + serverKey.interestOps());
        
        while (true) {
            // 2. 等待事件就绪
            selector.select();
            
            // 3. 获取就绪的 SelectionKey 集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove(); // 必须手动移除!
                
                // 4. 检查 key 的状态
                if (!key.isValid()) {
                    System.out.println("Key 已失效");
                    continue;
                }
                
                // 5. 根据就绪事件进行处理
                if (key.isAcceptable()) {
                    System.out.println("=== 处理 ACCEPT 事件 ===");
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    
                    client.configureBlocking(false);
                    
                    // 6. 新连接也注册,返回新的 SelectionKey
                    SelectionKey clientKey = client.register(
                        selector, 
                        SelectionKey.OP_READ
                    );
                    
                    // 7. 附加数据到 SelectionKey
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    clientKey.attach(buffer);
                    
                    System.out.println("新客户端连接,创建新的 SelectionKey");
                }
                
                if (key.isReadable()) {
                    System.out.println("=== 处理 READ 事件 ===");
                    SocketChannel client = (SocketChannel) key.channel();
                    
                    // 8. 获取附加的 Buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    
                    int len = client.read(buffer);
                    if (len > 0) {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        System.out.println("收到数据: " + new String(data));
                        buffer.clear();
                        
                        // 9. 修改感兴趣的事件(现在想写数据)
                        key.interestOps(SelectionKey.OP_WRITE);
                    } else if (len == -1) {
                        // 10. 客户端断开,取消 key
                        System.out.println("客户端断开连接");
                        key.cancel();
                        client.close();
                    }
                }
                
                if (key.isWritable()) {
                    System.out.println("=== 处理 WRITE 事件 ===");
                    SocketChannel client = (SocketChannel) key.channel();
                    
                    ByteBuffer response = ByteBuffer.wrap("Hello Client!".getBytes());
                    client.write(response);
                    
                    // 写完后,改回关注读事件
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
        }
    }
}

示例3:实际应用场景

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;

public class SelectionKeyPracticalDemo {
    
    // 客户端会话信息
    static class ClientSession {
        String clientId;
        long connectTime;
        ByteBuffer buffer;
        
        public ClientSession() {
            this.clientId = UUID.randomUUID().toString();
            this.connectTime = System.currentTimeMillis();
            this.buffer = ByteBuffer.allocate(1024);
        }
    }
    
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        
        // 场景1:服务端 ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(7397));
        serverChannel.configureBlocking(false);
        
        // 服务端通常只关心 ACCEPT 事件
        SelectionKey serverKey = serverChannel.register(
            selector, 
            SelectionKey.OP_ACCEPT
        );
        
        System.out.println("=== 服务端启动,监听端口 7397 ===");
        printInterestOps("服务端", serverKey);
        
        while (true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                
                if (!key.isValid()) {
                    continue;
                }
                
                if (key.isAcceptable()) {
                    // 接受客户端连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    
                    // 场景2:为每个客户端创建会话信息
                    ClientSession session = new ClientSession();
                    
                    // 场景3:客户端 SocketChannel 同时关心 READ 和 WRITE
                    SelectionKey clientKey = client.register(
                        selector,
                        SelectionKey.OP_READ | SelectionKey.OP_WRITE
                    );
                    
                    // 场景4:将会话信息附加到 SelectionKey
                    clientKey.attach(session);
                    
                    System.out.println("\n=== 新客户端连接 ===");
                    System.out.println("客户端ID: " + session.clientId);
                    System.out.println("连接时间: " + session.connectTime);
                    printInterestOps("客户端", clientKey);
                }
                
                if (key.isReadable()) {
                    System.out.println("\n=== 可读事件触发 ===");
                    SocketChannel client = (SocketChannel) key.channel();
                    
                    // 场景5:从 SelectionKey 获取会话信息
                    ClientSession session = (ClientSession) key.attachment();
                    
                    int len = client.read(session.buffer);
                    
                    if (len > 0) {
                        session.buffer.flip();
                        byte[] data = new byte[session.buffer.remaining()];
                        session.buffer.get(data);
                        System.out.println("客户端ID: " + session.clientId);
                        System.out.println("收到数据: " + new String(data));
                        session.buffer.clear();
                        
                        // 场景6:读完数据后,只关心写事件
                        key.interestOps(SelectionKey.OP_WRITE);
                        System.out.println("修改为只关心 WRITE 事件");
                        printInterestOps("客户端", key);
                    } else if (len == -1) {
                        System.out.println("客户端断开: " + session.clientId);
                        key.cancel();
                        client.close();
                    }
                }
                
                if (key.isWritable()) {
                    System.out.println("\n=== 可写事件触发 ===");
                    SocketChannel client = (SocketChannel) key.channel();
                    ClientSession session = (ClientSession) key.attachment();
                    
                    ByteBuffer response = ByteBuffer.wrap(
                        ("Hello, " + session.clientId).getBytes()
                    );
                    client.write(response);
                    
                    System.out.println("发送响应给客户端: " + session.clientId);
                    
                    // 场景7:写完后,改回只关心读事件
                    key.interestOps(SelectionKey.OP_READ);
                    System.out.println("修改为只关心 READ 事件");
                    printInterestOps("客户端", key);
                }
            }
        }
    }
    
    // 工具方法:打印当前关心的事件
    private static void printInterestOps(String name, SelectionKey key) {
        int ops = key.interestOps();
        System.out.println(name + " interestOps: " + ops + 
            " (二进制: " + Integer.toBinaryString(ops) + ")");
        System.out.println("关心的事件:");
        
        if ((ops & SelectionKey.OP_READ) != 0) {
            System.out.println("  - OP_READ (可读)");
        }
        if ((ops & SelectionKey.OP_WRITE) != 0) {
            System.out.println("  - OP_WRITE (可写)");
        }
        if ((ops & SelectionKey.OP_ACCEPT) != 0) {
            System.out.println("  - OP_ACCEPT (接受连接)");
        }
        if ((ops & SelectionKey.OP_CONNECT) != 0) {
            System.out.println("  - OP_CONNECT (连接完成)");
        }
    }
}

六、关键注意事项

1. 必须手动移除已处理的 key

Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    iterator.remove(); // 必须!否则会重复处理
    
    // 处理事件...
}

2. 检查 key 是否有效

if (!key.isValid()) {
    continue; // key 已被取消或 channel 已关闭
}

3. Interest Set 和 Ready Set 的区别

  • Interest Set:你想监听什么(主动设置)
  • Ready Set:实际发生了什么(被动通知)

4. 四种事件类型的使用场景

事件类型使用场景说明
OP_ACCEPT服务端 ServerSocketChannel监听是否有新的客户端连接
OP_CONNECT客户端 SocketChannel监听是否成功连接到服务器
OP_READ客户端/服务端 SocketChannel监听是否有数据可读
OP_WRITE客户端/服务端 SocketChannel监听是否可以写数据

七、总结

SelectionKey 核心要点

  1. SelectionKey 是桥梁:连接 Channel 和 Selector
  2. 管理两种事件集合:
    • Interest Set(感兴趣的事件)
    • Ready Set(就绪的事件)
  3. 可以附加自定义数据:方便业务处理
  4. 动态修改关注的事件:灵活应对不同场景

位运算核心要点

  1. 左移 <<:向左移,相当于乘以 2,用于定义事件常量
  2. 位或 |:有 1 就是 1,用来"组合"多个事件
  3. 位与 &:都是 1 才是 1,用来"判断"是否包含某个事件
  4. 位非 ~:取反,配合位与用来"移除"某个事件

为什么使用位运算?

  1. 节省空间:一个 int 可以表示 32 种事件
  2. 高效:位运算是 CPU 最快的运算
  3. 方便组合:用 | 轻松组合多个事件
  4. 方便判断:用 & 快速判断是否包含某个事件

记忆口诀

  • SelectionKey:Channel 和 Selector 的"结婚证"
  • Interest Set:我想要什么(主动)
  • Ready Set:实际发生了什么(被动)
  • 位或 |:组合事件
  • 位与 &:判断事件
HTTPDebugger实战:iTunes登录协议抓包与签名验证全解析 HTTPS抓包是分析客户端与服务端交互的重要手段,其原理是通过中间人证书解密TLS流量,从而观察请求与响应细节。在调试iTunes登录协议时,抓包能帮助我们定位签名验证失败、x-timestamp过期等棘手问题。然而,许多教程中常见的mumu模拟器抓包、fiddler手机抓包或charles抓包unknown问题,往往让新手陷入工具陷阱。本文以HTTPDebugger为切入点,从TLS握手到认证请求,系统拆解iTunes登录协议的关键字段与签名机制,覆盖证书配置、进程监听、域名过滤等实战经验,辅以Apple 阅读详情

相关推荐

PRPD图谱分析从入门到精通:5种典型放电模式的识别手册

本文详细解析PRPD图谱分析技术,重点介绍电晕放电、悬浮放电等5种典型放电模式的识别方法。通过37组真实案例对比,提供关键识别区和诊断流程图模板,帮助工程师快速掌握变电站设备状态监测的核心技能,提升绝缘缺陷诊断准确率。

weixin_29214199的博客 474

Netty源码分析---位运算详解

本文详细讲解了位运算的基础知识及其在Java NIO中的实际应用。首先介绍了位运算的基本概念和二进制表示方法,重点解析了左移运算符(<<)、位或运算(|)和位与运算(&)三种核心运算符的原理和使用场景。通过SelectionKey中的事件常量定义(OP_READ、OP_WRITE等)具体示例,展示了如何利用位运算高效地组合和判断多个事件状态。文章采用图解方式直观展示运算过程,并解释了这种设计在性能优化中的重要作用,为理解Java NIO底层机制提供了基础支撑。

java_rutu的博客 323

SAP-PP MD04详解一

MD04是SAP运行MRP后的结果查询事务码,其他功能强大,标准功能比如:查询/更改物料主数据、转换计划订单到生产订单/采购申请、查询/更改各个MRP元素对应的单据等等。而且还可以附加标准的菜单或者自定义添加事务码上去。老铁将做一个MD04详细说明系列,本篇为第一篇之MRP元素说明。 我们来看看MD04基本界面 可以看到主要的数据显示界面,有日期、MRP元素、MRP元素数据、再计划日期、收货/需求、可用数量、工厂、库存地点等栏位。光标定位到MRP元素列,按F1即可看到各类MRP元素的说...

weixin_52158549的博客 1万+

InnoDB 内存结构总览:Change Buffer、AHI 与 Log Buffer

本文系统梳理InnoDB内存结构中的ChangeBuffer、Adaptive Hash Index(AHI)与LogBuffer。ChangeBuffer缓存二级索引页不在内存时的变更,减少随机IO,仅在页加载时合并;聚簇索引修改立即执行,因数据本身不可延后。AHI基于高频等值查询自动构建哈希索引,加速定位,仅支持等值操作。LogBuffer为redo log内存缓冲,配合磁盘日志实现崩溃恢复。三者均服务于性能优化,其中ChangeBuffer与AHI共享BufferPool空间,而LogBuffer独立

bksczm的博客 298

ROS学习【11】-----基于ubuntu16.04上Gazebo机器人环境搭建的传感器仿真(camera、kinect、Lidar),并通过Rviz进行数据显示

ROS学习-----基于ubuntu16.04上Gazebo机器人环境搭建的传感器仿真目录一、Gazebo环境搭建前的准备1、将搭建模型下载到我们的gazebo环境下2、升级我们的gazebo版本二、创建配置launch文件参数的world文件1、进入工程包mbot_gazebo2、创建文件夹worlds3、进入该文件夹4、创建playground.world文件5、创建room.world文件三...

qq_42451251的博客 3611

MATLAB实战:用ARMAX模型搞定电力系统惯量实时评估(附完整代码)

本文详细介绍了如何利用MATLAB中的ARMAX模型实现电力系统惯量的实时评估。通过系统辨识技术,结合电力系统的动态特性,提供从数据预处理、模型辨识到实时评估的全流程实战指南,并附完整代码。ARMAX模型在新能源高渗透率电网中展现出显著优势,误差较传统方法降低50%以上。

b9c0d的博客 398

信创环境下PageOffice文档控件适配与实践

信创适配做到文档控件这一层,拼的已经不是"有没有国产版本",而是同一个功能在两类终端上是否真的等价。等价的标准很具体:同一次点击能得到同样的结果,同一份公文能得到同样的版式,同一个项目不需要维护两套代码。这背后是三件并不轻松的工作:服务端用一套接口同时支撑两类终端,客户端把两种系统与四种芯片的安装包分发清楚,文档内核层由产品侧跟着 WPS 的版本走。浏览器这一层的压力,则被收进了产品自己的窗口里——升级节奏由产品掌握,不再交给运气。

zi_wu_xian的专栏 327

保姆级教程:用Python仿真OFDM系统(附抗多径干扰优化方案)

本文提供了一份详细的Python仿真OFDM通信系统教程。通过NumPy和Matplotlib,从二进制比特流开始,逐步构建完整的OFDM系统模型,涵盖IFFT、循环前缀等核心概念。重点探讨了如何对抗多径干扰,并提供了优化方案,通过误码率曲线直观对比性能,适合通信工程学习和物理层开发实践。

weixin_29230649的博客 232

第三篇 第 6 课

前置:第三篇第 1 课(stdx 引入),第二篇第 1 课(std.regex)。环境:仓颉 SDK 1.2.0。本文所有代码与输出均逐行实测。

weixin_41618057的博客 516

基于线性约束最小方差准则(LCMV)的自适应波束形成算法的matlab仿真

波束形成(BeamForming)通过对多路麦克风(常为多路全向麦)信号进行合并,抑制非目标方向信号,增强目标方向信号;进而能够实现对特定方向的聚焦拾音,能够有效提高接收信号的信噪比,也有起到降噪的作用。基本原理是波的干涉:通过调整不同阵列单元信号之间的参数,使得某些角度的信号增强,而另一些角度的信号相互抵消,例如下图中来自±45°的输入信号未被增强,而在0度方向的输入信号同相位叠加后被增强。为了将窄带波束形成扩展至宽带,可以使用频域LCMV波束形成器。

平时工作较为繁忙,私信消息一般晚上回复,谢谢大家~~ 763

YOLOv8改进 | 卷积模块 | 用DWConv卷积替换Conv【轻量化网络】

YOLOv8改进,yolov8,yolov8创新,yolov8涨点

kay_545 7159

V8 机制、手写核心函数与 TypeScript 类型体操

或固定初始化(如数组先定长、对象先给全字段)保持类型与形状稳定;这些都比"换框架"更基础。只能处理纯数据(丢函数、Date、RegExp、循环引用报错)。生产级深拷贝要处理:原始值短路、循环引用、V8(Chrome / Node 的 JS 引擎)不是"解释一行跑一行"那么简单,它用。图:V8 执行流程——解释执行 + 热点优化,优化失败会 Deopt 回退。JS 内存自动管理,但"自动"不等于"不会漏"。是"固定间隔最多执行一次"。手写 Promise 是"懂异步"的试金石。是"停下后才执行最后一次",

qq_38468358的博客 351

CATIA二次开发——遍历几何图形集下的轴系及控制轴系生成位置

CATIA二次开发——轴系相关问题1.遍历几何图形集下的轴系2.解决轴系生成位置的问题 1.遍历几何图形集下的轴系 近期遇到的问题:遍历轴系的时候发现,AxisSystem属性的轴系只有放在轴系集合下,才能被找到,放在几何图形集或其他位置均不能被找到。本来想遍历几何图形集下的轴系元素,但是发现没有对应的属性,只能查找 几何图形集、几何元素、草图等元素,无法直接遍历轴系。 解决办法:(加入选择集,在选择集中筛选轴系类型) HybridBody axisHB = activePart.HybridBodies

WorkMan的博客 2731

【从零理解 Spring AI · 12】MCP 的两种角色:做客户端还是做服务端

上一篇讲了 MCP 的概念:标准协议、三大角色、M+N 替代 M×N。

nice_lcj520的博客 538

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

Java 集合容器高频面试题全解:HashMap 树化阈值、1.7 死循环与 ConcurrentHashMap 演进(2026 招聘 JD 真题复盘)

HashMap 树化阈值 8 的泊松分布依据、退化阈值 6 的防抖设计、JDK 1.7 头插成环死循环的排查与修复、1.8 尾插后为何仍线程不安全、ConcurrentHashMap 放弃分段锁的原因与 ForwardingNode 协作扩容、computeIfAbsent 死锁陷阱、大集合找重复的场景题选型,附三层追问链自测。题目来源为 38 期公开招聘 JD 样本与字节、拼多多、建行、美团、快手公开面经。

蒙眼过河 420

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

spdlog 同步日志库使用详解

spdlog是一个轻量级 C++ 日志库,支持同步日志、异步日志、多线程日志、日志文件轮转和自定义日志格式。对于嵌入式 Linux 应用开发,我建议先使用。它可以帮我们完成:日志级别过滤。时间戳和线程 ID 输出。日志格式化。终端和文件输出。多线程安全写入。日志文件按大小或时间轮转。我们不需要自己实现互斥锁、文件写入、日志格式化和轮转等底层功能。

zengjiajun2006的博客 1120

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

JavaSE通关手册:数据类型与变量

代码无需额外处理,编译时编译器自动完成转换。

xys_678的博客 221

Java 多态 学习笔记

多态是面向对象编程的重要特性,指同一操作作用于不同对象时产生不同行为。其核心在于“父类引用指向子类对象”,结合方法重写实现运行时动态绑定。多态使代码更灵活、可扩展,如用Person p = new Student();统一处理各类人员注册,无需修改调用逻辑。但多态无法直接调用子类特有方法,需通过instanceof判断并强制转换。编译时看左边(类型),运行时看右边(实际对象),实现解耦与维护便利。

AA_AA114514的博客 367

IDEA 简介:从零认识 IntelliJ IDEA

IDEA 全称,是由 JetBrains 公司开发的一款主流 Java 集成开发环境(IDE)。它集代码编辑、编译、调试、测试、版本控制、构建管理等功能于一体,被大量 Java 开发者视为日常开发的首选工具。所谓 IDE(Integrated Development Environment,集成开发环境),就是把写代码所需的各类工具整合到同一个界面中。相比只用文本编辑器加命令行编译,IDE 能显著减少重复操作,让开发者把精力集中在业务逻辑上。

ztI10的博客 174

Java实现批量线段相交检测(跨积判定+扫描线优化与踩坑复盘)

【代码】Java实现批量线段相交检测(跨积判定+扫描线优化与踩坑复盘)

m0_64476639的博客 341

c++ explicit关键字详解

explicit。

拒绝AI文,努力在AI时代写出感动人心的好文章 152

ubuntu安装hadoop系统

本文详细介绍了基于VMware克隆虚拟机搭建三节点Hadoop分布式集群的全过程:使用同一Ubuntu镜像克隆出master、worker1、worker2三台虚拟机,依次配置主机名、固定IP(如192.168.82.10/11/12)、SSH服务及免密登录,确保节点间网络互通。随后安装Java,下载配置Hadoop并分发至各节点,最终初始化启动集群,验证其存储与计算功能,实现高效的大数据环境部署。

qq_43592674的博客 350

TRingBuffer两端都能增删(快递站版)

TRingBuffer 是一个支持两端增删、下标访问且容量动态增长的环形双端队列。它通过无符号索引的永不归零设计,利用减法自动模运算实现空满判断与元素计数,避免了传统环形缓冲浪费格子的问题。内部以票号(索引)标记首尾,扩容时重排元素为连续存储,保证 O(1) 操作与高缓存效率。虽不线程安全,但其高效性使其成为游戏开发中事件队列、命令缓冲等场景的理想选择。

u013768914的博客 310

Mac上装了最新版本的Android Studio,但是没找到jdk的设置选项也没找到jbr的目录

新版Android Studio(2025.3.1+)因三项变更导致常见问题:一、JDK设置移至“DaemonJVM criteria”,新项目默认自动探测或下载JDK;二、JBR目录改名为带版本号的jbr-21等,需进入.app/Contents/查看,或通过About窗口确认运行时;三、命令行缺adb因platform-tools未加入PATH,需在SDK Manager中勾选并配置~/.zshrc中的ANDROID_HOME及PATH。重装或清理残留可解决缺失问题,切勿手动设JAVA_HOME或安装冲

Programming is a highly artistic endeavor 381

Java Arrays.asList 常见坑详解|UnsupportedOperationException、数组联动、基本类型 bug

解析 Arrays.asList 常见开发陷阱,演示 UnsupportedOperationException、底层数组引用共享、基本类型数组问题,区分 Arrays 内部 ArrayList 与 java.util.ArrayList,给出数组转 List 的各种场景正确代码。

weixin_42617033的博客 495

运动控制功能安全实践深度解析 | 安全功能篇(上)

安全功能不只是急停。SLS 限上限、SSR 守区间、SSM 管人员准入,三者约束对象不同,混用等于没设防。本文拆解限速类安全功能的判定逻辑、编码器与无传感器两条采集路径,以及汽车、玻璃、物流、烘焙、机器人等场景的落地差异。

MotroEngineer的博客 253
上一篇: NioEventLoop事件检测与触发机制总结
下一篇: Netty源码分析---register0方法详解
不早睡不改名@
博客等级 码龄5年 536粉丝 306原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值