TCP 最核心的价值是提供了可靠性,而 UDP 最核心的价值是灵活,你几乎可以用它来做任何事情。
例如:HTTP 协议 1.1 和 2.0 都基于 TCP,而到了 HTTP 3.0 就开始用 UDP 了。
TCP 作为一个传输层协议,最核心的能力是传输。传输需要保证可靠性,还需要控制流速,这两个核心能力均由滑动窗口提供。
TCP 中每个发送的请求都需要响应。如果一个请求没有收到响应,发送方就会认为这次发送出现了故障,会触发重发。
如上图所示:
将已发送的数据放到最左边,发送中的数据放到中间,未发送的数据放到右边。
假设我们最多同时发送 5 个封包,也就是窗口大小 = 5。窗口中的数据被同时发送出去,然后等待 ACK。如果一个封包 ACK 到达,我们就将它标记为已接收(深绿色)。
如下图所示,有两个封包的 ACK 到达,因此标记为绿色。
这个时候滑动窗口可以向右滑动,如下图所示:
如果发送过程中,部分数据没能收到 ACK 会怎样呢?这就可能发生重传。
如果发生下图这样的情况,段 4 迟迟没有收到 ACK。
在这个过程中,如果后来段 4 重传成功(接收到 ACK),那么窗口就会继续右移。
如果段 4 发送失败,还是没能收到 ACK,那么接收方也会抛弃段 5、段 6、段 7。
这样从段 4 开始之后的数据都需要重发。
在 TCP 协议中,如果接收方想丢弃某个段,可以选择不发 ACK。
发送端超时后,会重发这个 TCP 段。而有时候,接收方希望催促发送方尽快补发某个 TCP 段,这个时候可以使用快速重传能力。
例如段 1、段 2、段 4 到了,但是段 3 没有到。 接收方可以发送多次段 3 的 ACK。如果发送方收到多个段 3 的 ACK,就会重发段 3。这个机制称为快速重传。这和超时重发不同,是一种催促的机制。
为了不让发送方误以为段 3 已经收到了,在快速重传的情况下,接收方即便收到发来的段 4,依然会发段 3 的 ACK(不发段 4 的 ACK),直到发送方把段 3 重传。
思考:窗口大小的单位是?
在上面所有的图片中,窗口大小是 TCP 段的数量。
实际操作中,每个 TCP 段的大小不同,限制数量会让接收方的缓冲区不好操作,因此实际操作中窗口大小单位是字节数。
发送、接收窗口的大小可以用来控制 TCP 协议的流速。
窗口越大,同时可以发送、接收的数据就越多,支持的吞吐量也就越大。
当然,窗口越大,如果数据发生错误,损失也就越大,因为需要重传越多的数据。
举个例子:我们用 RTT 表示 Round Trip Time,就是消息一去一回的时间。
假设 RTT = 1ms,带宽是 1mb/s。
如果窗口大小为 1kb,那么 1ms 可以发送一个 1kb 的数据(含 TCP 头),1s 就可以发送 1mb 的数据,刚好可以将带宽用满。
如果 RTT 再慢一些,比如 RTT = 10ms,那么这样的设计就只能用完 1/10 的带宽。 当然你可以提高窗口大小提高吞吐量,但是实际的模型会比这个复杂,因为还存在重传、快速重传、丢包等因素。
而实际操作中,也不可以真的把带宽用完,所以最终我们会使用折中的方案,在延迟、丢包率、吞吐量中进行选择。
滑动窗口是 TCP 协议控制可靠性的核心。发送方将数据拆包,变成多个分组。然后将数据放入一个拥有滑动窗口的数组,依次发出,仍然遵循先入先出(FIFO)的顺序,但是窗口中的分组会一次性发送。窗口中序号最小的分组如果收到 ACK,窗口就会发生滑动;如果最小序号的分组长时间没有收到 ACK,就会触发整个窗口的数据重新发送。另一方面,在多次传输中,网络的平均延迟往往是相对固定的,这样 TCP 协议可以通过双方协商窗口大小控制流速。
既然发送方有窗口,那么接收方也需要有窗口吗?
接收方收到发送方的每个数据分组(或者称为 TCP Segment),接收方肯定需要缓存。
举例来说,如果发送方发送了:1, 2, 3, 4。 那么接收方可能收到的一种情况是:1,4,3。注意,没有收到 2 的原因可能是延迟、丢包等。这个时候,接收方有两种选择。
选择一:什么都不做(这样分组 2 的 ACK 就不会发送给发送方,发送方发现没有收到 2 的 ACK,过一段时间就有可能重发 2,3,4,5)。 当然具体设计还需要探讨,比如不重发整个分组,只重发已发送没有收到 ACK 的分组。
这种方法的缺陷是性能太差,重发了整个分组(或部分)。因此我们可以考虑另一种选择。
选择二:如果重发一个窗口,或部分窗口,问题就不会太大了。虽然增加了网络开销,但是毕竟有进步(1 进步了,不会再重发)。
性能方面最大的开销是等待超时的时间,就是发送方要等到超时时间才重发窗口,这样操作性能太差。
因此,TCP 协议有一个快速重传的机制——接收方发现接收到了 1,但是没有接收到 2,那么马上发送 3 个分组 2 的 ACK 给到发送方,这样发送方收到多个 ACK,就知道接收方没有收到 2,于是马上重发 2。
无论是上面哪种方案,接收方也维护一个滑动窗口,是一个不错的选择。接收窗口的状态,可以和发送窗口的状态相互对应了。
当接收方给发送方回复ack的时候会携带接收方窗口大小,发送方就会根据这个回复来动态调整自己的窗口大小。。双方协商,就是带上窗口大小。窗口大小通常是接收方说了算。
UDP(User Datagram Protocol),目标是在传输层提供直接发送报文(Datagram)的能力。
为什么不直接调用 IP 协议呢? 如果裸发数据,IP 协议不香吗?
这是因为传输层协议在承接上方应用层的调用,需要提供应用到应用的通信——因此要附上端口号。每个端口,代表不同的应用。传输层下层的 IP 协议,承接传输层的调用,将数据从主机传输到主机。IP 层不能区分应用,导致哪怕是在 IP 协议上进行简单封装,也需要单独一个协议。这就构成了 UDP 协议的市场空间。
UDP 的设计目标就是在允许用户直接发送报文的情况下,最大限度地简化应用的设计。
UDP 的报文非常简化,只有 5 个部分。
校验和(Checksum)机制,这个机制在很多的网络协议中都会存在,因为校验数据在传输过程中有没有丢失、损坏是一个普遍需求。
比如现在数据有 4 个 byte:a,b,c,d,那么一种最简单的校验和就是:
checksum=(a+b+c+d) ^ 0xff
如果发送方用上述方式计算出 Checksum,并将 a,b,c,d 和 Checksum 一起发送给接收方,接收方就可以用同样的算法再计算一遍,这样就可以确定数据有没有发生损坏(变化)。
当然 Checksum 的做法,只适用于数据发生少量变化的情况。如果数据发生较大的变动,校验和也可能发生碰撞。
你可以看到 UDP 的可靠性保证仅仅就是 Checksum 一种。
如果一个数据封包 Datagram 发生了数据损坏,UDP 可以通过 Checksum 纠错或者修复。
但是 UDP 没有提供再多的任何机制,比如 ACK、顺序保证以及流控等。
首先,这两个协议的目的不同:
TCP 协议的核心目标是提供可靠的网络传输
UDP 的目标是在提供报文交换能力基础上尽可能地简化协议轻装上阵。
TCP 核心是要在保证可靠性提供更好的服务。TCP 会有握手的过程,需要建立连接,保证双方同时在线。而且TCP 有时间窗口持续收集无序的数据,直到这一批数据都可以合理地排序组成连续的结果。
UDP 并不具备以上这些特性,它只管发送数据封包,而且 UDP 不需要 ACK,这意味着消息发送出去成功与否 UDP 是不管的。
TCP 每个数据封包都需要确认,因此天然不适应高速数据传输场景,比如观看视频(流媒体应用)、网络游戏(TCP 有延迟)等。具体来说,
如果网络游戏用 TCP,每个封包都需要确认,可能会造成一定的延迟;再比如音、视频传输天生就允许一定的丢包率;
Ping 和 DNS Lookup,这类型的操作只需要一次简单的请求/返回,不需要建立连接,用 UDP 就足够了。
ping (Packet Internet Groper):因特网包探索器,用于测试网络连接量的程序
DNS Lookup(域名解析):请求某域名下的资源,浏览器需要先通过DNS解析器得到该域名服务器的IP地址。在DNS查找完成之前,浏览器不能从主机名那里下载到任何东西。
如果考虑希望传输足够块,就可能会用 UDP。
再比如 HTTP 协议,如果考虑请求/返回的可靠性,用 TCP 比较合适。
但是像 HTTP 3.0 这类应用层协议,从功能性上思考,暂时没有找到太多的优化点,但是想要把网络优化到极致,就会用 UDP 作为底层技术,然后在 UDP 基础上解决可靠性。
所以理论上,任何一个用 TCP 协议构造的成熟应用层协议,都可以用 UDP 重构。
第一类:TCP 场景
第二类:UDP 场景
第三类:模糊地带(TCP、UDP 都可以考虑)
解决可靠性是非常复杂的,要考虑非常多的因素。
TCP 帮助我们在确保吞吐量、延迟、丢包率的基础上,保证可靠性。
UDP 则不同,UDP 提供了最小版的实现,只支持 Checksum。UDP 最核心的价值是灵活、轻量、传输速度快。
最后还有一个非常重要的考虑因素就是成本,如果没有足够专业的团队解决网络问题,TCP 无疑会是更好的选择。
Moba 类多人竞技游戏的网络应该用 TCP 还是 UDP**?
Moba类游戏的传输协议是基于UDP的封装。首先Moba类游戏一般对实时性有要求,如果使用了TCP,再怎么优化也受TCP连接效率的影响。而UDP传输效率相对来说较高,但可靠性欠佳。因此思路应该是基于UDP协议做一些优化,牺牲部分的传输效率,保证其可靠性,但是又不需要像TCP协议那样有一套完善的机制来保证其可靠性。
文章参考《计算机网络通关 29 讲》—— 林䭽