这是不是read
我们在socket网络编程中如果采用了非阻塞的方式,通常在accept新链接的时候,设置为非阻塞,如下代码:
java
Set<SelectionKey> selectedKeys = selector.selectedKeys();
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 初始只注册读事件,不注册写事件(因为没有数据要发)
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("New connection accepted: " + clientChannel.getRemoteAddress());
}
每当有新链接过来的时候 都会设置为非阻塞 clientChannel.configureBlocking(false);,并且在对应的channel上注册读事件clientChannel.register(selector, SelectionKey.OP_READ);。
当你将一个 Channel(如 SocketChannel)配置为非阻塞模式(channel.configureBlocking(false))后,调用 read(ByteBuffer buf) 的行为是:
- 立即执行:方法会立刻尝试从内核缓冲区读取数据到用户缓冲区。
- 立即返回:无论是否有数据,方法都不会挂起当前线程,而是马上返回一个整数结果。
在非阻塞模式下,read() 的返回值直接告诉你当前的状态:
- 返回值 > 0 :成功读到数据 。表示内核缓冲区中有数据,且已复制到你的
ByteBuffer中。这是你真正处理业务逻辑的时候。 - 返回值 = 0 :暂时没数据 。表示当前内核缓冲区中没有可用数据,但连接是正常的。线程不会阻塞,程序继续向下执行。通常你需要稍后再次尝试读取。
- 返回值 = -1 :连接断开。表示对端已经关闭了连接(正常关闭)。
但是实际上 我们不会一直轮训,这会浪费大量 CPU 资源(因为大部分时间返回都是 0)。
标准的做法是结合 Selector(选择器)使用:
-
注册事件 :将非阻塞的
Channel注册到Selector上,并关注OP_READ事件。 -
等待就绪 :调用
selector.select()。这个方法会阻塞(或超时),直到至少有一个注册的通道发生了你感兴趣的事件(即内核缓冲区有数据了,操作系统认为"可读"了)。 -
触发读取 :当
select()返回后,遍历SelectionKey,只有当key.isReadable()为true时,才去调用channel.read()。 -
此时调用
read()几乎 guaranteed 能读到数据(返回值 > 0) ,或者在对端刚关闭时读到 -1。
read 是系统调用吧
虽然非阻塞避免了线程挂起,但如果你使用轮询 (不断循环调用 read),会导致频繁的系统调用和上下文切换,极大地消耗 CPU 资源。这就是为什么必须配合 Selector来减少无效的系统调用次数。
这是不是write
写操作(write)在非阻塞模式下也是非阻塞的
当你调用 channel.write(buffer) 时:
-
立即返回 :方法不会等待数据真正发送到网卡或对方收到确认,它只是尝试将数据从你的
ByteBuffer复制到操作系统的内核发送缓冲区(Socket Send Buffer) 。 -
返回值含义:
- 返回值 > 0 :成功写入的字节数。注意,这不一定等于你缓冲区里剩余的所有数据(可能只写了一部分)。
- 返回值 = 0:
text
当 `write` 返回 `0` 时,发生的逻辑链条如下:
1. **尝试拷贝**:Java 调用系统调用,试图把 `ByteBuffer` 里的数据拷贝到操作系统的**发送缓冲区**(Socket Send Buffer)。
1. **检查空间**:操作系统发现,**发送缓冲区已经满了**(里面的数据还没被网卡发走,或者发走的速度赶不上写入的速度)。
1. **非阻塞策略**:因为设置了**非阻塞模式**,操作系统**绝不等待**(不睡觉、不挂起线程)。
1. **立即拒绝**:既然不能等,且现在没地方放数据,操作系统只能**立刻拒绝**这次拷贝请求。
1. **返回结果**:
- 在 Linux 底层,则会返回 `-1` 并设置错误码 `EAGAIN` (Try again later)。
- Java NIO 封装了这个行为,将其转换为 **`0`**,意思是:"这次操作**没有写入任何字节**"。
- 返回值 = -1:连接断开。
write 也是系统调用吧
每一次 channel.write() 调用,确实都会触发一次系统调用(从用户态切换到内核态)。
场景一:幸运情况(一次性写完)✅
-
数据量:很小(例如 100 字节)。
-
缓冲区状态:发送缓冲区很空(因为网卡发送速度很快,或者之前没发什么数据)。
-
过程:
- 调用
channel.write(buffer)。 - 操作系统发现缓冲区空间充足,直接把 100 字节全部拷贝进去。
- 返回结果 :
100(表示写入了 100 字节)。 - 后续操作 :
buffer.hasRemaining()为false。 - 结论 :不需要 注册
OP_WRITE,不需要等待下一次事件循环。本次调用直接结束,效率极高。
- 调用
统计事实 :在正常的网络应用中,80%~90% 的写操作都是一次性完成的。只有在网络极差、发送超大文件、或对端接收极慢时,才会出现写不完的情况。
场景二:不幸情况(分多次写入)⚠️
-
数据量 :很大(例如 10 MB 的文件)或者 突发流量(瞬间涌入大量数据)。
-
缓冲区状态:发送缓冲区满了(或剩余空间不足)。
-
过程:
-
调用
channel.write(buffer)。 -
操作系统只拷贝了 4096 字节(假设缓冲区只剩这么多了),然后说:"满了,不能再拷了"。
-
返回结果 :
4096(小于 buffer 剩余量)。 -
后续操作:
buffer.hasRemaining()仍为true(还有 99% 的数据没发出去)。- 必须 注册
OP_WRITE监听。 - 线程挂起/去处理其他事。
- 等待下一次
Selector通知"可写",再回来继续写剩下的数据。
-
结论 :这种情况下,确实需要在事件循环中多次 调用
write,直到所有数据发完。
-

理解上
write成功只意味着数据成功放入了操作系统的内核缓冲区。数据什么时候真正通过网卡发出去,是由操作系统的 TCP 协议栈控制的(滑动窗口、拥塞控制等),与 Java 线程无关
2.- 极度危险 。如果缓冲区满了,write 返回 0。如果你在 while 循环里不断调用 write,而缓冲区一直没空(比如客户端断网了但没通知),你的 CPU 会瞬间飙到 100%(忙等,Busy Waiting)。
- 正确做法 :
write返回 0 或没写完时,必须停止写入 ,注册OP_WRITE事件,让 Selector 告诉你"缓冲区有空位了"再回来写。
3.SelectionKey.OP_WRITE 代表什么
- 它仅代表: "内核的发送缓冲区里有空间,你可以往里面塞数据了(不会阻塞)。"
- 写完必须取消
OP_WRITE
否则会导致 CPU 100% 空转。平时只监听 OP_READ,只有在有剩余数据待发送时才临时监听 OP_WRITE
5.数据流向 当你调用 channel.write(buffer) 时,发生了一个内存拷贝 操作:
Java ByteBuffer (用户态) --> 拷贝 --> Socket Send Buffer (内核态)
一旦数据进入了内核的 Socket Send Buffer(发送缓冲区) ,write() 方法通常就会返回(在非阻塞模式下)。至于数据什么时候从内核缓冲区真正发到网卡上,那是操作系统内核的事,你的 Java 线程已经不管了。
同理,读操作也有一个 Socket Receive Buffer(接收缓冲区) 在内核里,数据先从网卡到内核缓冲区,你再通过 read() 拷贝到 Java 的 ByteBuffer。