TcpClient未正确接收数据

3

我有一个客户端服务器程序。

我是这样发送数据的:

private void Sender(string s,TcpClient sock)
{
   try
   {
      byte[] buffer = Encoding.UTF8.GetBytes(s);
      sock.Client.Send(buffer);
   }catch{}
}

并且在客户端接收就像这样:
byte[] buffer = new byte[PacketSize];
int size = client.Client.Receive(buffer);
String request = Encoding.UTF8.GetString(buffer, 0, size);

问题在于并不总是完全接收到数据,有时只接收到我发送的部分数据。 PacketSize 是10240,比我发送的字节数还要多。我已经在双方都设置了 SendBufferSize 和 ReceiveBufferSize。
最糟糕的是,有时候数据是完全接收到的!
可能出了什么问题?
1个回答

8
TcpClient.Receive返回的size值与您发送的缓冲字符串的长度不同。这是因为调用Receive时,不能保证一次性获取与Send调用发送的所有数据相同的数据。这种行为是TCP工作方式固有的(它是基于流而不是基于消息的数据协议)。
您无法通过使用更大的缓冲区来解决此问题,因为您提供的缓冲区只能“限制”Receive返回的数据量。即使您提供了1MB的缓冲区,并且有1MB的数据要读取,Receive也可以合法地返回任意数量的字节(甚至只有1个字节)。
您需要做的是,在调用Encoding.GetString之前确保已缓冲所有数据。为此,您需要知道有多少数据。因此,至少在发送时需要写入字符串字节的长度。
  byte[] buffer = Encoding.UTF8.GetBytes(s);
  byte[] length = BitConverter.GetBytes(buffer.Length);
  sock.Client.Send(length);
  sock.Client.Send(buffer);

当接收数据时,首先会读取长度(已知固定大小为4个字节),然后开始将其余数据缓冲到临时缓冲区中,直到你有了length字节(这可能需要任意数量的Receive调用,因此你需要一个while循环)。只有在这之后才能调用Encoding.GetString并得到原始字符串。
对观察到的行为的解释:
虽然操作系统的网络堆栈几乎不作任何保证,但实际上它通常会给你一个TCP数据包带来一个Receive调用的数据。由于TCP的MTU(最大数据包大小)允许大约1500字节的有效负载,简单的代码只要字符串长度小于此大小就可以正常工作。超过此大小,它将被拆分成多个数据包,并且一个Receive将只返回部分数据。

1
+1。并且不要忘记在TCP客户端/流上设置超时,否则如果发送数据的应用程序行为不当/存在网络问题,您的应用程序将挂起。 - stefan
@Jon,使用异步可能会更好,但这真的取决于应用程序。 - stefan
1
最好有一个起始字节标识符,以确保同步。因此,当您读取所有期望的字节时,下一个字节应该是已知值,然后是下一条消息(或EOF)。 - FlappySocks
@stefan:也许我们并不持不同意见。只是当我听到“标记”这个词时,我会想到“魔法值”,而我认为魔法值对你没有任何好处。 - Jon
通过使用标记/魔术值,您可以构建一个非常动态的协议,而无需更新旧客户端即可支持更多特性。 有时候这是好的,有时候这是非常糟糕的选择,实际上取决于应用程序。 但无论如何,您必须确保获取的数据是您想要的。任何方法都可以 :) - stefan
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接