为什么 NetworkStream 的读取方式像这样?

11

我有一个应用程序,使用TCPClient和它的底层NetworkStream通过TCP套接字发送以换行符结尾的消息。

实时数据流每100毫秒传输大约28k的数据进行监测。

我已经剥离了不相关的代码,以下是我们读取数据的基本方法:

TcpClient socket; // initialized elsewhere
byte[] bigbuffer = new byte[0x1000000];
socket.ReceiveBufferSize = 0x1000000;
NetworkStream ns = socket.GetStream();
int end = 0;
int sizeToRead = 0x1000000;
while (true)
{
  bytesRead = ns.Read(bigbuffer, end, sizeToRead);
  sizeToRead -= bytesRead;
  end += bytesRead;

  // check for newline in read buffer, and if found, slice it up, and return
  // data for deserialization in another thread

  // circular buffer
  if (sizeToRead == 0)
  {
    sizeToRead = 0x1000000;
    end = 0;
  }
}

我们看到的症状有点不稳定,这取决于我们发送回来的数据量,存在一种记录“滞后”的情况,其中我们从流中读取的数据与我们传递的数据相比逐渐变得越来越旧(在几分钟的流媒体后,“滞后”达到数十秒的数量级),直到最终所有数据一次性全部抓住,然后循环重复。

我们通过将sizeToRead最大化来解决问题,并且(无论是否需要,我们都这样做了),删除了TcpClient上设置的ReceiveBufferSize,并保持其默认值8192(仅更改ReceiveBufferSize无法纠正它)。

int sizeForThisRead = sizeToRead > 8192 ? 8192 : sizeToRead;
bytesRead = ns.Read(bigBuffer, end, sizeForThisRead);

我认为可能是与Nagle和延迟ACK的交互有关,但wireshark显示根据时间戳和查看数据(其中包含时间戳,并且服务器和客户端时钟在一秒内同步)数据正常到达。

我们在ns.Read之后输出日志,确保问题出现在Read调用而不是反序列化代码中。

所以我的想法是,如果你将TcpClient的ReceiveBufferSize设置得非常大,并且在其基础的NetworkStream的Read调用中传递bytesToRead要比预期到达的字节数多得多,则在Read调用中会发生超时等待这些字节到达,但它仍然无法返回流中的所有内容?这个循环中的每个连续调用都会超时,直到1兆字节缓冲区已满,在end重新设置为0后,它会吸入流中剩余的所有内容,导致它们全部赶上——但是它不应该这样做,因为在下一个迭代中它应该完全清空流(因为下一个sizeToRead仍然大于缓冲区中可用的数据)。

或者也许有些事情我没有考虑到而不能综合考虑-但也许这里聪明的人可以想到一些东西。

或者也许这是预期的行为-如果是这样,为什么?


1
减小sizeToRead是一个错误,它应该始终等于缓冲区大小。 - Hans Passant
为什么它必须是相同的大小? - paquetp
1
因为读取少于已分配缓冲区大小的数据毫无意义。实际上你会抱怨它的行为,逐渐读取更少的数据直到突然再次重置。 - Hans Passant
@JonSkeet - 好的,我按照你的要求尝试在我的本地网络上重现了它。当然,这些独立应用程序没有表现出相同的症状 - 这让我认为它与我运行它的目标特定的东西或者我没有考虑到的不同之处有关。独立程序和上述程序之间的区别在于生成数据的服务器来自类Unix(vxWorks)系统 - 然而,查看wireshark日志,服务器及时传递了数据。目标硬件不同(速度较慢的核心),但CPU利用率很低。 - paquetp
1
我能想到的唯一不同之处在于独立程序和目标之间的.NET框架版本不同,目标使用的是2.0版本,而我在独立程序中使用的是4客户端配置文件。此外,目标运行在较低优先级线程上(以避免因大量数据传入而导致GUI响应变慢),但仅为BelowNormal。 - paquetp
显示剩余6条评论
2个回答

6
这种行为很有趣,让我十分好奇,但是......我却无法看到它。
这个“反”答案提出了一种替代理论,可能可以解释问题中描述的滞后现象。我从问题和评论中推断了一些细节。
目标应用程序是一个交互式 UI 应用程序,具有三个操作线程:
1. 一个TcpClient网络数据消费者。 2. 一个数据队列消费者线程,将结果传递给UI。 3. UI 线程。
为了讨论方便,假设TheDataQueue是一个BlockingCollection实例(任何线程安全的队列都可以)。
BlockingCollection<string> TheDataQueue = new BlockingCollection<string>(1000);

该应用程序有两个同步操作需要等待数据。第一个操作是主要问题的 NetworkStream.Read 调用:

bytesRead = ns.Read(bigbuffer, end, sizeToRead);

第二个阻塞操作发生在将工作队列中的数据编组到UI以进行显示时。假设代码如下:
// A member method on the derived class of `System.Windows.Forms.Form` for the UI.
public void MarshallDataToUI()
{
    // Current thread: data queue consumer thread.
    // This call blocks if the data queue is empty.
    string text = TheDataQueue.Take();

    // Marshall the text to the UI thread.
    Invoke(new Action<string>(ReceiveText), text);
}

private void ReceiveText(string text)
{
    // Display the text.
    textBoxDataFeed.Text = text;

    // Explicitly process all Windows messages currently in the message queue to force
    // immediate UI refresh.  We want the UI to display the very latest data, right?
    // Note that this can be relatively slow...
    Application.DoEvents();
}

在这个应用程序设计中,观察到的滞后发生在网络将数据快速地传递给TheWorkQueue而UI无法及时显示时。
为什么@paquetp的日志显示了NetworkStream.Read的问题? NetworkStream.Read会阻塞直到有可用数据。如果日志报告等待更多数据的已经经过的时间,则会出现明显的延迟。但是TcpClient的网络缓冲区实际上为空,因为应用程序已经读取并排队了数据。如果实时数据流具有突发性,则这种情况经常发生。
你如何解释“最终它会一口气赶上来”?
这是数据队列消费者线程在TheDataQueue中处理积压的结果。
那么数据包捕获和数据时间戳呢?
如果一个项目在TheDataQueue中积压,那么数据时间戳是正确的。但是你还不能在UI中看到它们。数据包捕获的时间戳是准时的,因为网络数据已经由网络线程快速接收并排队。
这不都只是猜测吗?
不是的。有一对自定义应用程序(生成器和消费者)演示了这种行为。
截图显示数据队列被383个项目积压。数据时间戳比当前时间戳滞后约41秒。我暂停了生成器多次以模拟突发的网络数据。
然而,我从未能够使NetworkStream.Read表现出问题所述的行为。

这很棒。不过有一个问题 - 在这种情况下,CPU 利用率不会达到 100% 吗? - paquetp
因为这是由于未能快速处理数据队列的产物,所以它仍然应该足够快地服务于你的应用程序中的网络流中的读取,并读取其中的所有内容 - 不是吗?如果在读取调用中输出bytesRead,是否与UI中的延迟相关,就像我上面描述的那样?我之所以问,是因为当我描述的程序出现这种行为时,CPU利用率有相当大的空闲时间(70%空闲)。 - paquetp
啊,好的,现在我明白了 - 我在用户界面上看到你有一个队列计数 - 你提到它落后了383个项目。在我们的情况下,当数据滞后时,我们的数据队列是空的(因为我们还没有读取数据)。 - paquetp
1
在我的示例应用程序中,我可以从消费者的空数据队列开始,然后突发一些数据并观察队列非常快地填充。之后,当网络缓冲区为空时,消费者展示了处理数据队列的滞后现象。 - Joel Allison
如果您认为这些样例应用程序有助于您的调查,欢迎使用。 - Joel Allison
显示剩余5条评论

1

TcpClient.NoDelay属性获取或设置一个值,该值在发送或接收缓冲区未满时禁用延迟。

NoDelayfalse时,TcpClient不会在网络上传输数据包,直到它收集了大量的传出数据。由于TCP段中存在大量的开销,发送少量的数据是低效的。然而,确实存在需要发送非常少量的数据或期望每个发送的数据包立即响应的情况。您的决策应该权衡网络效率与应用程序要求的相对重要性。

来源:http://msdn.microsoft.com/en-us/library/system.net.sockets.tcpclient.nodelay(v=vs.110).aspx

推送位解释 默认情况下,Windows Server 2003 TCP/IP在满足以下条件之一时完成recv()调用:

  1. 数据到达并设置了PUSH位
  2. 用户recv缓冲区已满
  3. 自最后一次数据到达以来已经过去了0.5秒
如果在运行客户端应用程序的计算机上,TCP/IP实现未在发送操作中设置推送位,则可能导致响应延迟。最好在客户端上进行更正;但是,在Afd.sys中添加了一个配置参数(IgnorePushBitOnReceives),以强制其将所有到达的数据包视为已设置推送位。
尝试减少缓冲区大小,以强制供应商网络实现设置PSH位。
来源:http://technet.microsoft.com/en-us/library/cc758517(WS.10).aspx(在推送位解释下) 来源:http://technet.microsoft.com/en-us/library/cc781532(WS.10).aspx(在IgnorePushBitOnReceives下)

我会在Wireshark日志中检查pushbit,如果没有设置,将回滚我们的修复程序,设置注册表键并查看是否可以消除延迟。如果可以,这个答案将被接受。它肯定听起来可以解释我所看到的情况。 - paquetp
唉 - 推送位被服务器发送数据设置了,所以这不可能是原因。不过这个想法不错。 - paquetp

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