请选择 进入手机版 | 继续访问电脑版
网络

教育改变生活

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 16424|回复: 0

[基础知识] 彻底搞懂TCP协议:从 TCP 三次握手四次挥手说起(四)

[复制链接]

271

主题

284

帖子

1243

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
1243

最佳新人活跃会员热心会员突出贡献优秀版主

发表于 2020-9-2 16:47:53 | 显示全部楼层 |阅读模式
疑症(9)TCP 的流量控制
我们知道 TCP 的窗口(window)是一个 16bit 位字段,它代表的是窗口的字节容量,也就是 TCP 的标准窗口最大为 2^16-1=65535 个字节。另外在 TCP 的选项字段中还包含了一个 TCP 窗口扩大因子,option-kind 为 3,option-length 为 3 个字节,option-data 取值范围 0-14。窗口扩大因子用来扩大 TCP 窗口,可把原来 16bit 的窗口,扩大为 31bit。这个窗口是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。也就是,发送端是根据接收端通知的窗口大小来调整自己的发送速率的,以达到端到端的流量控制。尽管流量控制看起来简单明了,就是发送端根据接收端的限制来控制自己的发送就好了,但是细心的同学还是会有些疑问的。
1)发送端是怎么做到比较方便知道自己哪些包可以发,哪些包不能发呢? 2)如果接收端通知一个零窗口给发送端,这个时候发送端还能不能发送数据呢?如果不发数据,那一直等接收端口通知一个非 0 窗口吗,如果接收端一直不通知呢? 3)如果接收端处理能力很慢,这样接收端的窗口很快被填满,然后接收处理完几个字节,腾出几个字节的窗口后,通知发送端,这个时候发送端马上就发送几个字节给接收端吗?发送的话会不会太浪费了,就像一艘万吨油轮只装上几斤的油就开去目的地一样。对于发送端产生数据的能力很弱也一样,如果发送端慢吞吞产生几个字节的数据要发送,这个时候该不该立即发送呢?还是累积多点在发送?
疑问 1)的解决:
发送方要知道那些可以发,哪些不可以发,一个简明的方案就是按照接收方的窗口通告,发送方维护一个一样大小的发送窗口就可以了,在窗口内的可以发,窗口外的不可以发,窗口在发送序列上不断后移,这就是 TCP 中的滑动窗口。如下图所示,对于 TCP 发送端其发送缓存内的数据都可以分为 4 类: [1]-已经发送并得到接收端 ACK 的; [2]-已经发送但还未收到接收端 ACK 的; [3]-未发送但允许发送的(接收方还有空间); [4]-未发送且不允许发送(接收方没空间了)。
其中,[2]和[3]两部分合起来称之为发送窗口。
下面两图演示的窗口的滑动情况,收到 36 的 ACK 后,窗口向后滑动 5 个 byte。
疑问 2)的解决
由问题 1)我们知道,发送端的发送窗口是由接收端控制的。下图,展示了一个发送端是怎么受接收端控制的。
由上图我们知道,当接收端通知一个 zero 窗口的时候,发送端的发送窗口也变成了 0,也就是发送端不能发数据了。如果发送端一直等待,直到接收端通知一个非零窗口在发数据的话,这似乎太受限于接收端,如果接收端一直不通知新的窗口呢?显然发送端不能干等,起码有一个主动探测的机制。为解决 0 窗口的问题,TCP 使用了 Zero Window Probe 技术,缩写为 ZWP。发送端在窗口变成 0 后,会发 ZWP 的包给接收方,来探测目前接收端的窗口大小,一般这个值会设置成 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。
如果 3 次过后还是 0 的话,有的 TCP 实现就会发 RST 掉这个连接。正如有人的地方就会有商机,那么有等待的地方就很有可能出现 DDoS 攻击点。攻击者可以在和 Server 建立好连接后,就向 Server 通告一个 0 窗口,然后 Server 端就只能等待进行 ZWP,于是攻击者会并发大量的这样的请求,把 Server 端的资源耗尽。
疑问点 3)的解决
疑点 3)本质就是一个避免发送大量小包的问题。造成这个问题原因有二:
1)接收端一直在通知一个小的窗口; 2)发送端本身问题,一直在发送小包。这个问题,TCP 中有个术语叫 Silly Window Syndrome(糊涂窗口综合症)。
解决这个问题的思路有两种,1)接收端不通知小窗口,2)发送端积累一下数据在发送。
思路 1)是在接收端解决这个问题,David D Clark’s 方案,如果收到的数据导致 window size 小于某个值,就 ACK 一个 0 窗口,这就阻止发送端在发数据过来。等到接收端处理了一些数据后 windows size 大于等于了 MSS,或者 buffer 有一半为空,就可以通告一个非 0 窗口。思路 2)是在发送端解决这个问题,有个著名的 Nagle’s algorithm。Nagle 算法的规则: [1]如果包长度达到 MSS ,则允许发送; [2]如果该包含有 FIN ,则允许发送; [3]设置了 TCP_NODELAY 选项,则允许发送; [4]设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送; [5]上述条件都未满足,但发生了超时(一般为 200ms ),则立即发送。
规则[4]指出 TCP 连接上最多只能有一个未被确认的小数据包。从规则[4]可以看出 Nagle 算法并不禁止发送小的数据包(超时时间内),而是避免发送大量小的数据包。由于 Nagle 算法是依赖 ACK 的,如果 ACK 很快的话,也会出现一直发小包的情况,造成网络利用率低。TCP_CORK 选项则是禁止发送小的数据包(超时时间内),设置该选项后,TCP 会尽力把小数据包拼接成一个大的数据包(一个 MTU)再发送出去,当然也不会一直等,发生了超时(一般为 200ms),也立即发送。Nagle 算法和 CP_CORK 选项提高了网络的利用率,但是增加是延时。从规则[3]可以看出,设置 TCP_NODELAY 选项,就是完全禁用 Nagle 算法了。
这里要说一个小插曲,Nagle 算法和延迟确认(Delayed Acknoledgement)一起,当出现(write-write-read)的时候会引发一个 40ms 的延时问题,这个问题在 HTTP svr 中体现的比较明显。场景如下:
客户端在请求下载 HTTP svr 中的一个小文件,一般情况下,HTTP svr 都是先发送 HTTP 响应头部,然后在发送 HTTP 响应 BODY(特别是比较多的实现在发送文件的实施采用的是 sendfile 系统调用,这就出现 write-write-read 模式了)。当发送头部的时候,由于头部较小,于是形成一个小的 TCP 包发送到客户端,这个时候开始发送 body,由于 body 也较小,这样还是形成一个小的 TCP 数据包,根据 Nagle 算法,HTTP svr 已经发送一个小的数据包了,在收到第一个小包的 ACK 后或等待 200ms 超时后才能在发小包,HTTP svr 不能发送这个 body 小 TCP 包。
客户端收到 http 响应头后,由于这是一个小的 TCP 包,于是客户端开启延迟确认,客户端在等待 Svr 的第二个包来在一起确认或等待一个超时(一般是 40ms)在发送 ACK 包;这样就出现了你等我、然而我也在等你的死锁状态,于是出现最多的情况是客户端等待一个 40ms 的超时,然后发送 ACK 给 HTTP svr,HTTP svr 收到 ACK 包后在发送 body 部分。大家在测 HTTP svr 的时候就要留意这个问题了。
疑症(10)TCP 的拥塞控制
谈到拥塞控制,就要先谈谈拥塞的因素和本质。本质上,网络上拥塞的原因就是大家都想独享整个网络资源,对于 TCP,端到端的流量控制必然会导致网络拥堵。这是因为 TCP 只看到对端的接收空间的大小,而无法知道链路上的容量,只要双方的处理能力很强,那么就可以以很大的速率发包,于是链路很快出现拥堵,进而引起大量的丢包,丢包又引发发送端的重传风暴,进一步加剧链路的拥塞。另外一个拥塞的因素是链路上的转发节点,例如路由器,再好的路由器只要接入网络,总是会拉低网络的总带宽,如果在路由器节点上出现处理瓶颈,那么就很容易出现拥塞。由于 TCP 看不到网络的状况,那么拥塞控制是必须的并且需要采用试探性的方式来控制拥塞,于是拥塞控制要完成两个任务:[1]公平性;[2]拥塞过后的恢复。
TCP 发展到现在,拥塞控制方面的算法很多,其中 Reno 是目前应用最广泛且较为成熟的算法,下面着重介绍一下 Reno 算法(RFC5681)。介绍该算法前,首先介绍一个概念 duplicate acknowledgment(冗余 ACK、重复 ACK)一般情况下一个 ACK 被称为冗余 ACK,要同时满足下面几个条件(对于 SACK,那么根据 SACK 的一些信息来进一步判断)。 [1] 接收 ACK 的那端已经发出了一些还没被 ACK 的数据包; [2] 该 ACK 没有捎带 data; [3] 该 ACK 的 SYN 和 FIN 位都是 off 的,也就是既不是 SYN 包的 ACK 也不是 FIN 包的 ACK; [4] 该 ACK 的确认号等于接收 ACK 那端已经收到的 ACK 的最大确认号; [5] 该 ACK 通知的窗口等接收该 ACK 的那端上一个收到的 ACK 的窗口。
Reno 算法包含 4 个部分: [1]慢热启动算法 – Slow Start; [2]拥塞避免算法 – Congestion Avoidance; [3]快速重传 - Fast Retransimit; [4]快速恢复算法 – Fast Recovery。
TCP 的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,根据前面的讨论,我们知道有一个接收端通告的接收窗口(rwnd)用于流量控制;加上拥塞控制后,发送端真正的发送窗口=min(rwnd,cwnd)。关于 cwnd 的单位,在 TCP 中是以字节来做单位的,我们假设 TCP 每次传输都是按照 MSS 大小来发送数据,因此你可以认为 cwnd 按照数据包个数来做单位也可以理解,下面如果没有特别说明是字节,那么 cwnd 增加 1 也就是相当于字节数增加 1 个 MSS 大小。
【1】慢热启动算法 – Slow Start
慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络情况,然后在慢慢提速。不要一上来就拼命发包,这样很容易造成链路的拥堵,出现拥堵了在想到要降速来缓解拥堵这就有点成本高了,毕竟无数的先例告诫我们先污染后治理的成本是很高的。慢启动的算法如下(cwnd 全称 Congestion Window): 1)连接建好的开始先初始化 cwnd = N,表明可以传 N 个 MSS 大小的数据; 2)每当收到一个 ACK,++cwnd; 呈线性上升; 3)每当过了一个 RTT,cwnd = cwnd*2; 呈指数让升; 4)还有一个慢启动门限 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入"拥塞避免算法 - Congestion Avoidance"。
根据 RFC5681,如果 MSS > 2190 bytes,则 N = 2;如果 MSS < 1095 bytes,则 N =4;如果 2190 bytes >= MSS >= 1095 bytes,则 N = 3;一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》建议把 cwnd 初始化成了 10 个 MSS。Linux 3.0 后采用了这篇论文的建议。
【2】拥塞避免算法 – Congestion Avoidance
慢启动的时候说过,cwnd 是指数快速增长的,但是增长是有个门限 ssthresh(一般来说大多数的实现 ssthresh 的值是 65535 字节)的,到达门限后进入拥塞避免阶段。在进入拥塞避免阶段后,cwnd 值变化算法如下: 1)每收到一个 ACK,调整 cwnd 为 (cwnd + 1/cwnd) * MSS 个字节; 2)每经过一个 RTT 的时长,cwnd 增加 1 个 MSS 大小。
TCP 是看不到网络的整体状况的,那么 TCP 认为网络拥塞的主要依据是它重传了报文段。前面我们说过 TCP 的重传分两种情况: 1)出现 RTO 超时,重传数据包。这种情况下,TCP 就认为出现拥塞的可能性就很大,于是它反应非常'强烈' [1] 调整门限 ssthresh 的值为当前 cwnd 值的 1/2; [2] reset 自己的 cwnd 值为 1; [3] 然后重新进入慢启动过程。
2)在 RTO 超时前,收到 3 个 duplicate ACK 进行重传数据包。这种情况下,收到 3 个冗余 ACK 后说明确实有中间的分段丢失,然而后面的分段确实到达了接收端,因为这样才会发送冗余 ACK,这一般是路由器故障或者轻度拥塞或者其它不太严重的原因引起的,因此此时拥塞窗口缩小的幅度就不能太大,此时进入快速重传。
【3】快速重传 - Fast Retransimit 做的事情有:
1) 调整门限 ssthresh 的值为当前 cwnd 值的 1/2; 2) 将 cwnd 值设置为新的 ssthresh 的值; 3) 重新进入拥塞避免阶段。
在快速重传的时候,一般网络只是轻微拥堵,在进入拥塞避免后,cwnd 恢复的比较慢。针对这个,“快速恢复”算法被添加进来,当收到 3 个冗余 ACK 时,TCP 最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段。
【4】快速恢复算法 – Fast Recovery :
快速恢复的思想是“数据包守恒”原则,即带宽不变的情况下,在网络同一时刻能容纳数据包数量是恒定的。当“老”数据包离开了网络后,就能向网络中发送一个“新”的数据包。既然已经收到了 3 个冗余 ACK,说明有三个数据分段已经到达了接收端,既然三个分段已经离开了网络,那么就是说可以在发送 3 个分段了。于是只要发送方收到一个冗余的 ACK,于是 cwnd 加 1 个 MSS。快速恢复步骤如下(在进入快速恢复前,cwnd 和 sshthresh 已被更新为:sshthresh = cwnd /2,cwnd = sshthresh): 1)把 cwnd 设置为 ssthresh 的值加 3,重传 Duplicated ACKs 指定的数据包; 2)如果再收到 duplicated Acks,那么 cwnd = cwnd +1; 3)如果收到新的 ACK,而非 duplicated Ack,那么将 cwnd 重新设置为【3】中 1)的 sshthresh 的值。然后进入拥塞避免状态。
细心的同学可能会发现快速恢复有个比较明显的缺陷就是:它依赖于 3 个冗余 ACK,并假定很多情况下,3 个冗余的 ACK 只代表丢失一个包。但是 3 个冗余 ACK 也很有可能是丢失了很多个包,快速恢复只是重传了一个包,然后其他丢失的包就只能等待到 RTO 超时了。超时会导致 ssthresh 减半,并且退出了 Fast Recovery 阶段,多个超时会导致 TCP 传输速率呈级数下降。出现这个问题的主要原因是过早退出了 Fast Recovery 阶段。
为解决这个问题,提出了 New Reno 算法,该算法是在没有 SACK 的支持下改进 Fast Recovery 算法(SACK 改变 TCP 的确认机制,把乱序等信息会全部告诉对方,SACK 本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包),具体改进如下:
1)发送端收到 3 个冗余 ACK 后,重传冗余 ACK 指示可能丢失的那个包 segment1,如果 segment1 的 ACK 通告接收端已经收到发送端的全部已经发出的数据的话,那么就是只丢失一个包,如果没有,那么就是有多个包丢失了; 2)发送端根据 segment1 的 ACK 判断出有多个包丢失,那么发送端继续重传窗口内未被 ACK 的第一个包,直到 sliding window 内发出去的包全被 ACK 了,才真正退出 Fast Recovery 阶段。
我们可以看到,拥塞控制在拥塞避免阶段,cwnd 是加性增加的,在判断出现拥塞的时候采取的是指数递减。为什么要这样做呢?这是出于公平性的原则,拥塞窗口的增加受惠的只是自己,而拥塞窗口减少受益的是大家。这种指数递减的方式实现了公平性,一旦出现丢包,那么立即减半退避,可以给其他新建的连接腾出足够的带宽空间,从而保证整个的公平性。
至此,TCP 的疑难杂症基本介绍完毕了,总的来说 TCP 是一个有连接的、可靠的、带流量控制和拥塞控制的端到端的协议。TCP 的发送端能发多少数据,由发送端的发送窗口决定(当然发送窗口又被接收端的接收窗口、发送端的拥塞窗口限制)的,那么一个 TCP 连接的传输稳定状态应该体现在发送端的发送窗口的稳定状态上,这样的话,TCP 的发送窗口有哪些稳定状态呢?TCP 的发送窗口稳定状态主要有上面三种稳定状态:
【1】接收端拥有大窗口的经典锯齿状
大多数情况下都是处于这样的稳定状态,这是因为,一般情况下机器的处理速度就是比较快,这样 TCP 的接收端都是拥有较大的窗口,这时发送端的发送窗口就完全由其拥塞窗口 cwnd 决定了;网络上拥有成千上万的 TCP 连接,它们在相互争用网络带宽,TCP 的流量控制使得它想要独享整个网络,而拥塞控制又限制其必要时做出牺牲来体现公平性。于是在传输稳定的时候 TCP 发送端呈现出下面过程的反复:
[1]用慢启动或者拥塞避免方式不断增加其拥塞窗口,直到丢包的发生; [2]然后将发送窗口将下降到 1 或者下降一半,进入慢启动或者拥塞避免阶段(要看是由于超时丢包还是由于冗余 ACK 丢包);过程如下图:
【2】接收端拥有小窗口的直线状态
这种情况下是接收端非常慢速,接收窗口一直很小,这样发送窗口就完全有接收窗口决定了。由于发送窗口小,发送数据少,网络就不会出现拥塞了,于是发送窗口就一直稳定的等于那个较小的接收窗口,呈直线状态。
【3】两个直连网络端点间的满载状态下的直线状态
这种情况下,Peer 两端直连,并且只有位于一个 TCP 连接,那么这个连接将独享网络带宽,这里不存在拥塞问题,在他们处理能力足够的情况下,TCP 的流量控制使得他们能够跑慢整个网络带宽。
通过上面我们知道,在 TCP 传输稳定的时候,各个 TCP 连接会均分网络带宽的。相信大家学生时代经常会发生这样的场景,自己在看视频的时候突然出现视频卡顿,于是就大叫起来,哪个开了迅雷,赶紧给我停了。其实简单的下载加速就是开启多个 TCP 连接来分段下载就达到加速的效果,假设宿舍的带宽是 1000K/s,一开始两个在看视频,每人平均网速是 500k/s,这速度看起视频来那叫一个顺溜。突然其中一个同学打打开迅雷开着 99 个 TCP 连接在下载爱情动作片,这个时候平均下来你能分到的带宽就剩下 10k/s,这网速下你的视频还不卡成幻灯片。
在通信链路带宽固定(假设为 W),多人公用一个网络带宽的情况下,利用 TCP 协议的拥塞控制的公平性,多开几个 TCP 连接就能多分到一些带宽(当然要忽略有些用 UDP 协议带来的影响),然而不管怎么最多也就能把整个带宽抢到,于是在占满整个带宽的情况下,下载一个大小为 FS 的文件,那么最快需要的时间是 FS/W,难道就没办法加速了吗?
答案是有的,这样因为网络是网状的,一个节点是要和很多几点互联的,这就存在多个带宽为 W 的通信链路,如果我们能够将要下载的文件,一半从 A 通信链路下载,另外一半从 B 通信链路下载,这样整个下载时间就减半了为 FS/(2W),这就是 p2p 加速。相信大家学生时代在下载爱情动作片的时候也遇到过这种情况,明明外网速度没这么快的,自己下载的爱情动作片的速度却达到几 M/s,那是因为,你的左后或右后的宿友在帮你加速中。我们都知道 P2P 模式下载会快,并且越多人下载就越快,那么问题来了,P2P 下载加速理论上的加速比是多少呢?

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

WEB前端

QQ|手机版|小黑屋|金桨网|助学堂  咨询请联系站长。

GMT+8, 2024-4-16 13:13 , Processed in 0.057767 second(s), 23 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表