TCP延迟确认的解决方法是什么?

时间:2014-03-22 21:40:09

标签: c# c++ c winsock

我发布了一个在线(基于网格)的视频游戏,它使用TCP协议来确保服务器 - 客户端网络拓扑中的可靠通信。我的游戏工作得相当好,但遭遇的延迟高于预期(类似的TCP游戏似乎在将延迟保持在最低限度方面做得更好)。

在调查时,我发现运行 Microsoft Windows (而不是 Mac OS X 客户端)的客户端的延迟仅出乎意料地高。此外,我发现如果Windows客户端设置TcpAckFrequency=1 in the registry并重新启动他们的计算机,他们的延迟就会变得正常。

我的网络设计似乎没有考虑延迟确认:

  

不考虑延迟确认,Nagle算法和Winsock缓冲的交互的设计可以极大地影响性能。   (http://support.microsoft.com/kb/214397

但是,我发现几乎不可能在我的游戏(或任何游戏)中考虑延迟确认。根据MSDN,Microsoft TCP堆栈使用以下标准来决定何时在接收的数据包上发送一个ACK:

  
      
  • 如果在延迟计时器到期(200ms)之前收到第二个数据包,则发送ACK。
  •   
  • 如果在收到第二个数据包之前有与ACK相同的方向发送数据并且延迟计时器到期,   ACK与数据段搭载并立即发送。
  •   
  • 当延迟计时器到期(200ms)时,发送ACK。
  •   
     

http://support.microsoft.com/kb/214397

阅读本文,可以假设Microsoft TCP堆栈上延迟确认的解决方法如下:

  1. 禁用Nagle算法(TCP_NODELAY)。
  2. 禁用套接字的发送缓冲区(SO_SNDBUF = 0),以便可以预期对send的调用发送数据包。
  3. 当调用send时,如果不希望立即发送更多数据,请再次使用将被接收方丢弃的单字节数据再次呼叫send
  4. 通过这种方法,接收器将在与前一个数据包大致相同的时间接收第二个数据包。因此,ACK应立即从接收方发送给发送方(模拟注册表中TcpAckFrequency=1的作用)。

    但是,从我的测试来看,这种延迟仅提高了注册表编辑工作量的一半左右。我错过了什么?

    <小时/> 问:为什么不使用UDP?

    A:我选择了TCP,因为我发送的每个数据包都需要到达(并且按顺序);如果丢失(或变得无序),则没有值得重传的数据包。只有当数据包可以被丢弃/无序时,UDP才能比TCP更快!

7 个答案:

答案 0 :(得分:15)

由于Windows Vista,必须在调用connect之前设置TCP_NODELAY选项,或者在调用listen之前(在服务器上)设置TCP_NODELAY选项。如果您在调用TCP_NODELAY后设置connect,则实际不会禁用Nagle算法,但GetSocketOption会声明Nagle已被禁用!这一切似乎都没有记载,并且与该主题的许多教程/文章所教导的内容相矛盾。

Nagle实际上已禁用,TCP延迟确认不再导致延迟。

答案 1 :(得分:2)

你应该没有什么需要做的。您建议的所有解决方法都是帮助那些没有正确设计的协议来处理TCP。据推测,您的协议旨在通过TCP工作,对吗?

你的问题几乎肯定是其中一个或两个:

  1. 您正在使用少量数据调用TCP发送函数,即使您没有理由无法使用较大的块进行调用。

  2. 您没有实现应用程序协议数据单元的应用程序级别确认。实现这些,以便ACK可以捎带它们。

答案 2 :(得分:1)

  

使用这种方法,第二个数据包将被接收   接收器与前一个数据包大约在同一时间。作为一个   结果,ACK应立即从接收器发送到   sender(模拟TcpAckFrequency = 1在注册表中执行的操作)。

我不相信这总会导致发送第二个独立的数据包。我知道你有Nagle的残疾人和零发送缓冲区,但我看到了陌生人的事情。一些wirehark转储可能会有所帮助。

一个想法:发送一个完整的MSS的数据(通常是1500-MTU网络上的1460字节),而不是你的'canary'数据包只有一个字节。

答案 3 :(得分:0)

使用可靠的UDP库并编写自己的拥塞控制算法,这肯定会克服您的TCP延迟问题。

以下库,我用于可靠的UDP传输:

http://udt.sourceforge.net/

答案 4 :(得分:0)

要解决此问题,有必要了解TCP连接的正常运行。 Telnet是一个很好的分析示例。

TCP通过确认成功的数据传输来保证交付。 “Ack”可以作为消息单独发送,但这会引入相当多的开销 - Ack是非常小的消息本身,但是较低级别的协议会添加额外的标头。出于这个原因,TCP倾向于将Ack消息搭载在它正在发送的另一个数据包上。通过Telnet查看交互式shell,可以获得源源不断的击键和响应。如果键入有一点停顿,屏幕上就没有任何回音。流量停止的唯一情况是,如果输出没有相应的输入。但是因为你只能读得这么快,所以可以等几百毫秒来看看是否有按键来搭载Ack。

因此,总结一下,我们两种方式都有稳定的数据包流,Ack通常会背负。如果出于应用原因导致流程中断,则不会发现延迟Ack。

回到你的协议:你显然没有请求/响应协议。这意味着Ack不能捎带(问题1)。虽然接收操作系统将发送单独的Acks,但它不会发送垃圾邮件。

您通过TCP_NODELAY的解决方法和发送(Windows)端的两个数据包假定接收方也是Windows,或者至少表现如此。 这是一厢情愿的想法,而非工程。其他操作系统可能会决定等待三个数据包发送Ack,这会完全中断您使用TCP_NODELAY强制一个额外数据包。 “等待3个包”只是一个例子;还有许多其他有效的算法可以防止Ack垃圾邮件被你的第二个单字节虚拟数据包所欺骗。

真正的解决方案是什么?在协议级别发送响应。无论操作系统如何,它都会在协议响应上搭载TCP Ack。反过来,这个响应也会强制Ack在另一个方向(响应也是TCP消息)但你不关心响应的延迟。响应就是这样接收操作系统捎带第一个Ack。

答案 5 :(得分:-1)

我建议你让Nagle算法和缓冲区打开,因为它的基本目的是收集小写到完整/更大的数据包(这会大大提高性能),但同时使用FlushFileBuffers(在完成发送一段时间后,在插座上。

我在这里假设,你的游戏有某种主循环,处理东西,然后在进入下一轮之前等待一段时间:

while(run_my_game)
{
    process_game_events_and_send_data_over_network();
    Sleep(20 - time_spent_processing);
};

我现在建议在Sleep()调用之前插入FlushFileBuffers():

while(run_my_game)
{
    process_game_events_and_send_data_over_network();
    FlushFileBuffers(my_socket);
    Sleep(20 - time_spent_processing);
};

这样,您最迟会在应用程序进入休眠状态之前延迟发送pakets以等待下一轮。您应该从Nagel的算法中获得性能优势,最大限度地减少延迟。

如果这不起作用,如果你发布一些(伪)代码来解释你的程序实际工作原理会很有帮助。

编辑: 当我再次考虑你的问题时,还有两件事情进入我的脑海:

a)延迟的ACK pakets确实应该 NOT 导致任何延迟,因为它们以与您发送的数据相反的方向传播。它们最坏的情况是阻塞发送队列。然而,当连接和存储器限制的带宽允许时,在几个pakets之后,TCP将解决这个问题。因此,除非您的机器具有非常低的RAM(不足以容纳更大的发送队列),或者您实际上传输的数据多于连接所允许的数据,否则延迟的ACK缓冲区是一种优化,实际上会提高性能。

b)您正在使用专用线程进行发送。我想知道为什么。 AFAIK是Socket API多线程安全的,因此每个产品线程都可以自己发送数据 - 除非你的应用程序需要这样的队列,我建议也删除这个专用的发送线程,并带有额外的同步开销并延迟它可能会导致。

我&#39;特别提到这里的延误。因为操作系统可能决定不立即安排发送线程再次执行,当它在队列中被解除阻塞时。典型的重新调度延迟在10ms范围内,但在负载下它们可以突破到50ms或更长。作为一个工作场所,您可以尝试摆弄调度优先级。但这不会减少操作系统本身的延迟。

顺便说一下。您可以轻松地对TCP和您的网络进行基准测试,只需在客户端上安装一个线程,在服务器上安装一个线程,只需使用某些数据进行ping / pong。

答案 6 :(得分:-2)

  

我发送的每个数据包都需要到达(并按顺序);

此要求是导致延迟的原因。

要么你的网络丢包可以忽略不计,UDP会传输每个数据包,要么你有丢失,TCP正在进行重传,将所有内容延迟(重复)重传间隔(至少是往返时间)。此延迟不一致,因为它是由丢失的数据包触发的;抖动通常比由分组合并引起的可预测的确认延迟具有更差的后果

  

只有当数据包可以被丢弃/无序时,UDP才能比TCP快!

这是一个容易做出的假设,但却是错误的。

还有其他方法可以提高除ARQ之外的丢弃率,从而提供更低的延迟:前向纠错方法可以提高丢弃恢复的延迟,但需要额外的带宽。