什么是更好的?很多小的TCP数据包,还是一个很长的TCP数据包?[关闭]


16

我正在为自己制作的游戏在服务器之间发送大量数据。

我目前发送这样的位置数据:

sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));

显然,它正在发送各自的X,Y和Z值。

这样发送数据会更有效吗?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));

2
根据我的有限经验,数据包丢失通常在5%以下。
mucaho 2015年

5
sendToClient是否实际发送数据包?如果是这样,您是如何做到的?
2015年

1
@mucaho我自己或任何东西都从未测量过它,但令我惊讶的是TCP的边缘如此粗糙。我本来希望能达到0.5%或更低的水平。
Panzercrisis

1
@Panzercrisis我必须同意你的观点。我个人认为5%的损失是不可接受的。如果您考虑到像我这样发送的东西,比如游戏中产生了一条新船,那么即使没有1%的机率也没有收到,这将是灾难性的,因为我会得到看不见的船。
joehot200 2015年

2
别吓坏了,我的意思是将5%作为上限:)实际上,正如其他评论所指出的,它要好得多。
mucaho

Answers:


28

TCP段有很多开销。当您发送带有一个TCP数据包的10字节消息时,您实际上发送的是:

  • 16字节的IPv4标头(当IPv6变得常见时将增加到40字节)
  • 16个字节的TCP标头
  • 10字节有效载荷
  • 使用的数据链路和物理层协议的额外开销

导致产生42个字节的流量来传输10个字节的数据。因此,您仅使用不到可用带宽的25%。而且,这还没有考虑到诸如以太网或PPPoE之类的低级协议所消耗的开销(但是由于存在很多替代方案,因此很难估算这些开销)。

同样,许多小数据包给路由器,防火墙,交换机和其他网络基础设施设备带来了更大的压力,因此,当您,服务提供商和用户不投资高质量的硬件时,这可能会成为另一个瓶颈。

因此,您应该尝试在一个TCP段中一次发送所有可用的数据。

关于处理数据包丢失:使用TCP时,您无需担心。该协议本身确保重新发送所有丢失的数据包并按顺序处理这些数据包,因此您可以假定您发送的所有数据包都将到达另一端,并且它们将按照发送它们的顺序到达。这样做的代价是,当数据包丢失时,您的播放器将经历相当大的滞后,因为一个丢失的数据包将暂停整个数据流,直到重新请求并接收它为止。

如果出现问题,可以随时使用UDP。但是,随后您需要找到自己的解决方案来解决丢失和乱序的消息(至少可以保证确实到达的消息,到达的消息完整且完好无损)。


1
TCP恢复数据包丢失方式的开销是否取决于数据包的大小而有所不同?
Panzercrisis

@Panzercrisis仅在需要重新发送较大的数据包的情况下。
菲利普

8
我要指出的是,操作系统几乎肯定会将Nagles算法en.wikipedia.org/wiki/Nagle%27s_algorithm应用于传出数据,这意味着无论在应用程序中执行单独的写入还是将它们组合在一起,都可以将它们组合在一起在实际通过TCP传递出去之前。

1
@Vality我使用过的大多数套接字API都可以为每个套接字激活或停用nagle。对于大多数游戏,我建议您停用它,因为低延迟通常比节省带宽更重要。
菲利普

4
Nagle的算法是一种,但不是在发送端缓存数据的唯一原因。有没有办法可靠力数据发送。同样,在发送数据后的任何地方,都可以在NAT,路由器,代理甚至接收端进行缓冲/分段。TCP不能保证您会收到数据的大小和时间,只能保证顺序和可靠地到达。如果需要大小保证,请使用UDP。TCP 似乎更容易理解的事实并不能使其成为解决所有问题的最佳工具!
2015年

10

一个大的(在合理范围内)更好。

如您所说,丢包是主要原因。数据包通常以固定大小的帧发送,因此,与一个带有10个小消息的10帧相比,占用一个大消息的帧更好。

但是,对于标准TCP,这不是真正的问题,除非您将其禁用。(这被称为Nagle的算法,对于游戏,您应该禁用它。)TCP将等待固定的超时或直到程序包“满”。“满”的位置是一些魔术数字,部分由帧大小确定。


我听说过Nagle的算法,但是禁用它真的是一个好主意吗?我刚从StackOverflow答案中获得答案,有人说它更有效(并且出于明显的原因,我想要效率)。
joehot200

6
@ joehot200唯一正确的答案是“取决于”。是的,它对于发送大量数据更有效,但对于游戏往往需要的实时流传输却不那么有效。
D端

4
@ joehot200:Nagle的算法与某些TCP实现有时使用的延迟确认算法的交互性较差。如果某些TCP实现希望在之后获得更多数据,则会在收到一些数据后延迟发送ACK(因为确认后一个数据包也将隐式地确认较早的数据包)。Nagle的算法说,如果某个单元已经发送了一些数据但没有听到确认,则它不应发送部分数据包。有时这两种方法相互作用很差,彼此等待对方做某事,直到...
超级猫

2
...一个“理智的计时器”启动并解决该情况(大约一秒钟)。
超级猫

2
不幸的是,禁用Nagle的算法将无济于事,以防止在另一主机端进行缓冲。禁用Nagle的算法并不能确保您recv()每次send()通话都能接到一个电话,而这正是大多数人想要的。不过,使用协议可以保证这一点,例如UDP。“当您拥有的只是TCP时,一切看起来就像是流”
Panda Pajama 2015年

6

先前的所有答案都不正确。在实践中,发行多长时间都没关系send()还是几个小线send()呼叫。

如Phillip所言,TCP段有一些开销,但是作为应用程序程序员,您无法控制段的生成方式。简单来说:

send()调用不一定转换为一个TCP段。

操作系统是 完全免费的缓冲所有数据并将其发送到一个段中,或者将较长的数据分成几个小段。

这有几个含义,但是最重要的是:

一个send()呼叫或一个TCP段不一定转换为一个成功recv()另一端的呼叫

其背后的原因是TCP是协议。TCP将您的数据视为长字节流,并且绝对没有“数据包”的概念。用send()向该流添加字节时,recv()从另一端获取字节。TCP将积极地缓冲和分割您认为合适的数据,以确保您的数据尽快到达另一端。

如果要使用TCP发送和接收“数据包”,则必须实现数据包开始标记,长度标记等。如何使用像UDP这样的面向消息的协议呢?UDP保证一个send()呼叫转换为一个发送的数据报和一个recv()呼叫!

当您拥有的只是TCP时,一切看起来就像流


1
在每个消息前面加上消息长度不是那么棘手。
ysdx

无论数据包聚合是否有效,在数据包聚合时您都可以切换一下。为了确保及时传送未填充的数据包,在游戏网络中将其关闭并不罕见。
Lars Viklund'Mar

这完全是操作系统或特定于库的。另外,您确实有很多控制权-如果需要的话。的确,您没有完全的控制权,总是允许TCP合并两个消息,或者如果不适合MTU,则拆分一个消息,但是您仍然可以向正确的方向暗示它。进行各种配置设置,手动发送消息(间隔1秒)或缓冲数据并一次性发送。
Dorus

@ysdx:否,不在发送方,但是在接收方。由于无法保证确切的数据来源recv(),因此您需要进行自己的缓冲以弥补这一点。我将其列为与通过UDP实现可靠性相同的难度。
2015年

@Pagnda Pajama:接收方的一个简单的实现是:(while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }为了简洁起见,省略了错误处理和部分读取,但是它并没有太大变化)。这样做select并不会增加更多的复杂性,如果您使用的是TCP,则可能仍然需要缓冲部分消息。在UDP上实现可靠的可靠性要比这复杂得多。
ysdx

3

许多小包装都可以。实际上,如果您担心TCP开销,只需插入一个bufferstream可收集最​​多1500个字符(或任何TCP MTU,最好动态地请求它)的,然后在一个地方处理该问题。这样做可以为您另外创建的每个额外包节省约40个字节的开销。

也就是说,发送较少的数据还是更好的方法,在那里建立更大的对象会有所帮助。当然,发送"UID:10|1|2|3比发送要小UID:10;x:1UID:10;y:2UID:10;z:3。实际上,在这一点上,您也不应重新发明轮子,而应使用诸如protobuf之类的库,该库可以将此类数据减少到10个字节或更少的字符串。

您唯一不应忘记的是插入一个 Flush在流中的相关位置命令,因为一旦停止向流中添加数据,它就可能无限等待发送任何内容。当您的客户端正在等待该数据时,这确实有问题,并且在客户端发送下一条命令之前,您的服务器不会发送任何新内容。

包裹丢失是您可能会在这里影响的一点点。您发送的每个字节都可能被破坏,TCP会自动请求重传。较小的软件包意味着每个软件包被破坏的机会较小,但是由于它们增加了开销,因此您发送的字节甚至更多,从而增加了丢失软件包的几率。当包丢失时,TCP将缓冲所有后续数据,直到丢失的包被重新发送并接收。这导致较大的延迟(ping)。尽管由于封装损耗而导致的带宽总损耗可以忽略不计,但对于游戏来说,较高的ping是不希望的。

Bottom line: Send as little data as possible, send large packages, and do not write your own low level methods to do so, but rely on well known libraries and methods like bufferstream and protobuf to handle the heavy lifting.


Actually for simple things like this its easy to roll your own. Much easier than going through a 50 page documentation to use another person's library, and then after that you've still got to deal with their bugs and gotchas.
Pacerier

没错,编写自己的东西bufferstream很琐碎,这就是为什么我称它为方法。您仍然希望在一个地方处理它,而不是将缓冲区逻辑与消息代码集成在一起。至于对象序列化,我非常怀疑您能否获得比其他人投入的工作时间还要多的东西,即使您确实尝试,我也强烈建议您对照已知实现对基准解决方案进行基准测试。
Dorus

2

尽管我自己是网络编程的新手,但我想通过添加几点分享我的有限经验:

  • TCP确实存在开销-您必须测量相关统计数据
  • UDP是网络游戏场景的事实上的解决方案,但是所有依赖于它的实现都具有额外的CPU侧算法来解决丢包或乱序发送的数据包

关于度量,应考虑的度量标准是:

如前所述,如果您发现自己在某种程度上不受限制并且可以使用UDP,请继续使用。有一些基于UDP的实现,因此您不必重新发明轮子,也不必与多年的经验和经验证的经验相抗衡。值得一提的此类实现是:

结论:由于UDP的性能可能比TCP的性能高3倍,因此,一旦您确定自己的场景对UDP友好,就可以考虑使用它。被警告!在UDP之上实现完整的TCP堆栈始终是一个坏主意。


我正在使用UDP。我只是切换到TCP。对于客户端所需的关键数据,UDP的数据包丢失简直是无法接受的。我可以通过UDP发送运动数据。
joehot200

So, the best things that you can do are: indeed, use TCP just for crucial operations OR use an UDP based software protocol implementation (with Enet being simple and UDT being well tested). But first, measure the loss and decide whether UDT would bring you an edge.
teodron
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.