前言
这篇文章是整个读书总结系列的最后一篇,有关TCP我想总结的内容都会在这篇文章结束。当然这并不是TCP的全部,总共的五篇文章都只是计算机网络的基础。枯燥而又繁杂的知识点只是进入网络领域的入场券,学会理解了基础才可能继续往下深耕。
作为上篇的承接,下篇我们开始着手认识TCP的拥塞避免策略。对于传统的TCP而言,拥塞的判断完全依赖于丢包的情况,这样判断的理由是基于对现实网络情况的总结经验。我们会考虑超时和收到重复ack两种情况的拥塞,并对于它们的不同做出不同的决断。之后会简单介绍一个TCP连接上的四个定时器。文章的最后我们简单讨论了TCP的现在和未来。
说实话我并不认为TCP这些具体的操作内容对于阅读者有任何的意义,因为这些书本理论的东西和实际存在了非常大的鸿沟。我希望的是,能在介绍这些内容的过程中,和读者分享一个感受:TCP做为一个工程化的东西,受限于技术、硬件等等条件,设计的过程是充满推断和妥协的。如何使用有限的资源实现一个理论上不可能的需求,TCP给我们上了非常好的一课。
《TCP/IP协议 卷一》后面的章节还介绍了一些其他的内容,主要是几个应用层的协议。可以简单看一看了解一下,需要时再去参考。不再专门讨论总结。
TCP的拥塞避免
TCP提供了可靠的传输服务。其中一个很重要的策略就是接收端必须返回ack,用以让发送端确认数据抵达。但这个策略的问题在于,不可靠的信道上发送的数据报和返回的ack都有可能丢失,为了解决这个问题,发送数据时,TCP会在发送端设置一个定时器用以检查ack的返回。如果定时器溢出时还没有收到接收端的ack,那么发送端就会重传这部分的数据。
TCP发送数据的ack和两军问题不同在于,数据正确送达只需要发送端确认即可。所以问题变的简单很多,只需要一次单向握手就可以解决。
要实现一个高效的传输服务,关键就在于超时和重传策略的确定。简单来说,选择一个合适的超时间隔和超时如何继续重传是非常重要的。
首先让我们看一看超时时间间隔的选择。在TCP当中有一个非常重要的概念:RTT(往返时间Round-Trip Time)。在计算机网络中它是一个重要的性能指标。
什么是RTT
摘取一段教材的定义
“We define the round-trip time, which is the time it takes for a small packet to travel from client to server and back to the client.”
“The RTT includes packet-progation delays, packet-queuing delays and packet -processing delay.”
RTT指代的是从发送端发送数据开始,接收端收到数据立即确认,到发送端收到来自接收端确认往返双向的时延。通常情况下一个单向的时延我们是这样计算的。
单向时延 = 传输时延(t1) + 传播时延(t2) + 排队时延(t3)
t1
指数据从进入节点到传输媒体所需的时间,通常等于 数据块长度
/信道带宽
t2
指信号在信道上传播所需的时间,通常等于 信道长度
/传播速率
t3
通常受每一跳设备及收发两端负荷情况还有吞吐情况影响
上述公式中可以看出,RTT的大小受多个因素影响。假设在同一个连接上t1
可以认为保持不变,但路由器和网络流量的不确定,导致RTT的大小是会经常发生变化的。为了解决这样一个问题,TCP会跟踪这些变化并不断调整RTT,以此为根据来设置相应的超时间隔。
为了方便理解,先举一个简单的例子:
作者读高中的时候每天骑车往返与学校和家,假设往返一趟的时间是20min。每天放学的时间是5:40 pm,那么在六点之前如果我还没有回家,父母就会电话老师询问是否学校有留堂。
参考上面的内容,父母会根据我往返学校的时间得出我大概的回家时间,如果我没有按时回来,那么大概率的情况是有意外情况的发生。
对比参考RTT,指代的就是我往返学校的时间,父母就是发送端。TCP会根据RTT来决定超时间隔,判断发送数据报的ack大概的返回时间,从而设置定时器。如果定时器溢出发送端仍然没有收到ack,那么我们有理由认为数据报在传输的过程中丢失,需要进行重传。
本文不介绍RTT测量相关的内容,有兴趣的朋友可以自行查阅
这里需要强调的是,对于发送端而言,报文被传送出去之后发生的事情,实际都是无处可知的。TCP很多时候采取的策略都是基于已有的部分信息,结合实验得出的普遍结论来推断得到的。结合重传策略来说,在定时器溢出时,发送端实际并不知道数据报的实际情况。但是TCP会以此作为信道拥塞的判定标准,认为信道发生拥塞需要处理。
信道拥塞这个结论的前提是数据报的丢失是因为拥塞而不是分组数据受到损坏(一般来说损坏导致的丢失远小于1%),获取的RTT是相对准确的。试想,如果前提条件不再成立,那么信道拥塞的结论是否还值得相信呢?
在网络不太稳定的场景(比如无线网络)中,无线网络因为短暂的信号干扰会导致的丢包。但传统的TCP流控算法会认为发生拥塞从而指数避让,导致速率大幅下降。很显然在TCP设计之初并没有考虑到这样的情况。
实际这也是一个必然。工作中的经历时常让我思考完美这个东西在工程当中是否真的存在,因为考虑到成本,时间以及技术等等各方面的限制,要做到让各方面都满意实际并不现实。总结过往的经验,基于已有的信息做大胆假设,结合实际需求做出一个取舍,是非常必要的。
为了处理分组丢失的情况,TCP会采取拥塞避免算法。
在一个TCP的连接上,发送数据的速率同时受通告窗口和拥塞窗口约束,这是TCP的流量控制。在拥塞窗口大小超过通告窗口之前,TCP会通过慢启动的方式不断扩大拥塞窗口,直到拥塞窗口超过通告窗口大小或者信道发生拥塞,TCP分组丢失。
拥塞避免算法本质是为了解决传输速率过快导致的分组丢失的问题。虽然拥塞避免算法和慢启动算法是两个目的不同,独立的算法,但是两者通常会在一起实现。对于一个TCP连接,拥塞避免算法和慢启动算法维持了两个变量:拥塞窗口和慢启动门限ssthresh。
先简单介绍一下,sshthresh是一个阈值。当cwnd小于等于ssthresh时TCP采取的是慢启动算法,当突破这个阈值之后TCP会转为拥塞避免算法。详细的分析我们在下面的算法过程中继续分析。
具体的算法过程如下:
1) 对于一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
这里需要强调的是两个变量的单位是不一样的!cwnd是以报文段为单位,大小是三次握手约定的MSS;而ssthresh这个阈值在开始则被置为TCP单个报文的最大长度。
65535这个大小限制是因为报文头部描述报文长度的字段字节数限制的。当然我们可以通过选项扩大这个限制。
2)TCP的输出不能超过cwnd和通告窗口
这个非常简单,是我们强调很多遍的流量控制。拥塞窗口是发送方对于网络拥塞的试探和估测,而通告窗口则是接收方对于自身缓冲区可用大小的描述。
3)当拥塞发生时,sshthresh会被置为当前窗口大小的一半。如果是超时引起的拥塞,那么cwnd会被设置为1个报文段
首先我们要考虑的是拥塞发生的情况:第一种是我们上文提到的分组丢失导致的超时;另一种是连续的数据报丢失了其中的一个,后续报文没有丢失发送端会收到重复确认。
我们假设分组丢失是由于网络拥塞造成的,对于上述的两种情况TCP的判断是不一样的:超时被认为网络拥塞严重几近瘫痪;而重复确认则是网络发生了一点拥塞,影响了部分通信但网络目前仍然可用。基于这样两个判断TCP采取了不同的处理方案,重复确认的情况会触发TCP的快速重传和快速恢复算法,这一部分我们放在后面详细介绍,这里先考虑超时这一种情况。
让我们重新回头来看sshthresh。超时导致的拥塞发生时,sshthresh会被置为当前窗口大小的一半,要了解这样设置的原因,我们需要重新认识一下什么是sshthresh。
TCP的传输速率同时受通告窗口和拥塞窗口约束,这里我们假设通告窗口的大小保持不变,而拥塞窗口根据慢启动算法以指数的形式增长。
拥塞通常应该是发生在t1
之前,也就是拥塞窗口小于等于通告窗口之前。假设拥塞窗口扩大到通告窗口之后仍未发生拥塞,那么之后最小窗口会一直受通告窗口约束,一般情况下不可能再发生拥塞。基于这样一个判断,在拥塞发生时TCP会更多关注拥塞窗口的大小。
让我们重新回顾慢启动算法下拥塞窗口的扩大规则:1,2,4,8... 以指数级增长。考虑这样一个情况
收到确认 | 窗口大小 | 是否发生拥塞 |
---|---|---|
1 | 1 | False |
2 | 2 | False |
3 | 4 | False |
4 | 8 | False |
5 | 16 | False |
6 | 32 | True |
假设在时刻6拥塞窗口扩大到32个报文段大小的时候发生了拥塞,我们可以得到这样一个信息:合适的窗口大小范围应该在16 ~ 32个报文段之间。sshthresh设置为当前发生拥塞的窗口大小的一半,实际是为了记录上一个可用的拥塞窗口大小。
因为拥塞窗口的急剧缩小,发送端的传输速率会发生骤降。
4)当新的数据被对方确认时会增加cwnd,但具体增加的方式取决于当前是在进行慢启动还是拥塞避免算法
当cwnd小于sshthresh时,TCP采取的是慢启动算法;当cwnd慢慢扩大到sshthresh时TCP会转为拥塞避免的算法来增加cwnd。
这个策略非常容易理解。考虑之前的例子,sshthresh记录的是上一个可用的拥塞窗口大小,那么我们认为当cwnd小于这个大小时可以放心的采用慢启动算法;但是当cwnd达到这个阈值以后,很显然慢启动算法已经不再合适,拥塞窗口的指数级扩大会造成网络拥塞,所以我们改换了策略来避免拥塞的发生,这就是拥塞避免算法。
拥塞避免算法要求每次收到一个确认会将cwnd增加1/cwnd,和慢启动的指数增加相比,拥塞避免算法是一种加性的增加。TCP希望在一个RTT内cwnd最多增加一个报文段大小。
书中21.6介绍拥塞避免算法确实使用的是每次增加1/cwnd,但是很显然这个公式并不合理。因为后续无限叠加下去结果一定小于2/cwnd(参考1 + 1/2 + 1/3 + .... )
在21.8中实际举了一个拥塞避免的例子,这一节给出了实际的计算过程。其中拥塞避免发生时cwnd的计算公式如下
cwnd = (segsize * segsize)/cwnd + segsize/8
我不太理解1/cwnd的增加方式是如何理解的,个人更倾向于认为这是一种错误的打印。
需要强调的是为了描述和理解的方便,我们更倾向于使用报文段来作为单位描述两个变量,但实际上两者都是以字节为单位来进行维护的。
总结
拥塞避免算法本质上是为了应对超时这样一种拥塞情况做出的应对措施,替换慢启动,以一种更加平缓线性的增长方式来扩大拥塞窗口,从而不断试探逼近网络的极限。
在拥塞发生的时候受cwnd会被重置为1,这会导致TCP的传输速率发生跳水。理由也非常的简单:我们认为此时网络几近瘫痪,发送端降低传输速率让网络有时间恢复通畅是十分有必要的。
TCP的快速重传和快速恢复
现在让我们审视拥塞的另一种情况:连续传输的报文中间丢失了一个分组。后续报文的送达会让接收端立即产生一个重复的ack返回给发送端,并且这个ack不应该被迟延。举一个简单的例子:
图上的报文段3丢失,但是后续的报文段4 5 6成功送达。接收端会不停的告诉发送端ack 2。之前文章我说过,可以把ack 2理解为报文段2之前的数据都已经被送达确认啦,但是在这里送达确认并不适合,我们改换一种理解的方式,将ack 2理解为报文段2之前的数据已经被送达确认啦,请给我报文段3。
让我们重新查看上图:报文4 5 6的送达会让接收端不停的向发送端发出请求:“请给我报文段 3”。从这个对话中可以挖掘出什么信息?
接收端没有收到报文段3
但是接收端收到了报文段3之后的三个报文段
通常情况下,发送端一个报文段如果只发送顺利,那么应当至多收到一个来自接收端的确认。但是上篇文章介绍过成块数据流的传输,TCP会不等待接收端的确认就将数据报发送出去,虽然我们发送是有序的,但传输的过程是无序的,我们无法保证数据报有序送达接收端。
对于发送端而言实质上无法判断重复ack究竟是因为报文段丢失还是几个报文段传递过程发生了乱序。考虑到这样一个情况,在收到1 - 2 次重复ack时TCP认为可能是中间发生了乱序,但是如果重复ack的次数到达3之后,TCP有理由认为这个分组已经丢失,那么此时TCP会立即重传丢失的报文段,无需等待超时定时器的溢出,这就是快速重传算法。
具体的过程如下:
1)当收到3个重复的ack时,将ssthresh设置为当前cwnd的一半。重传丢失的报文段,并设置cwnd为sshthresh加上3倍报文的大小
sshthresh的设置和超时情况一致,区别的地方在于超时引起的拥塞cwnd会被置为1个报文段大小;而报文段丢失则是将cwnd设置为sshthresh加上3倍报文段大小。我们必须思考两种策略不同的原因。
回想上文所说,TCP认为超时引起拥塞时,当前网络几近瘫痪;而报文段丢失的情况,网络虽然发生拥塞但仍然保持工作。假设第二种情况下如果我们仍然将cwnd设置为1个报文段大小会发生什么?
因为cwnd的骤减(突变为1),发送端将在长时间内不能发送任何一个报文!对于发送端而言,实际可用的窗口大小等于Min(通告窗口,拥塞窗口)
减去处于inflight状态下报文段的大小。
很显然发生快速重传时发送端至少有4个未被确认的报文段(丢失的报文段 + 3个引起重复确认的报文段)。当这个疑似丢失的报文段重传成功之后,接收方会同时返回多个ack的确认,原因很简单,丢失报文段后面的数据早已经被送达接收端。
如果我们采取和超时一样的策略,按照慢启动算法,cwnd会迅速的扩大。对于TCP而言,发送端的传输速率会几乎停止,等待一个RTT左右的时间,然后速率又突然暴涨。
很显然传输速率这样大的一个波动是很难接受的。
为此cwnd并没有被置为1,而是设置成了sshthresh + 3倍报文的大小。TCP认为当前网络并没有瘫痪还可以继续工作,所以慢启动的过程被省略。加上3倍报文大小是因为连续收到了三次重复ack,TCP会乐观的认为丢失报文段之后至少已经发成功三个报文段,那么我允许你的窗口透支这部分的大小。
2)快速重传之后,如果继续收到重复确认,那么每一个重复确认都会让cwnd扩大一个报文段的大小
实际这里cwnd扩大的原因和第一步中+ 3个报文段大小的原因类似,本质都是发送端在透支拥塞窗口的大小。TCP这么做的根据如下:
网络仍然在正常工作
cwnd = cnwd/2 调整之后已经缩小到一个安全的阈值
透支额度是为了应对处于inflight状态的报文段压缩实际可用的拥塞窗口大小
3)当下一个新的报文段确认到达之后,cwnd会被调整为sshthresh,也就是第一步中发生报文段丢失时cwnd的一半。
通常情况来说,新的报文段确认除了确认重传数据的送达,也应该会对丢失的分组和收到第一个重复ack之间发出的所有报文段进行确认。
cwnd调整的原因就在于此,我们在第1) 2)步骤中透支的窗口大小是考虑到处于inflight状态的报文段,但新的确定到来时这些处于inflight的报文段都得到了送达确认,那么透支的窗口大小就应该被还回。
这部分的策略我们称之为快速恢复。
我们举一个简单的例子来说明快速恢复策略的优势。假设TCP在传输101报文时发生了丢失,触发快速重传,假设cwnd = 10,那么发送端发送完110报文会停止发送。
- 如果不使用快速恢复算法
发送端收到的报文 | 收到报文的解释 | cwnd当前的大小 | 注释 | 当前发送端的状态 |
---|---|---|---|---|
ack 101 | 接收端收到102 | 10 | 第一次重复确认 | 停止发送 |
ack 101 | 接收端收到103 | 10 | 第一次重复确认 | 停止发送 |
ack 101 | 接收端收到104 | 10 | 第一次重复确认 | 停止发送 |
触发快速重传 | ||||
ack 101 | 接收端收到105 | 5 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到106 | 5 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到107 | 5 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到108 | 5 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到109 | 5 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到110 | 5 | inflight packet = 10 > cwnd | 停止发送 |
时间应该小于一个RTT | 等待101确认 | |||
ack 110 | 5 | 此时开始新的数据发送 |
- 使用快速恢复算法
发送端收到的报文 | 收到报文的解释 | cwnd当前的大小 | 注释 | 当前发送端的状态 |
---|---|---|---|---|
ack 101 | 接收端收到102 | 10 | 第一次重复确认 | 停止发送 |
ack 101 | 接收端收到103 | 10 | 第一次重复确认 | 停止发送 |
ack 101 | 接收端收到104 | 10 | 第一次重复确认 | 停止发送 |
5 + 3 = 8 | 触发快速重传 | |||
ack 101 | 接收端收到105 | 9 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到106 | 10 | inflight packet = 10 > cwnd | 停止发送 |
ack 101 | 接收端收到107 | 11 | inflight packet = 10 = cwnd | 发送 111 |
ack 101 | 接收端收到108 | 12 | inflight状态的报文数量大于cwnd | 发送 112 |
ack 101 | 接收端收到109 | 13 | inflight状态的报文数量大于cwnd | 发送 113 |
ack 101 | 接收端收到110 | 14 | inflight状态的报文数量大于cwnd | 发送 114 |
ack 110 | 5 | 触发快速恢复 | ||
拥塞避免算法 |
快速恢复的优势显而易见。
总结
快速重传和快速恢复本质是TCP对拥塞不严重的情况做出的特殊处理,避免发送端传输速率大起大落,能够保持一个较为稳定的速率一直传输。
至此,有关TCP的基础拥塞处理策略大概介绍完毕。花了这么大篇幅介绍拥塞避免的处理,实际意义并不是让读者要去死抠其中某个算法的实现细节,而是理解设计的思想,为什么会去这样设计。因为同一套策略可能面对不同的网络,表现是完全不一样的。
我认为,很多时候工程上的问题单独来看实际是无解的。所谓解决问题的策略,都是建立在一个实验数据的基础之上,从数据中总结出相关现象的规律,并以此为依据来进行策略的指定。当工程的环境发生改变,依赖的前提发生了改变,那么之前的策略反而可能成为累赘。
TCP就是一个很好的例子。历史的惯性很多时候不允许我们随意的推翻已有的技术,所以更多的时候我们会选择对原有技术进行修修补补。后面的文章我们会开一个小节,来简单谈谈TCP的现在和未来,了解一下它是如何拓展来适应现代的网络环境的。
TCP的定时器
对于每一个连接,TCP管理着四个不同的定时器,之前的文章里我们已经介绍过三个了,分别是:
重传定时器。用于判断接收端消息确认是否超时,时间的设定和RTT相关,TCP根据超时来设定拥塞避免的策略。
保活定时器。TCP连接会定时的检测一个空闲的连接是否仍然正常保持,可惜这个保活策略并不可靠。通常情况下应用层协议并不依赖TCP的保活机制,而是根据自身的应用场景指定额外的保活机制。
2MSL定时器。用以测量一个TCP连接处于TIME_WAIT的时间。
最后一个定时器和TCP的窗口相关:
- 坚持定时器。在收到零窗口的消息之后,TCP发送端会周期性地向接收端查询窗口是否扩大。
通告窗口是接收端采取的流量控制。当接收端无法继续接收新的数据时候,可以通过零窗口来阻止发送端继续传送数据,直到接收端处理结束,进行窗口更新,发送端继续传送数据。
但是窗口更新本质是一条重复的ack数据,只不过这条ack消息里通告窗口的大小发生了变化而已。回想文章开始我们说过的一句话,文件传输的过程是单向确认,也就是发送端确认送达即可。问题在于窗口更新的ack对应消息之前已经被发送端确认了,如果这条窗口更新这条报文丢失,那么双方将陷入死锁。为此发送端设置坚持定时器来主动查询窗口更新。
我们可以简单模拟这样一种情况:发送端尝试不断发送数据,接收端在accept()
之后执行sleep()
,让发送的数据堆积在缓冲区即可。
保活定时器
对于一些需要维持长连接的业务场景,保活机制是必不可少的。但是TCP本身提供的保活选项大多数情况下并不适用,具体的讨论我们放在了系列文章的第一篇,所以保活的功能一般会由各自的应用层协议来进行实现。
为此,我们不再讨论TCP的保活定时器。
TCP的现在
TCP设计之初是以拨号SLIP链路和以太数据链路为参考的,在80和90年代以太网是允许TCP/IP最主要的数据链路方式。但随着时代的推进,以太网的速率不断提高,无限网络的普及,TCP原先的设计可能不再适应。
路径MTU的发现
在三次握手的过程当中,TCP会约定MSS作为最大报文段来避免分片,但是显然存在一些缺陷
MSS实际只受通信两端MTU的影响,中间路由的MTU无法判断
MSS是一个静态的数值,无法动态感知数值的变化
为了解决发现中间网络的MTU这个问题,TCP设计了路径MTU发现机制。这个机制必须要和IP协议配合实现,利用的就是IP协议中DF(Don’t Framement)字段的设置。当DF为1时如果报文大小超过中间路由的MTU,那么中间路由会拒绝分片并返回ICMP 不可达的差错报文。
ICMP的差错报文会通知TCP减少报文段大小进行重传。如果仍然收到同一个中间路由的ICMP差错报文,那么TCP就需要继续尝试下一个可能的最小MTU。
中间路由是动态变化的,那么也就是说MTU可能会发生改变,这也是我们为什么要选择路径发现MTU的原因。考虑MTU缩小的情况,那么我们也必须考虑到MTU增大的情况,毕竟维持一个较小的MTU传输并不是一个高效的选择。路径MTU发现机制会在减少MTU一段时间之后,尝试使用一个较大的MTU来进行测试。
当然扩大的上限仍然是MSS
IPv6协议期待支持的设备提供的MTU最小是1280,目的就是为了避免MTU发现的过程。应该来说MTU相关的机制都是为了解决硬件设备标准不统一带来的麻烦。
长肥管道
上一篇文章的结尾我们讨论了带宽时延乘积,用以描述通信管道的容量。当这个容量不断扩大时,TCP就显得有些力不从心了,我们描述这种具有较大带宽时延乘积的管道,称之为长肥管道。
这个描述实际非常贴切。如果想要提高带宽时延乘积,可以水平拉长也就是RTT扩大,可以是垂直拉高也就是带宽增大,或者是两者同时拉伸。在这种情况下TCP会遇到很多问题
- TCP首部中的描述报文大小的字段是16bit,这也就说窗口最大的限制是65535。在长肥管道上,现有的窗口大小无法把管道充满,换句话说也就是资源利用的不充分。
为了解决这个问题,TCP新增了窗口扩大选项:允许TCP把最大窗口大小拓展到32 bit来描述。当然,这并没有修改TCP首部的内容,因为我们必须考虑到和老版本的兼容性。TCP的做法是在选项当中使用一个字节来描述扩大因子x,范围是0 - 14。最大窗口会在这个基础上扩大,2 ^ 16 * 2 ^ x。
这个参数的约定是在三次握手的过程,前提是双方都必须支持这个选项。如果接收端不支持,那么发送端会将这个字节设为0用以兼容旧的实现。
需要注意的是发送双方的窗口扩大因子可以不一致。
长肥管道上的多分组丢失,现有的算法仍然效率不高。
现有的RTT计算方式对于长肥管道并不合适。
考虑这样一个简单的情况:RTT是对一个数据信号(包含数据的报文段)以较低的频率(每个窗口一次)进行采样的,假设窗口的大小为x个报文段大小,那么采样速率也就是1/x。当窗口较小时采样速率是可以接收的,但在长肥管道上x变大,这个时候采样速率就会变得很小,RTT相对会不太精准。
对于依赖RTT来进行判断的重传策略,一个不精确的RTT是不可接受的。错误的拥塞判断会导致传输速率大大降低
- 长肥管道上很容易出现
Sequence Number
环回。
Sequence Number
的环回问题在原先的网络上几乎不可能出现。但在长肥管道上由于带宽时延乘积较大,并且可能使用了窗口扩大选项,使得Sequence Number
的环回速度加快,一个MSL内很容易发生环回。
为了解决这个问题,TCP引入了时间戳选项。发送端会在每一个发出的报文当中放入一个时间戳,而接收方在ack里会将这个时间戳返回。需要注意的是虽然时间戳是在两端时间传递,但实际只有发送端会据此进行比对和判断,接收端只是简单的回显,所以这个时间戳的值不需要两端同步校验。
对于处理Sequence Number
环回的问题,时间戳实际就是一个单向递增的数字,作为Sequence Number
的拓展,辅助判断每个报文的排列顺序。这就是PAWS算法。
时间戳选项的引入实际还可以帮助我们进行更准确的RTT计算,发送端可以根据每一个ack来进行RTT的计算,而不是窗口大小。
依赖每一个ack而不是每个报文的原因是,虽然发送的报文都携带有时间戳,但接收端的可能会合并多个报文段的确认。
TCP会维持两个状态变量用于时间戳的处理:tsrecent和lastack,分别记录下一个ack将要发送的时间戳和最后发送的ack中的确认序号。
这基本是书中翻译的原话,非常的含糊!因为书中并没有说明具体是谁来维持这两个变量,对于TCP而言是有发送端和接收端两个对象的,计算RTT是发送端的需求,所以很容易错误的理解成这两个变量由发送端维持。实际这部分是由接收端负责,解决的问题是在确认多个报文的ack中如何设置时间戳的问题。
试想,接收端收到了报文段1(1 - 1024)和报文段2(1025 - 2048)之后,用一个ack进行确认,laskack非常容易理解,确认序号应该是2048,但时间戳应该选择哪一个报文?
选择报文段1和报文段2时间戳的不同在于,是否包含了ack捎带这一部分的时间。很显然,对于RTT的计算ack捎带这部分的延迟时间是应该考虑的,所以返回的ack应当选择报文段1的时间戳。这个时间戳由变量tsrecent保存。
当发送端收到ack之后,可以取出时间戳和本地时间比对,计算RTT时间。因为时间戳都是以发送端为标准的,所以无需额外的处理。
和窗口扩大因子选项类似,这些新增的选项都需要在握手过程中互相确认,是否支持。这是为了兼容旧版本所必须的操作。
BBR算法
这部分并未在《TCP/IP 卷一》中提及,但在提及TCP的现在和未来时是一个不可绕开的话题。作为新的拥塞避免控制算法,BBRs算法摈弃了经典拥塞避免算法对于拥塞的理解和判断。更多的讨论欢迎移步知乎的讨论Linux Kernel 4.9 中的 BBR 算法与之前的 TCP 拥塞控制相比有什么优势? ,本文不再赘述。