Java NIO:如何多次读取缓冲区?

Java NIO: How can I read into a buffer possibly multiple times?

提问人:sleekster 提问时间:8/6/2023 最后编辑:Peter Mortensensleekster 更新时间:8/7/2023 访问量:86

问:

我正在做一个 Java 项目。 我有以下代码,它是选择器的 while 循环的一部分。

响应应该小于,并以换行符结尾。 但据我了解,有可能(即使我没有观察到这一点,因为消息很小)只有部分消息可以用于.256OP_READ

正如你所看到的,在读取的情况下,我正在分配一个 ,读取它并转换为字符串。Buffer

处理部分消息的适当方法是什么?理想情况下,我想在阅读之前等待整个消息准备就绪。一种幼稚的方法是在连接关闭之前进行 and 读取。Map<SocketChannel, Buffer>

if (key.isReadable()) {
    SocketChannel socketChannel = (SocketChannel) key.channel();

    // Read the response from the server
    ByteBuffer buffer = ByteBuffer.allocate(256);
    int bytesRead = socketChannel.read(buffer);

    if (bytesRead == -1) {
        // The server closed the connection
        socketChannel.close();
        key.cancel();
        channels.remove(socketChannel);
    } else {
        buffer.flip();
        byte[] responseBytes = new byte[buffer.remaining()];
        buffer.get(responseBytes);
        String response = new String(responseBytes);
        if (response.endsWith("\n")) {
            socketChannel.close();
            key.cancel();
            channels.remove(socketChannel);
            responses.add(response);
            System.out.println(response);
        } else {
            System.out.println("need to read more!!!!");
        }
    }
}
Java 非阻塞 java-nio

评论

0赞 user207421 8/7/2023
应在接受连接时分配缓冲区。您可以将其存储为密钥附件,也可以存储在作为密钥附件的对象中。这样一来,您始终在每个连接中读取相同的缓冲区。

答:

2赞 rzwitserloot 8/6/2023 #1

处理部分消息的适当方法是什么?

是的,确实,这种情况可能会发生,异步系统需要处理这种“我们已经完成了一半”的概念。

作为一个广泛的概述,嗯,你说了:缓冲区

如果一条消息已经接收了一半,那么到目前为止您收到的数据只需要存储在某个地方。当你不使用异步,而是使用线程时(你可能应该这样做 - 异步的性能改进几乎总是比粉丝异步博客倾向于兜售的要少得多,以及 Project Loom - 这是 OpenJDK 团队,致力于添加新的语言功能和库以改进 JDK 中的线程概念,其中大部分已经是 JDK20 的一部分, 添加了虚拟线程的概念,可以关闭剩余的一点距离)——所有这些“状态”(例如“半条消息”)实际上都自动存储在堆栈上。使用异步,您必须手动管理所有这些东西。

存储这些东西的预期位置是在缓冲区中 - 而朋友旨在让您有效地做到这一点。ByteBuffer

通过聊天应用程序,以下是使用低级异步 NIO 基元的工作方式:、通道选择器ByteBuffer

情境:您是一个聊天系统,可以让 20 人在一个群组中一起聊天。所有 20 个客户端连接一次并保持长时间运行的连接;客户端可以发送一条消息“SEND stuff to show to show to the other 19 participant goes here”,这将导致其他 19 个连接将一些数据从服务器传输到这 19 个客户端,并带有“SHOW stuff to show to the other 19 participant go here”。

我们目前的状态是所有 20 个连接都发送了它们需要发送的所有内容;他们现在都在等待有人输入一些东西。

这 20 个连接的配置方式都相同

  • 您在 nio 选择器中注册,您目前对发送数据不感兴趣(因为没有什么可发送的),但您对读取数据感兴趣。然后你告诉你的选择器去睡觉,如果有什么“有趣”的事情发生,就叫醒你。你为所有 20 个连接注册这个,有趣的定义是:有一些东西要读 - 现在你的系统进入睡眠状态,因为真的没有什么可做的。
  • Jane 输入了一些东西,因此选择器唤醒了线程并说:给你,这个套接字(Jane 的套接字)现在能够做一些你告诉我你感兴趣的事情:你可以从中读取。
  • 您要求该套接字填充您的缓冲区。缓冲区充满了“SEND Hello ever”,仅此而已。Jane 显然还没有完成输入——数据不会以换行符结尾。
  • 你。。。只需返回睡眠状态 - 无需更改“您感兴趣的事件”列表:如果 20 个套接字中的任何一个有数据要读取,您仍然感兴趣,并且您仍然不感兴趣 20 个套接字中的任何一个是否能够接收写入。这非常重要;如果你说你对此感兴趣,选择器会持续唤醒,你的 CPU 会变为 100%(因为所有 20 个插槽都能够发送数据。我们不在乎,因为还没有任何东西可以发送)。Jane 仍在打字(或者她已经完成了,但网络连接速度很慢),所以选择器会重新进入睡眠状态,因为您感兴趣的 20 件事都不可用。Jane 的插座还没有准备好阅读 - 你阅读了到目前为止的所有内容。那个半生不熟的消息挂在.ByteBuffer
  • 最终,你的 NIO 选择器再次醒来,并告诉你,再一次,Jane 的套接字还有更多要读的。你的代码应该告诉那个套接字:这里有一个字节缓冲区(它的位置在它的一半,就像在里面一样),请尽可能地填充它。插座写字,我们第二次做这个歌舞套路。SEND Hello everybody, how's li
  • NIO 选择器第三次唤醒,你做同样的事情,套接字向它写入“fe!\n”并完成。你做了同样的事情,你已经做过两次了,并检查你的缓冲区是否包含一整条消息,这一次,它确实如此(你看到那个换行符,你就知道它的含义了)。
  • 您现在更新选择器的“我对什么感兴趣”列表(选择器键): 您现在对 20 个套接字中的 19 个套接字的“准备写入”感兴趣,假设在这个系统中,jane 在数据发布之前无法发送更多数据(我不会真正以这种方式设计它,但为了示例的缘故), 所以你更新了 Jane 的套接字,你对任何事情都不感兴趣。即使有更多数据要读取,也不会。它只会停留在网络缓冲区中。
  • 你告诉你的选择器睡觉。该调用会立即结束,因为您对某些事件感兴趣,即:所有这 19 个套接字都已准备好发送,并且您说您对此感兴趣。
  • 你浏览了你的清单,“我感兴趣的东西已经准备好了”,然后你开始发送“SHOW,大家好,生活怎么样!然而,虽然你能够一次性发送第一个套接字的整个东西,但第二个套接字在它返回之前只会发出“SHOW Hello everybod”——它不能再发送了(出于某种原因,这个人在一个很小的网络缓冲区上,谁知道呢)。你做同样的事情:'y, how's life!\n' 仍然在 中,你仍然需要将其发送到套接字 2。ByteBuffer
  • 您仍然对套接字 2 的“准备写入数据”事件感兴趣。最终,你通过所有 19 个套接字,对于其中两个,你无法将整个信息传出去。对于这 2 个,您仍然对“准备发送”感兴趣,对于其他 17 个,您清除这些密钥(您对它们已准备好发送不感兴趣)。最终,你再次醒来,那 2 个准备发送更多;您最终会获得所有数据。在您重新启用的过程中的某个地方,或者对于一个好的聊天应用程序,在所有这些过程中,它一直处于启用状态,您有兴趣从他们那里阅读,以防有人在不是所有内容都出来的情况下输入。

这里发生了各种非常复杂的毛茸茸的情况,你必须处理所有这些情况。例如,想象一下人们如此疯狂地打字,而 Jane 的网络连接是如此不稳定,以至于人们只是设法以比 Jane 的网络所能处理的更高的数量发送数据。包含仍然需要发送给 Jane 的所有数据的数据只会越来越大(每次有人输入消息时,所有 20 个“要发送的东西”缓冲区都会被编辑以附加这些消息。它们不时被“后台处理”(仍然要发送的数据被移到前面,因此在末尾有空间添加更多“也发送这个”)。ByteBuffer

最终,Jane 的缓冲区完全满了。当然,你可以编写一些代码来扩展它(创建一个新的、更大的代码,将原来的较小数据复制到它上面,用这个新的更大的字节缓冲区替换哈希图中的条目),但如果你这样做,最终你会有一个 1 GB 大的“发送”字节缓冲区。在某些时候,你必须做出决定。这是你的应用程序,没有一个标准的答案。您是否要阻止其他 19 个再键入,直到 jane 的缓冲区中有足够的空间?是否要断开 Jane 的连接?你想停止把消息放在 Jane 的缓冲区里吗(她最终不会看到这些东西;一旦她的网络赶上了,她就会看到新消息,但这些消息会丢失给她)。是否要将它们缓存到磁盘?你想放弃 jane 并告诉其他人系统正在这样做吗?无穷无尽的答案 - 你编写代码来做你认为适合你的应用程序的事情。ByteBuffer

如果这听起来非常复杂,那是因为它确实如此!优点是单个线程可以同时读取写入所有 20 个连接,尽管性能提升很小,而且可能是负的(线程不是什么危险的妖怪;20 个线程不算什么,它们的开销以及它们之间的切换是无限的。上面的 NIO 设置也有开销 - 所有这些缓冲区)。

有各种像 Grizzly 这样的框架应该可以更轻松地编写这样的应用程序。