【问题标题】:TCP packets out of order in Unity TCP client connection on Mac only仅 Mac 上的 Unity TCP 客户端连接中的 TCP 数据包无序
【发布时间】:2021-02-17 09:24:29
【问题描述】:

我有一个使用 TCP 连接到服务器的 Unity 脚本。服务器发送任意长度的消息。消息的长度在消息的前 4 个字节中描述,我用它来确定完整消息何时到达。出于某种原因,消息没有以正确的顺序“缝合”在一起。但是,此问题仅在 Mac 上发生。相同的脚本在 Windows 上运行良好(同样版本的 Unity)。有什么想法吗?

Unity 版本:2018.4.19f

连接代码如下(为了问题的简单,我删掉了一些东西):

using System;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

public class ServerConnection : MonoBehaviour {
    // Example connection parameters
    public string IP = "192.168.0.10"; 
    public int Port = 8085;

    /// The task object where TCP connection runs.
    Task ServerTask { get; set; }

    TcpClient Client { get; set; }
    Socket Socket { get; set; }
    string Message { get; set; }

    /// Gets called on the push of a button
    public void Connect() {
        ServerTask = Task.Factory.StartNew(listenForDataAsync, TaskCreationOptions.LongRunning);
    }

    async private void listenForDataAsync() {
        Debug.Log(string.Format("Connecting to server: IP={0}, Port={1}", IP, Port));
        try {
            Client = new TcpClient(IP, Port);
            Debug.Log("Connection established");

            byte[] message = new byte[0];
            byte[] msgSize = new byte[4];
            int messageSize = 0;
            using (NetworkStream stream = Client.GetStream()) {
                while (true) {
                    // Wait for 4 bytes to get message size
                    await stream.ReadAsync(msgSize, 0, 4);

                    // Convert message size byte array to an int
                    int totalMessageSize = BitConverter.ToInt32(msgSize, 0);
                
                    // subtract 4 from total message size (4 bytes previously read)
                    messageSize = totalMessageSize - 4;

                    // Wait for the rest of the message
                    message = new byte[messageSize];
                    int readBytes = 0;
                    while (messageSize != 0) {
                        readBytes = await stream.ReadAsync(message, readBytes, messageSize);
                        messageSize -= readBytes;
                    }

                    // Decode byte array to string
                    string response = System.Text.ASCIIEncoding.ASCII.GetString(message);

                    // On Mac, response has the message out of order. 
                    // On Windows, it is always perfectly fine.
                }
            }
        }
    }
}

编辑: 服务器是使用 Qt 在 C++ 中实现的,并且在 Windows 机器上运行。服务器发送可能包含大量数据的响应。整个消息由 4 个字节组成,指示整个消息的长度,然后是响应本身。响应是一个 Json 字符串。我得到的错误是从上面的代码中获得的最终字符串的顺序与发送时的顺序不同。消息以} 结尾(关闭外部 Json 对象)。但是,在收到的响应中,在} 之后,还有大量其他值应该是 Json 对象内的数组的一部分。举个例子,服务器发送的响应如下:

{data: [0, 1, 2, 3, 4, 5, 6, 7]}

上面的代码是这样的:

{data: [0, 1, 2]}, 3, 4, 5, 7

这只发生在响应开始达到千字节范围(或更高)时,我强调,完全相同的代码在 Windows 上运行良好。

服务器通过以下方式发送消息:

// The response in Json
QJsonObject responseJson;

// Convert to byte array
QJsonDocument doc(responseJson);
QByteArray message = doc.toJson(QJsonDocument::JsonFormat::Compact);

// Insert 4 bytes indicating length of message
qint32 messageSize = sizeof(qint32) + message.size();
message.insert(0, (const char*)&messageSize, sizeof(qint32));

// Send message
// In here, socket is a QTcpSocket
socket->write(message);

Edit #2 这是输出的图像。 } 之后的值应该是 Message 之前的大数组的一部分。

【问题讨论】:

  • 我要提防的一件事——你正确地循环接收消息的正文——但你假设你总是会一次性获得描述长度的 4 个字节。这种假设也可能不合理。
  • 我之前考虑过这个问题,但我认为第一个数据包不太可能小于 4 个字节(或者是吗?)。为了安全起见,我想我可以一次读取 1 个字节,谢谢指出。
  • TCP 不会乱序传送数据。甚至在 Mac 上也没有。您在某处的代码中存在错误,并且 任何 假设您对哪些字节一起交付所做的假设是完全无效的假设。 TCP 只保证字节将按顺序传送,没有其他保证。每当您未能检查读取操作的字节数时,您的代码都是错误的,需要修复。如果您已经以文本形式发送数据,那么您可能会发现最好使用StreamReaderStreamWriter 并让换行符作为数据分隔符。如果要读取二进制数据,请...
  • ...查看BinaryReader,因为它封装了用于读取给定类型数据的所有必要字节的逻辑(但要小心其字符串处理...它使用了一种不寻常的方案编码字符串)。
  • 说了这么多,您的问题太模糊,无法提供实际答案。您需要提供minimal reproducible example,当涉及网络时,它意味着连接的两个端点,您需要更清楚问题是什么。 “乱序”可能意味着任何事情。您需要准确地解释代码的作用,以及它与您的预期有何不同。

标签: c# macos unity3d tcpclient


【解决方案1】:

任何时候你看到Read/ReadAsync而不捕获和使用结果:代码就是错误的。

幸运的是,这不是一个巨大的修复:

int toRead = 4, offset = 0, read;
while (toRead != 0 && (read = stream.ReadAsync(msgSize, offset, toRead)) > 0)
{
    offset += read;
    toRead -= read;
}
if (toRead != 0) throw new EndOfStreamException();

附带说明:BitConverter.ToInt32 很危险 - 它假定 CPU 字节序,不是固定的。最好使用BinaryPrimitives.ReadInt32BigEndianBinaryPrimitives.ReadInt32LittleEndian

【讨论】:

  • 感谢BinaryPrimitives 的提示。我确实计划更改接收前 4 个字节的方式,因为我不能假设所有 4 个字节都会同时到达。但是,您的建议并没有解决我的问题。
  • @Velchivz 您可能还想在第二个读取循环中检查您的偏移逻辑并比较/对比:stream.ReadAsync(message, readBytes, messageSize); 看起来也错误(偏移量应该严格增加,而不是跳到最后一个读取返回)。除此之外,您将不得不添加日志记录。您要求我们在不访问网络字节的情况下远程调试网络代码
  • 知道了!您在偏移量上是正确的。这很奇怪,因为这适用于 Windows。知道为什么会这样吗?
  • @Velchivz 它是偶然工作的,可能是由于网络堆栈中的缓冲区大小不同;但代码仍然被破坏,它最终在 Windows 上失败
猜你喜欢
  • 1970-01-01
  • 2022-01-16
  • 2010-11-18
  • 2017-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-27
相关资源
最近更新 更多