learn-tech/专栏/趣谈网络协议/12TCP协议(下):西行必定多妖孽,恒心智慧消磨难.md
2024-10-16 11:00:45 +08:00

231 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
12 TCP协议西行必定多妖孽恒心智慧消磨难
我们前面说到玄奘西行,要出网关。既然出了网关,那就是在公网上传输数据,公网往往是不可靠的,因而需要很多的机制去保证传输的可靠性,这里面需要恒心,也即各种重传的策略,还需要有智慧,也就是说,这里面包含着大量的算法。
如何做个靠谱的人?
TCP想成为一个成熟稳重的人成为一个靠谱的人。那一个人怎么样才算靠谱呢咱们工作中经常就有这样的场景比如你交代给下属一个事情以后下属到底能不能做到做到什么程度什么时候能够交付往往就会有应答有回复。这样处理事情的过程中一旦有异常你也可以尽快知道而不是交代完之后就石沉大海过了一个月再问他说啊我不记得了。
对应到网络协议上,就是客户端每发送的一个包,服务器端都应该有个回复,如果服务器端超过一定的时间没有回复,客户端就会重新发送这个包,直到有回复。
这个发送应答的过程是什么样呢?可以是上一个收到了应答,再发送下一个。这种模式有点像两个人直接打电话,你一句,我一句。但是这种方式的缺点是效率比较低。如果一方在电话那头处理的时间比较长,这一头就要干等着,双方都没办法干其他事情。咱们在日常工作中也不是这样的,不能你交代你的下属办一件事情,就一直打着电话看着他做,而是应该他按照你的安排,先将事情记录下来,办完一件回复一件。在他办事情的过程中,你还可以同时交代新的事情,这样双方就并行了。
如果使⽤这种模式,其实需要你和你的下属就不能靠脑⼦了,⽽是要都准备⼀个本⼦,你每交代下属⼀个事情,双方的本子都要记录⼀下。
当你的下属做完⼀件事情,就回复你,做完了,你就在你的本⼦上将这个事情划去。同时你的本⼦上每件事情都有时限,如果超过了时限下属还没有回复,你就要主动重新交代⼀下:上次那件事情,你还没回复我,咋样啦?
既然多件事情可以一起处理那就需要给每个事情编个号防止弄错了。例如程序员平时看任务的时候都会看JIRA的ID而不是每次都要描述一下具体的事情。在大部分情况下对于事情的处理是按照顺序来的先来的先处理这就给应答和汇报工作带来了方便。等开周会的时候每个程序员都可以将JIRA ID的列表拉出来说以上的都做完了⽽不⽤⼀个个说。
如何实现一个靠谱的协议?
TCP协议使用的也是同样的模式。为了保证顺序性每一个包都有一个ID。在建立连接的时候会商定起始的ID是什么然后按照ID一个个发送。为了保证不丢包对于发送的包都要进行应答但是这个应答也不是一个一个来的而是会应答某个之前的ID表示都收到了这种模式称为累计确认或者累计应答cumulative acknowledgment
为了记录所有发送的包和接收的包TCP也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的ID一个个排列根据处理的情况分成四个部分。
第一部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。
第二部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。
第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是马上就要交代的。
第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,而且暂时还不会交代给下属的。
这里面为什么要区分第三部分和第四部分呢?没交代的,一下子全交代了不就完了吗?
这就是我们上一节提到的十个词口诀里的“流量控制,把握分寸”。作为项目管理人员,你应该根据以往的工作情况和这个员工反馈的能力、抗压力等,先在心中估测一下,这个人一天能做多少工作。如果工作布置少了,就会不饱和;如果工作布置多了,他就会做不完;如果你使劲逼迫,人家可能就要辞职了。
到底一个员工能够同时处理多少事情呢在TCP里接收端会给发送端报一个窗口的大小叫Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分就是已经交代了没做完的加上马上要交代的。超过这个窗口的接收端做不过来就不能发送了。
于是,发送端需要保持下面的数据结构。
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没到出现了乱序缓存着但是没办法ACK。
根据这个例子,我们可以知道,顺序问题和丢包问题都有可能发生,所以我们先来看确认与重发的机制。
假设4的确认到了不幸的是5的ACK丢了6、7的数据包丢了这该怎么办呢
一种方法就是超时重试也即对每一个发送了但是没有ACK的包都有设一个定时器超过了一定的时间就重新尝试。但是这个超时的时间如何评估呢这个时间不宜过短时间必须大于往返时间RTT否则会引起不必要的重传。也不宜过长这样超时时间变长访问就变慢了。
估计往返时间需要TCP通过采样RTT的时间然后进行加权平均算出一个值而且这个值还是要不断变化的因为网络状况不断地变化。除了采样RTT还要采样RTT的波动范围计算出一个估计的超时时间。由于重传时间是不断变化的我们称为自适应重传算法Adaptive Retransmission Algorithm
如果过一段时间5、6、7都超时了就会重新发送。接收方发现5原来接收过于是丢弃56收到了发送ACK要求下一个是77不幸又丢了。当7再次超时的时候有需要重传的时候TCP的策略是超时间隔加倍。每当遇到一次超时重传的时候都会将下一次超时时间间隔设为先前值的两倍。两次超时就说明网络环境差不宜频繁反复发送。
超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?
有一个可以快速重传的机制当接收方收到一个序号大于下一个所期望的报文段时就会检测到数据流中的一个间隔于是它就会发送冗余的ACK仍然ACK的是期望接收的报文段。而当客户端收到三个冗余的ACK后就会在定时器过期之前重传丢失的报文段。
例如接收方发现6收到了8也收到了但是7还没来那肯定是丢了于是发送6的ACK要求下一个是7。接下来收到后续的包仍然发送6的ACK要求下一个是7。当客户端收到3个重复ACK就会发现7的确丢了不等超时马上重发。
还有一种方式称为Selective Acknowledgment SACK。这种方式需要在TCP头里加一个SACK的东西可以将缓存的地图发送给发送方。例如可以发送ACK6、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发送包常被比喻为往一个水管里面灌水而TCP的拥塞控制就是在不堵塞不丢包的情况下尽量发挥带宽。
水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量=水管粗细 x 水管长度。对于到网络上,通道的容量 = 带宽 × 往返延迟。
如果我们设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。
-
如图所示假设往返时间为8s去4s回4s每秒发送一个包每个包1024byte。已经过去了8s则8个包都发出去了其中前4个包已经到达接收端但是ACK还没有返回不能算发送成功。5-8后四个包还在路上还没被接收。这个时候整个管道正好撑满在发送端已发送未确认的为8个包正好等于带宽也即每秒发送1个包乘以来回时间8s。
如果我们在这个基础上再调大窗口,使得单位时间内更多的包可以发送,会出现什么现象呢?
我们来想原来发送一个包从一端到达另一端假设一共经过四个设备每个设备处理一个包时间耗费1s所以到达另一端需要耗费4s如果发送的更加快速则单位时间内会有更多的包到达这些中间设备这些设备还是只能每秒处理一个包的话多出来的包就会被丢弃这是我们不想看到的。
这个时候我们可以想其他的办法例如这个四个设备本来每秒处理一个包但是我们在这些设备上加缓存处理不过来的在队列里面排着这样包就不会丢失但是缺点是会增加时延这个缓存的包4s肯定到达不了接收端了如果时延达到一定程度就会超时重传也是我们不想看到的。
于是TCP的拥塞控制主要来避免两种现象包丢失和超时重传。一旦出现了这些现象就说明发送速度太快了要慢一点。但是一开始我怎么知道速度多快呢我怎么知道应该把窗口调整到多大呢
如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。
一条TCP连接开始cwnd设置为一个报文段一次只能发送一个当收到这一个确认的时候cwnd加一于是一次能够发送两个当这两个的确认到来的时候每个确认cwnd加一两个确认cwnd加二于是一次能够发送四个当这四个的确认到来的时候每个确认cwnd加一四个确认cwnd加四于是一次能够发送八个。可以看出这是指数性的增长。
涨到什么时候是个头呢有一个值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拥塞算法。它企图找到一个平衡点就是通过不断地加快发送速度将管道填满但是不要填满中间设备的缓存因为这样时延会增加在这个平衡点可以很好的达到高带宽和低时延的平衡。
小结
好了,这一节我们就到这里,总结一下:
顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的,这其实就相当于你领导和你的工作备忘录,布置过的工作要有编号,干完了有反馈,活不能派太多,也不能太少;
拥塞控制是通过拥塞窗口来解决的,相当于往管道里面倒水,快了容易溢出,慢了浪费带宽,要摸着石头过河,找到最优值。
最后留两个思考题:
TCP的BBR听起来很牛你知道他是如何达到这个最优点的嘛
学会了UDP和TCP你知道如何基于这两种协议写程序吗这样的程序会有什么坑呢
欢迎你留言和我讨论。趣谈网络协议,我们下期见!