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

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

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

看来我的网络设计没有考虑延迟确认:

不考虑延迟确认 ,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调用来send数据包。
  3. 当调用send ,如果不希望立即send更多数据,则再次使用将被接收器丢弃的单字节数据调用send

利用这种方法,接收器将在与前一数据分组大致相同的时间接收第二数据分组。 因此, ACK应立即从接收方发送到发送方(模拟TcpAckFrequency=1在注册表中执行的操作)。

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


问:为什么不使用UDP?

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

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

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

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

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

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

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

利用这种方法,接收器将在与前一数据分组大致相同的时间接收第二数据分组。 因此,ACK应立即从接收方发送到发送方(模拟TcpAckFrequency = 1在注册表中执行的操作)。

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

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

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

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

http://udt.sourceforge.net/

要解决这个问题,有必要了解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。

我建议你让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); }; 

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

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

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

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

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

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

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

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

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

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

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

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

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