有没有人懂socketChannel中的write,read方法啊,给我讲讲

2 阅读6分钟

这是不是read

我们在socket网络编程中如果采用了非阻塞的方式,通常在accept新链接的时候,设置为非阻塞,如下代码:

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
 当 `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 字节)。

  • 缓冲区状态:发送缓冲区很空(因为网卡发送速度很快,或者之前没发什么数据)。

  • 过程

    1. 调用 channel.write(buffer)
    2. 操作系统发现缓冲区空间充足,直接把 100 字节全部拷贝进去。
    3. 返回结果100(表示写入了 100 字节)。
    4. 后续操作buffer.hasRemaining() 为 false
    5. 结论不需要注册 OP_WRITE,不需要等待下一次事件循环。本次调用直接结束,效率极高。

统计事实:在正常的网络应用中,80%~90% 的写操作都是一次性完成的。只有在网络极差、发送超大文件、或对端接收极慢时,才会出现写不完的情况。

场景二:不幸情况(分多次写入)⚠️

  • 数据量:很大(例如 10 MB 的文件)或者 突发流量(瞬间涌入大量数据)。

  • 缓冲区状态:发送缓冲区满了(或剩余空间不足)。

  • 过程

    1. 调用 channel.write(buffer)

    2. 操作系统只拷贝了 4096 字节(假设缓冲区只剩这么多了),然后说:“满了,不能再拷了”。

    3. 返回结果4096(小于 buffer 剩余量)。

    4. 后续操作

      • buffer.hasRemaining() 仍为 true(还有 99% 的数据没发出去)。
      • 必须注册 OP_WRITE 监听。
      • 线程挂起/去处理其他事。
      • 等待下一次 Selector 通知“可写”,再回来继续写剩下的数据。
    5. 结论:这种情况下,确实需要在事件循环中多次调用 write,直到所有数据发完。

image.png

理解上

  1. write 成功只意味着数据成功放入了操作系统的内核缓冲区。数据什么时候真正通过网卡发出去,是由操作系统的 TCP 协议栈控制的(滑动窗口、拥塞控制等),与 Java 线程无关

2.- 极度危险。如果缓冲区满了,write 返回 0。如果你在 while 循环里不断调用 write,而缓冲区一直没空(比如客户端断网了但没通知),你的 CPU 会瞬间飙到 100%(忙等,Busy Waiting)。

  • 正确做法write 返回 0 或没写完时,必须停止写入,注册 OP_WRITE 事件,让 Selector 告诉你“缓冲区有空位了”再回来写。

3.SelectionKey.OP_WRITE 代表什么

  • 它仅代表: “内核的发送缓冲区里有空间,你可以往里面塞数据了(不会阻塞)。”
  1. 写完必须取消 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。