【问题标题】:How does Java NIO break up messages?Java NIO 如何分解消息?
【发布时间】:2017-07-23 16:47:31
【问题描述】:

我正在编写一个与普通 Java 客户端配对的玩具 Java NIO 服务器。客户端使用普通 Socket 向服务器发送字符串消息。服务器接收到消息并将内容转储到终端。

我注意到来自客户端的相同消息每次都以不同的方式分解为字节缓冲区。我了解这是 NIO 的预期行为,但想大致了解 NIO 是如何决定截断消息的?

示例:向服务器发送字符串“这是一条测试消息”。以下是服务器日志的摘录(每行代表接收到的 1 个字节缓冲区)。

Run 1:
Server receiving: this is a test message

Run 2:
Server receiving: t
Server receiving: his is a test message

Run 3:
Server receiving: this is 
Server receiving: a test message

更新 - 问题已解决

我已经安装了 Wireshark 来分析数据包,很明显,随机“分手”是由于我使用 DataOutputStream 作为作者,它逐个字符地发送消息!所以每个角色都有一个数据包...

将作者更改为BufferedWriter 后,我的短消息现在按预期作为单个数据包发送。所以事实是,Java NIO 实际上做了一件聪明的事,并将我的小数据包合并到 1 到 2 个字节缓冲区!


UPDATE2 - 澄清

感谢大家的回复。感谢@StephenC 指出,除非我自己对消息进行编码(是的,我在写信给BufferedWriter 后确实调用了flush()),否则我的消息总是有可能通过多个数据包到达。

所以事实是,Java NIO 实际上做了一件聪明的事,并合并了我的 tiny

实际上,没有。合并发生在 BufferedWriter 层中。当应用程序刷新或关闭 DataOutputStream 或 BufferdWriters 缓冲区填满时,缓冲写入器只会将“一堆”字节传递给 NIO 层。

我实际上指的是我第一次尝试使用DataOutputStream(我从网上的一个例子中得到它,既然你已经指出了,这显然是对类的不正确使用)。 BufferedWriter 没有参与。在那种情况下,我的简单作家就像

DataOutputStream out = new DataOutputStream(socket.getOutputStream());
out.writeBytes("this is a test message");

Wireshark 确认此消息已发送(本地主机上的服务器)1 个字符一个数据包(实际消息总共 22 个数据包,不包括所有 ACK 等)。

我可能错了,但这种行为似乎表明 NIO 服务器将这 22 个数据包组合成 1-2 个字节缓冲区?


我在这里尝试实现的最终游戏是一个简单的 Java NIO 服务器,能够使用 TCP 从各种客户端接收请求和数据流,其中一些可能由第三方用 C++ 或 C# 编写。这不是时间关键,因此客户端可以一次性发送所有数据,服务器可以按照自己的节奏处理它们。这就是为什么我使用普通的Socket 而不是 NIO 客户端在 Java 中编写了一个玩具客户端。因此,这种情况下的客户端不能真正直接操作ByteBuffer,所以我可能需要某种消息格式。我可以完成这项工作吗?

【问题讨论】:

  • 欢迎来到 SO。通常,最好发布您所指的相关代码,而不是试图通过文字来解释该代码。
  • @m0skit0 感谢您的建议。这更像是一个关于 Java NIO 如何工作的一般问题。我将在原始帖子中添加一个示例。 :-)
  • 这不是“NIO 的预期行为”,而是 TCP 的实际行为。 NIO 没有做任何事情来改变这一点。合并数据包的是BufferedWriter,而不是 NIO。

标签: java nio


【解决方案1】:

如果您通过 TCP/IP 套接字发送数据,则没有“消息”本身。您发送和接收的是字节流。

如果您询问是否可以发送 N 个字节的块,并让接收者在一次读取调用中准确地获取 N 个字节,那么答案是不能保证一定会发生。然而,“分解”“消息”的是 TCP/IP 堆栈。不是蔚来。不是 Java。

通过 TCP/IP 连接发送的数据最终会分解成网络数据包进行传输。这通常会根据原始写入请求大小擦除任何“消息”结构。

如果你想在 TCP/IP 字节流的顶部有一个可靠的消息结构,你需要在流本身中对其进行编码;例如使用“消息结束”标记或在每条消息前加上字节数。 (如果你想用花哨的话,你需要在 TCP/IP 流的顶部实现一个“消息协议”。)


关于你的更新,我觉得还是有一些误解:

... 很明显,随机“分手”是由于我使用 DataOutputStream 作为作者,它逐个字符地发送消息!所以每个角色都有一个数据包...

是的,对套接字流的大量小写入可能会导致网络级别的严重碎片。但是,它不会总是。如果由于网络带宽限制或接收器读取缓慢而导致足够的“背压”,那么这将导致更大的数据包。

将 writer 更改为 BufferedWriter 后,我的短消息现在按预期作为单个数据包发送。

是的。向堆栈添加缓冲是好的。但是,您可能正在做其他事情;例如在每条消息后调用flush()。如果您不这样做,那么我希望网络数据包包含一系列消息和部分消息。

此外,如果消息太大而无法放入单个网络数据包中,或者存在严重的背压(见上文),那么无论如何您都可能会在数据包中收到多条/部分消息。无论哪种方式,接收者不应该依赖于每次读取一个(完整的)消息。

简而言之,您可能没有真正解决您的问题!

所以事实上,Java NIO 确实做了一件聪明的事,并合并了我的小

实际上,没有。合并发生在BufferedWriter 层中。当应用程序刷新或关闭DataOutputStreamBufferdWriters 缓冲区填满时,缓冲写入器只会将“一堆”字节传递给 NIO 层。


FWIW - 鉴于您对所做工作的描述,使用 NIO 不太可能有助于提高性能。如果您想最大化性能,您应该停止使用BufferedWriterDataOutputStream。而是“手动”对消息进行编码,将字节或字符直接放入 ByteBufferCharBuffer

(另外,DataOutputStream 用于二进制数据,而不是文本。将一个放在 Writer 前面似乎不正确……如果您真的这样做的话。)

【讨论】:

  • 感谢@StephenC 的回复。虽然我了解 TCP/IP 在数据包级别处理“分解”部分,但我对以下事实很感兴趣,例如我的示例中的短字符串有时可以跨多个字节缓冲区接收。我希望内容可以舒适地放入单个数据包中?这就是为什么感觉就像 NIO 将我的字符串“随机”“分解”成字节缓冲区。
  • @Andy 您可以尝试捕获网络流量(例如使用 Wireshark),以查看您的数据如何划分为数据包。但是对于 TCP,它确实并不总是像短消息 = 单个数据包那样简单。而且即使只是一个数据包,在到达 Java NIO 之前也有好几层处理。
  • @biziclop 谢谢你的建议。我刚刚安装了 Wireshark(由于 Windows localhost 的实现,有点扭曲),问题在 5 分钟内就变得明显了。我将发布对原始问题的更新。干杯。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-30
  • 1970-01-01
  • 2012-04-03
  • 1970-01-01
  • 2018-04-26
  • 1970-01-01
相关资源
最近更新 更多