[趣谈网络协议学习] 12 TCP协议(下):西行必定多妖孽,恒心智慧消磨难

TCP 协议为了保证顺序性,每个包都有一个ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为保证不丢包,对于发送的包都要进行应答,应答不是一个一个来,而是应答某个之前的ID,表示都收到了,称累计确认累计应答。

流量控制

流量控制,通过滑动窗口控制发送方的速度,防止将接收方塞满。

发送端:

在 TCP 里,接收端会给发送端报一个窗口的大小,叫Advertised window。这个窗口的大小应该等于下面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

	第一部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。
	第二部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。
	第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是马上就要交代的
	第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,而且暂时还不会交代给下属的。

为什么要区分第三部分和第四部分呢?为了“流量控制,把握分寸”

发送端需要保持的数据结构

  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

接收端:

对于接收端来讲,它的缓存里记录的内容要简单一些。

	第一部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。
	第二部分:还没接收,但是马上就能接收的。也即是我自己的能够接受的最大工作量。
	第三部分:还没接收,也没法接收的。也即超过工作量的部分,实在做不完

接收端需要保持的数据结构

  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead 之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected 是第一部分和第二部分的分界线。

第二部分的窗口有多大呢?

NextByteExpected 和 LastByteRead 的差是还没被应用层读取的部分占用掉 MaxRcvBuffer 的量,定义为 A。

AdvertisedWindow 其实是 MaxRcvBuffer 减去 A。也就是:AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。

那第二部分和第三部分的分界线在哪里呢?

NextByteExpected 加 AdvertisedWindow 就是第二部分和第三部分的分界线,其实也就是 LastByteRead 加上MaxRcvBuffer。

第二部分里面,由于受到的包可能不是顺序的,会出现空挡,只有和第一部分连续的,可以马上进行回复,中间空着的部分需要等待,哪怕后面的已经来了。

顺序问题与丢包问题

如上面的图:

  • 在发送端来看,1、2、3 已经发送并确认;4、5、6、7、8、9 都是发送了还没确认;10、11、12 是还没发出的;13、14、15 是接收方没有空间,不准备发的。

  • 在接收端来看,1、2、3、4、5 是已经完成 ACK,但是没读取的;6、7 是等待接收的;8、9 是已经接收,但是没有 ACK 的。

  • 发送端和接收端当前的状态如下

    • 1、2、3 没有问题,双方达成了一致。(接收端 接收确认,发送端 发送被接收确认)
    • 4、5 接收方说 ACK 了,但是发送方还没收到(接收端 接收确认,发送端 发送被接收未确认),有可能丢了,有可能在路上。
    • 6、7、8、9 肯定都发了,但是 8、9 已经到了,但是 6、7 没到(接收端 6 7 8 9不能接收确认),出现了乱序,缓存着但是没办法 ACK。

假设 4 的确认到了,不幸的是,5 的 ACK 丢了(对于发送端),6、7 的数据包丢了(对于接收端)

处理方法:

一种方法是超时重传

对每个发送了但没有收到ACK的包,设一个定时器,超时后重传。时间应大于RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。

估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均。除了采样RTT,还要采样RTT的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,称为自适应重传算法(AdaptiveRetransmission Algorithm)

如果过一段时间,5、6、7 都超时了,发送端就会重新发送,接收端发现 5 原来接收过,于是丢弃 5;6 收到了,发送 ACK,要求下一个是 7,7 不幸又丢了。当 7 再次超时的时候,有需要重传的时候,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

超时触发重传存在的问题是,超时周期可能相对较长。有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,说明数据流中有间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。例如,接收方发现 6、8、9 都已经接收了,就是 7 没来,那肯定是丢了,于是发送三个 6 的 ACK,要求下一个是 7。客户端收到 3 个,就会发现 7 的确又丢了,不等超时,马上重发。

另一种方法是Selective Acknowledgment (SACK)

这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制问题

流量控制机制,在对于包的确认中,同时会携带一个窗口的大小

  1. 先假设窗口不变的情况,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了。
    发送端:
    发送端
  2. 假设发送端发送过猛,会将第三部分的 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分为 0。
    发送端:
    发送端
  3. 来自接收端包 5 的确认到达的时候,在发送端相当于窗口再滑动了一格,才可以有更多的包可以发送了,例如第 14 个包才可以发送。
    发送端:
    发送端
  4. 如果接收方实在处理的太慢,导致缓存中没有空间,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。
  5. 假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
    接收端:
    接收端
  6. 这个新的窗口 8 通过 6 的确认消息到达发送端的时候,窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从 9 改成了 8。
    发送端:
    发送端
  7. 如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0。
    接收端:
    接收端
  8. 当这个窗口通过包 14 的确认到达发送端的时候,发送端的窗口也调整为 0,停止发送。
    发送端:
    发送端
  9. 如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。

注意:当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。

拥塞控制问题

拥塞控制,通过拥塞窗口控制发送方的速度,防止将网络塞满。

前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。

LastByteSent - LastByteAcked <= min {cwnd, rwnd} ,拥塞窗口和滑动窗口共同控制发送的速度。

如何判断网络是否满?
网络上,通道的容量 = 带宽 × 往返延迟。如果我们设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。
拥塞控制
如图所示,假设往返时间为 8s,去 4s,回 4s,每秒发送一个包,每个包 1024byte。4 个包已经到达接收端,但是 ACK 还没有返回,不能算发送成功。5-8 后四个包还在路上,还没被接收。这个时候,整个管道正好撑满。

如果我们在这个基础上再调大窗口,使得单位时间内更多的包可以发送,会出现什么现象呢?

假设一共经过四个设备,每个设备处理一个包时间耗费 1s,所以到达另一端需要耗费 4s。

如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃,这是我们不想看到的。

如果在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,如果时延达到一定程度,就会超时重传也是我们不想看到的。

于是 TCP 的拥塞控制主要来避免两种现象,包丢失超时重传

开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?

慢启动阶段:需要试探性的发包,一开始 拥塞窗口cwnd =1,收到一个确认加1,于是下次发送2个包,下下次发送4个包,接下来的一次就发送8个包,窗口大小就会这样不断翻倍,指数增长。

拥塞避免阶段:窗口大小达到ssthresh后( ssthresh 为 65535 个字节),开始线性增加。每收到一个确认后,cwnd 增加 1/cwnd,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。

线性增长还是增长直到出现丢包(拥塞的一种表现形式),将 sshresh 设为 cwnd/2,cwnd = 1,重新开始慢启动。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。

快速重传算法,当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为网络拥塞情况不严重,就使cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3

拥塞控制
TCP这种知进退,使得时延很重要的情况下,反而降低了速度。

TCP 的拥塞控制是有问题的:

  • 丢包不代表网络已满,也可能是管子本来就漏水。公网带宽不满也会丢包,这个时候就认为拥塞了,其实是不对的。
  • TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。

为了优化这两个问题,后来有了TCP BBR 拥塞算法。企图找一个平衡点,通过不断加快发送速度,填满网络,但不填满中间设备的缓存,可达到高带宽和低时延的平衡。

小结

顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的。

拥塞控制是通过拥塞窗口来解决的,相当于往管道里面倒水,快了容易溢出,慢了浪费带宽。

参考资料:

趣谈网络协议(极客时间)链接:
http://gk.link/a/106nW


GitHub链接:
https://github.com/lichangke/LeetCode
知乎个人首页:
https://www.zhihu.com/people/lichangke/
CSDN首页:
https://me.csdn.net/leacock1991
欢迎大家来一起交流学习

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值