【问题标题】:Very Strange Problem sending data via Sockets in C#非常奇怪的问题在 C# 中通过套接字发送数据
【发布时间】:2011-01-31 19:17:30
【问题描述】:

对于这篇冗长的帖子,我深表歉意。在传达问题的同时,我已将其尽可能小。

好吧,这快把我逼疯了。我有一个客户端和一个服务器程序,都是用 C# 编写的。服务器通过 Socket.Send() 向客户端发送数据。客户端通过 Socket.BeginReceive 和 Socket.Receive 接收数据。我的伪协议如下:服务器发送一个两字节(短)值,指示实际数据的长度,紧接着是实际数据。客户端异步读取前两个字节,将字节转换为短字节,然后立即从套接字同步读取那么多字节。

现在每隔几秒左右一个周期可以正常工作,但是当我提高速度时,事情变得很奇怪。似乎客户端在尝试从两字节长度读取时会随机读取实际数据。然后它会尝试将这两个任意字节转换为一个短字节,这会导致一个完全不正确的值,从而导致崩溃。以下代码来自我的程序,但经过修剪以仅显示重要的行。

服务器端发送数据的方法:

private static object myLock = new object();
private static bool sendData(Socket sock, String prefix, byte[] data)
{
    lock(myLock){
        try
        {
            // prefix is always a 4-bytes string
            // encoder is an ASCIIEncoding object    
            byte[] prefixBytes = encoder.GetBytes(prefix);
            short length = (short)(prefixBytes.Length + data.Length);

            sock.Send(BitConverter.GetBytes(length));
            sock.Send(prefixBytes);
            sock.Send(data);

            return true;
        } 
        catch(Exception e){/*blah blah blah*/}
    }
}

接收数据的客户端方法:

private static object myLock = new object();
private void receiveData(IAsyncResult result)
{
    lock(myLock){
        byte[] buffer = new byte[1024];
        Socket sock = result.AsyncState as Socket;
        try
        {
            sock.EndReceive(result);
            short n = BitConverter.ToInt16(smallBuffer, 0);
            // smallBuffer is a 2-byte array

            // Receive n bytes
            sock.Receive(buffer, n, SocketFlags.None);

            // Determine the prefix.  encoder is an ASCIIEncoding object
            String prefix = encoder.GetString(buffer, 0, 4);

            // Code to process the data goes here

            sock.BeginReceive(smallBuffer, 0, 2, SocketFlags.None, receiveData, sock);
        }
        catch(Exception e){/*blah blah blah*/}
    }
}

可靠地重现问题的服务器端代码:

byte[] b = new byte[1020];  // arbitrary length

for (int i = 0; i < b.Length; i++)
    b[i] = 7;  // arbitrary value of 7

while (true)
{
    sendData(socket, "PRFX", b);
    // socket is a Socket connected to a client running the same code as above
    // "PRFX" is an arbitrary 4-character string that will be sent
}

查看上面的代码,可以确定服务器将永远发送数字1024,包括前缀在内的总数据的长度,作为一个短(0x400)后跟ASCII二进制中的“PRFX”后跟一个一堆 7 (0x07)。客户端将永远读取前两个字节 (0x400),将其解释为 1024,将该值存储为 n,然后从流中读取 1024 个字节。

这确实是它在前 40 次左右的迭代中所做的,但是,客户端会自然而然地读取前两个字节并将它们解释为 1799,而不是 1024!十六进制的 1799 是 0x0707,这是两个连续的 7!那是数据,而不是长度!那两个字节怎么了?无论我在字节数组中放入什么值都会发生这种情况,我只选择了 7,因为很容易看出与 1799 的相关性。

如果您此时仍在阅读,我为您的奉献精神鼓掌。

一些重要的观察:

  • 减少 b 的长度会增加问题发生前的迭代次数,但不会阻止问题的发生
  • 在循环的每次迭代之间添加显着延迟可能会阻止问题发生
  • 当在同一主机上同时使用客户端和服务器并通过环回地址连接时,不会发生这种情况。

如前所述,这让我发疯!我总是能够解决我的编程问题,但这个问题完全难倒了我。因此,我在这里恳求有关此主题的任何建议或知识。

谢谢。

【问题讨论】:

    标签: c# arrays sockets byte


    【解决方案1】:

    我怀疑您假设整个消息都通过一次呼叫传递给receiveData。一般来说,情况并非如此。碎片化、延迟等最终可能意味着数据以运球形式到达,因此您可能会在仅存在时调用 receiveData 函数,例如900 字节准备就绪。你的电话sock.Receive(buffer, n, SocketFlags.None); 说“把数据给我到缓冲区,最多n 字节” - 你可能会收到更少,实际读取的字节数由Receive 返回。

    这解释了为什么减少 b、增加更多延迟或使用同一主机似乎可以“解决”问题 - 使用这些方法(更小 b,更小的数据;添加延迟意味着管道中的整体数据更少;本地地址没有网络)。

    要查看这是否确实是问题所在,请记录Receive 的返回值。如果我的诊断正确,它偶尔会小于 1024。要修复它,您需要等到本地网络堆栈的缓冲区包含所有数据(不理想),或者在数据到达时接收数据,将其存储在本地,直到整个消息准备好并随后处理它。

    【讨论】:

    • 这是我曾经遇到的一个问题。尝试在while循环中从网络中读取数据,直到读取到“length”参数中指定的数据量
    • 重新登录1024;它当然可能在读取 2 字节标头时失败
    • @Marc - 我的意思是,通过记录 Receive 的返回值,如果底层网络在一次 Receive 中提供的消息少于整个消息,您将在给定的测试用例中看到小于 1024。
    • 这修复了它!我只是将接收代码卡在等待所有数据的 while 循环中。我不敢相信我没有意识到这一点!非常感谢!
    【解决方案2】:

    在我看来,这里的主要问题是不检查 EndReceive 的结果,即读取的字节数。如果套接字打开,这可以是您指定的最大值 &gt; 0&lt;=。假设因为您要求 2 个字节,所以读取了 2 个字节是不安全的。读取实际数据时同上。

    您必须始终循环,累积您期望的数据量(并减少剩余计数,并相应地增加偏移量)。

    同步/异步的混合也不会帮助它变得容易:£

    【讨论】:

      【解决方案3】:
      sock.Receive(buffer, n, SocketFlags.None);
      

      你没有检查函数的返回值。套接字将返回 最多 'n' 个字节,但只会返回可用的内容。您可能没有通过此读取接收到全部“数据”。因此,当您执行下一次套接字读取时,您实际上得到的是剩余数据的前两个字节,而不是下一次传输的长度。

      【讨论】:

        【解决方案4】:

        当您使用套接字时,您必须预计套接字传输的字节数可能比您预期的要少。您必须循环 .Receive 方法以获取剩余的字节。

        当您通过套接字发送字节时也是如此。您必须检查发送了多少字节,并在 Send 上循环,直到发送完所有字节。

        此行为是由于网络层将消息拆分为多个数据包所致。如果您的消息很短,那么您遇到这种情况的可能性就较小。但你应该始终为它编写代码。

        每个缓冲区的字节数越多,您很可能会看到发送者的消息被分成多个数据包。每个读取操作都会收到一个数据包,这只是您的消息的一部分。但也可以拆分小的缓冲区。

        【讨论】:

          【解决方案5】:

          我的猜测是smallBuffer 是在接收方法之外声明的,并且正在被多个线程重用。换句话说,这不是线程安全的。

          编写好的套接字代码很难。由于您同时编写客户端和服务器,因此您可能想看看Windows Communication Foundation (WCF),它可以让您轻松发送和接收整个对象。

          【讨论】:

          • 感谢您的回答。我忘了提到这两种方法都在一个锁块中。我将适当地更新代码,以便其他人可以看到我是否正确锁定它。
          猜你喜欢
          • 2011-08-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-12-20
          • 2018-05-23
          • 2011-03-28
          • 2016-08-03
          相关资源
          最近更新 更多