【问题标题】:Why does NetworkStream Read like this?为什么 NetworkStream 会这样读取?
【发布时间】:2014-06-15 10:33:06
【问题描述】:

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

数据以大约每 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;
  }
}

根据我们发回的数据量,我们看到的症状是间歇性地出现,记录会出现“滞后”,我们从流中读取的数据会逐渐变老我们正在交付的内容(在几分钟的流式传输之后,延迟大约为 10 秒),直到最终它全部赶上一个大镜头,然后循环重复。

我们通过最大化 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 设置得非常大,并且在您的 Read 调用中,它的底层 NetworkStream 传递 bytesToRead 比预期到达的字节多得多,那么 Read 就会发生超时调用等待这些字节到达,但它仍然没有返回流中的所有内容?此循环中的每个连续调用都会超时,直到 1 meg 缓冲区已满,之后当 'end' 重置为 0 时,它会吸入流中剩余的所有内容,导致所有内容都赶上 - 但它不应该这样做是因为在我看来,逻辑看起来应该在下一次迭代时完全清空流(因为下一个 sizeToRead 仍然是 > 缓冲区中可用的数据)。

或者也许这是我没有想到的东西,我无法合成 - 但也许这里的那些聪明的灵魂可能会想到一些东西。

或者这可能是预期的行为 - 如果是,为什么?

【问题讨论】:

  • 减小 sizeToRead 是一个错误,它应该始终等于缓冲区大小。
  • 为什么一定要一样大?
  • 因为读取少于您分配的缓冲区的内容是没有意义的。事实上,你抱怨它的行为方式,阅读越来越少,直到你突然重新设置它。
  • 哦,但是当您不想踩踏之前的数据而不先将其复制到其他地方时,它确实有意义。我将尝试将其更改为始终读取 1 meg 的完整缓冲区,并在每次读取后将读取的内容复制到新缓冲区(这是上面设计不需要的副本,一个小的优化),让你知道它是否有帮助。
  • 我认为独立程序和目标之间唯一不同的是 .net 框架是目标的 2.0,我在独立程序上运行 4 个客户端配置文件。此外,目标在较低优先级的线程上运行(不会因为传入的数据量而拖累 GUI 响应),但它只是低于正常。

标签: c# tcp stream tcpclient


【解决方案1】:

这种行为太有趣了,我只好自己亲眼看看,而且……我看不到。

这个anti-答案提出了另一种理论,可以解释问题中描述的滞后。我不得不从问题和 cmets 中推断出一些细节。

目标应用程序是具有三个操作线程的交互式 UI 应用程序:

  1. TcpClient 网络数据消费者。
  2. 将结果传递到 UI 的数据队列使用者线程。
  3. UI 线程。

出于本次讨论的目的,假设TheDataQueueBlockingCollection<string> 实例(任何线程安全队列都可以):

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% 吗?
  • 在这些应用程序中,Consumer 中的 UI 线程非常努力地推动 CPU,但管道的其余部分受 I/O 限制。
  • 所以我对此进行了一些灰质的分析,得出以下分析:我们在日志中输出的不仅是读取之间的延迟,还有bytesRead,并且读取的数据量与滞后,当它“赶上”时,每个读取调用的 bytesRead 都高一个数量级,并且读取调用之间的延迟要小得多。我在上面理解的症状是没有足够快地为读取的内容提供服务,并且日志输出会在您的应用程序中显示您正在及时读取所有数据。这是正确的吗?
  • 是的,服务不够快。关于日志记录,NetworkStream.DataAvailable 可以告诉您数据是否准备好从网络缓冲区读取。当为 false 时,您可以预期下一个 Read 将阻塞等待更多数据。尝试将此与您的 Read 延迟相关联。
  • 在我的示例应用程序中,我可以从消费者中的一个空数据队列开始,然后突发一些数据并观察队列很快填满。之后,消费者在处理数据队列时表现出延迟,而网络缓冲区为空。
【解决方案2】:

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. 用户接收缓冲区已满
  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,如果未设置,将回滚我们的修复程序,设置注册表项并查看是否消除了延迟。如果是这样,这个答案将被接受。听起来肯定可以解释我所看到的。
  • 唉 - 推送位正在由发送数据的服务器设置,所以这不可能。不过不错。
猜你喜欢
  • 2018-10-15
  • 2020-10-01
  • 2011-06-03
  • 2018-06-08
  • 2014-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多