第12讲 | TCP协议(下):西行必定多妖孽,恒心智慧消磨难

公网是不可靠的,需要很多机制保证传输的可靠性,这里需要恒心,即各种重传的策略,还要有智慧,即各种算法。

如何做个靠谱的人?

怎么算是个靠谱的人?领导交代的事情到底能不能做到,什么时候完成都要有一个应答,回复。这样,处理过程中有异常,能尽快让领导知道。

对应到网络协议上,客户端每发送一个包,服务端都应该有个回复,如果服务端超过一定的时间没有回复,客户端就会重新发送这个包,直到有回复。

这个发送应答的过程是怎样的呢?可以是上一个收到了应答,再发送下一个。这种模式像是打电话,你一句,我一句,缺点是效率比较低。如果一方在电话那头处理的事件比较长,这一头得干等着,双方都没法干其他事情。

日常办公可不是这样的,交代下属办一件事,他应该将事情记录下来,办完一件回复一件。在他办事的过程中,可以同时交代新的事情。使用这种模式,需要准备一个本子,每交代下属一个事,双方的本子都要记录一下。

当下属做完一件事,就回复你做完了,你就在本子上将这个事情划掉。同时你的本子上每件事都有时限,如果超过了时限还没有回复,就要主动重新交代一下,上次那件事,你还没回复我,咋样了?

既然多件事可以一起处理,那就需要给每个事情编个号,防止弄错。

如何实现一个靠谱的协议?

TCP协议使用的也是同样的模式。为了保证顺序,每一个包都一个ID。在建立连接的时候,商定起始ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,这个应答不是一个一个来的,而是会应答之前的某个ID,表示这个ID之前的包都收到了,这种模式称为累计确认或累计应答(cumulative acknowledge)。

为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别有缓存来保存记录。发送端的缓存里按照包的ID一个个排列,根据处理的情况分成四个部分。

第一部分:发送了并且已经确认的。

第二部分:发送了并且尚未确认的。

第三部分:没有发送,但是已经等待发送的。

第四部分:没有发送,并且暂时还不会发送的。

为什么第三部分和第四部分要做区分呢?一下子都发送走不行吗?

我们上一节的口诀里有“流量控制,把握分寸”。作为项目的管理人员,你应该根据员工的工作能力、抗压能力评估分给他多少工作量。工作量布置少了,就会不饱和;布置多了,就会做不完。

在TCP里,接收端会给发送端报一个窗口的大小,叫Advertise window。这个窗口大小等于上面的第二部分加上第三部分。超过这个窗口的,接收端就做不过来了。

发送端保存下面的数据结构。

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

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

第一部分:接收并且确认过的。

第二部分:还没接收,但马上就能接收的。

第三部分:还没接收,也没法接的。

对应的数据结构如下:

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

NextByteExpected-LastByteRead就是还没被应用层读取的部分,我们定义为A。

AdvertiseWindow=MaxRcvBuffer-A。

即:AdvertiseWindow=MaxRcvBuffer - ( (NextByteExpected-1) - LastByteRead)。

顺序问题与丢包问题

结合刚才的图,在发送端来看,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没到,出现了乱序,缓存着但是没办法ACK。

我们可以看到,顺序问题和丢包问题都有可能发生,所以我们先来看确认与重发的机制。

假设4的确认到了,5的确认丢了,6、7的数据包丢了,怎么办?

一种方法是超时重试,对每一个发送了,但没有ACK的包都有一个定时器,超过了一定时间就重新尝试。这个时间不宜过短,必须大于往返时间RTT,否则会引起不必要的重传。也不宜过长,不然访问会很慢。

估计往返时间,需要TCP通过采样RTT的时间,然后进行加权平均,算出一个值,这个值会随着网络的情况不断变化。除了采样RTT,还要采样RTT的波动范围,算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。

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

超时触发重传的问题是,超时周期可能较长。是不是又更快的方式呢?

有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的ACK,仍然ACK的时期望接收的报文段。而当发送端接收到三个冗余的ACK后,就会在定时器过期之前,重传丢失的报文段。

例如,接收方发现6收到了,8也收到了,但是7还没来,那肯定是丢了,于是发送6的ACK,要求下一个是7。接下来,收到后续的包,然后发送6的ACK,要求下一个是7。当发送端收到3个重复的ACK,就会发现7确实丢了。不等超时,马上重发。

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

流量控制问题

对于包的确认中,同时会携带一个窗口大小信息。

我们先假设窗口不变的情况,窗口大小始终为9。4的确认来的时候,右移一个,这个时候第13个包可以发送了。

这个时候,假设发送端发送过猛,将第三部分的10、11、12、13全部发送完毕,之后就停止发送,未发送可发送部分为0.

当包5的确认到达的时候,在发送端相当于窗口再滑动一格,第14个包才可以发送。

如果接收方实在处理太慢,导致缓冲区没有空间,可以通过修改窗口的大小,甚至设置为0,则发送方将暂时停止发送。

我们假设一个极端情况,接收端应用一直不读取缓存中的数据,当数据包6确认后,窗口大小就不能再是9了,要缩小变为8。

这个新的窗口8通过6的确认消息到达发送端的时候,你会发现窗口并没有平行右移,仅仅是左边的边右移了,窗口的大小从9变成了8。

如果接收端还是一直不处理数据,随着确认的包越来越多,窗口越来越小,直到为0。

当这个窗口通过包14的确认到达发送端的时候,发送端的窗口也调整为0,停止发送。

如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口大小。当接收方比较慢的时候,别空出一个字节来就赶快告诉发送方,然后马上又填满,可以当窗口太小的时候,不更新窗口,直到达到一定大小,才更新窗口。

这就是我们常说的流量控制。

拥塞控制

拥塞控制也是通过窗口大小来控制,前面的滑动窗口rwnd是怕发送方吧接收方缓存塞满,而拥塞窗口cwnd,是怕把网络塞满。

这里有一个公式LastByteSent - LastByteAcked <= min {cwnd, rwnd},是拥塞窗口和滑动窗口共同控制发送的速度。

TCP发送包常被比喻为往一个水管里面灌水,而TCP的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。

水管的粗细好比网络的带宽,即每秒能够发送多少数据;水管的长度好比端到端的时延。水管里的水量=水管粗细 × 水管长度;

通道的容量 = 带宽 × 往返延迟。

如果我们设置发送窗口,使得发送但未确认的包为通道的容量,就能撑满整个管道。

如图所示,假设往返时间是8s,去4s,回4s,每秒发送一个包,每个包1024byte。已经过去8s,则8个包都发出去了,前4个包已经到达接收端,但是ACK还没有返回到发送端,不能算发送成功。5-8后4个包还在路上,还没被接收。这个时候管道正好撑满。在发送端,以发送8个未确认的为8个包,正好等于带宽,即每秒发送1个包,乘以来回时间8s。

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

原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包耗费1s,所以到达另一端要耗费4s,如果发送的速度更快,单位时间内有更多的包到达中间设备,这些设备处理不过来,包就会被丢弃,这不是我们希望的。

还有其他处理办法,例如在这四个中间设备增加缓存,处理不过来就在队列里排着,这样包就不会丢失,但是会增加时延。如果超时到一定程度就会超时重传。这也不是我们希望的。

TCP的拥塞控制主要就是用来避免产生这两种现象,包丢失和超时重传。一但出现这两种现象就说明,发送速度太快了,要慢一点。但是我一开始怎么知道速度多快合适呢?我们知道该把窗口调整到多大呢?

有一个叫做慢启动的过程。

一条TCP连接开始,cwnd设置为一个报文段,一次只能发送已给;当收到这一个确认的时候,cwnd加一,于是一次发送两个;当这两个的确认到来的时候,每个确认cwnd增加1,cwnd变为4,于是一次能够发送四个;当这四个的确认到来的时候,cwnd变为8,于是一次能发送八个。可以看出这是指数型增长。

涨到什么时候呢?有一个值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拥塞算法。它企图找到一个平衡点,不断地加快发送速度,将管道填满,但不要填满中间设备的缓存,因为这样会增加延时。很好地到达高带宽和低延时的平衡。

小结

  • 顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的。
  • 拥塞控制是通过拥塞窗口来解决的。相当于往水管理倒水,快乐会溢出,满了浪费带宽,要摸着石头过河,找到最优值。

两个思考题:

1. 你知道TCP的BBR是如何达到最优点的吗?

2. 学会了UDP和TCP,你知道如何基于这两种协议写程序吗?

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值