关于TCP协议的介绍的相关书籍已经太多太多,而且在搜索引擎中输入TCP关键字,也会有数以千计的结构展现你面前。但是笔者觉得没有经历过自己整理的东西永远都是别人的,虽然我们整理时可能只是把别人非常完善东西搬过来罢了,但是你要记住一句话:我们要站在巨人的肩膀上眺望远方。写这篇博客目的是让自己加深对TCP协议的理解,也便于自己日后再复习。
TCP协议全称传输控制协议,是传输层中设计非常复杂的一个协议,这里介绍以下TCP的主要特点,然后我们后面的详细介绍就以这些特点展开:
我们这里介绍了TCP的报头,没错,让人感觉到枯燥无味,但是这却是TCP各种复杂机制实现的基础,对于TCP的报头信息读者们一定要做到烂熟于心,最好能自己画出来,因为当你看到某一个选项时,你就会想起TCP的某个机制。
我们这里将停止等待协议和超时重传协议放在一起介绍,因为他们本身都是非常简单的机制,并且放在一起我们通过不同的场景能让大家更好的理解,这里我们要清楚的是停止等待协议其实是一种确认应该的机制:就是说,发送端发送一个数据后就停止发送进行等待,直到收到接收端的确认才发送下一个数据,如下图。虽然TCP协议是全双工的,但是这里我们只讨论单个方向上的数据传输,这样能帮助我们加快理解且不那么复杂:
下图是未出现差错的情况下两台主机进行数据传输的场景,纵轴是时间线,可以看出只要在收到上一个数据的应答发送方才会发送下一个数据,这就是我们所说的确认应答机制。
上图中我们是在一种理想化的情况下讨论的,但是我们都知道由于网络情况的不相同,数据在发送的过程中有可能丢失也有可能出现错误,如果出现检验错误,那么接收端也会直接丢弃这个数据并不给予发送端应答,那么为了实现可靠传输我们就要引入我们的超时重传机制。
如下图,不管是由于数据错误还是丢失,发送端都没有接收到ACK确认,如果发送端过了一段一段时间没有收到应答,那么就会重传数据,这种机制叫做超时重传机制。没发送一个数据报就会设置一个超时计时器,如果在计时器过期之前收到应答,那么就撤销已经设置的计时器。
所以因为超时重传的机制我们应该注意以下几点,发送端发送完一个数据之后并不可以了立即将这个数据从缓冲区中删除,因为可能后面要进行超时重传。每个数据使用序号编号是必要的,因为这样你才能知道你需要重传的数据是哪个。
这里除了数据丢失,还会出现应答丢失的场景,而且有时候应答并不是丢了,而是阻塞在网络中迟到了,那么TCP又是怎么处理的呢?
我们使用上面的俩个简单协议就实现了在不可靠网络上的可靠传输,上述的这种可靠传输协议称为自动重传协议ARQ,意思是说重传的请求是自动进行的,接收方不需要主动请求发送端重传某个出错的分组。
我们上述的俩种协议中提到了超时重传计时器,很多同非常好奇,那么网络情况这样的变化莫测,如果将时间设置的太长会导致网络空闲的时间增大,降低传输效率。但是如果设置的时间太短有可能产生很多不必要的重传,使网络负载增大。那么这个超时计时器的时间应该设置为多久呢?一起来看看
我们讲TCP头部时间戳选项时提到了一个RTT的概念,他指的是报段传输的往返时间,而我们计算超时时间是应当收集不同的RTT并求他们的加权平均RTTS,RTTS也被称为平滑的往返时间,每当我们计算新的RTTS就需要使用到下面的公式:
公式中的a通常取值为1/8,也就是0.125,通过这个公式我们就得到了新的RTTS,但是这还没完。我们还需要计算一个RTTD,他指的是RTT和RTTS差值的加权平均值,第一次RTTD取RTT的一半,之后的RTTD按如下公式计算:
这里的b取值为1/4,也就是0.25,我们现在得到了RTTD和RTTS后,最后一起来看看超时计时器时间RTO的公式:
但是这里有一个很大问题:因为由于存在重传机制,你就无法判断同一个报文的应答是否是经过重传的,如果经过重传的报文那么就一定会导致RTTS变大,所以超时时间RTO也就会变大,相反,如果把一个未经过重传的报文当作重传的报文的计算,这样又会导致RTO变小,那么这个问题怎么解决呢?
TCP规定,在计算平均RTTS时,只要报文重传了,就不在把它列为计算样本。可是这又导致新的问题,如果某个时间点报文量突然增大,会引起重传报文,但是由于规定重传的报文RTT不作为样本,所以RTTS就意味着无法更新,进一步导致重传时间无法更新,所以为了解决问题的问题,名叫Karn算法中规定,超时重传时间是旧重传时间的两倍,直到不在发生重传时,才根据以上公式重新计算时间。事实证明,这种方法是行得通的。
现在我们已经介绍了最简单的停止等待协议,但是他有一个很大的问题,那就他的效率问题,每次发出一个数据是经过发送数据TD;数据报往返时间RTT;处理时间TA,用TD除以这三项加起来得到的结果就是信道的利用率。可想而知多么感人,所以这里TCP又引入了滑动窗口协议,这里先不详细谈此协议,后面下一小节我们会仔细刨析他:
为了解决效率,我们引入了滑动窗口,这是TCP协议精华的所在,也是连续ARQ协议最基本的概念。连续ARQ协议规定,发送端每收到一个应答,窗口就会向后一个分组的位置,而此时我们也采用了一种积累确认的方式,对按需的到达的最后一个分组发送确认,但这里我们先不详细谈滑动窗口协议,先来解决选项字段的一个遗留问题。
还记得我们的选项中的SACK字段么,SACK选项需要使用一个字节指明,还需要另一个字节指明选项占多少字节,这个字段是客户端和服务器建立连接时协商的一个字段。他解决的问题如下:
如果上图中的2,3,4,5正在送达的路上,但是只收到了3,5,说明4丢失了,这时候由于只能向发送端确定3以前的数据收到了,而发送端并不知道4以后的下落,所以导致4以后的数据都要重新发送,这可能会使网络负载变高,这种问题叫做回退。而我们SACK可以解决这个问题,我们只需要标明确实数据的左边界和右边界返回给发送端,让他仅仅传送丢失的数据即可。
但是,but因为一个边界就占用4个字节,两个边界为8字节,而选项部分一共为40字节,这就可以看出SACK并不是一种足够优的做法,并且SACK文档没有指明发送方怎么响应SACK,所以大多数情况下并不会使用SACK这一选项。
上面我们已经提到过滑动窗口是一种使用连续ARQ协议的机制,这种机制会维护一个发送窗口,窗口中所有的数据可以连续的发送数据而不需要等待对方的确认,上面第一小结中向右的方向是时间轴,每收到新的确认滑动窗口即可向右滑动。
窗口大小的确定:
首先我们要纠正一下很多同学错误的观点:滑动窗口的大小不变。这里的理解是非常有问题的,就拿发送窗口的大小来说,一般发送窗口的大小要根据接收方接收缓冲区剩余空间大小来定的,如果接收端的上层并不立即取走数据,那么随着发送方数据的不停发送,接收方缓冲慢慢被沾满,发送窗口的大小也就将会趋向于0。
数据发送中窗口的变化:
这里非常有必要阐明发送窗口在发送缓存的形态,如上图的发送窗口所示:
现在来观察上图中的发送和接收窗口,接收窗口中序号为32,33的数据已经收到,但是却没有收到31。由于发送方没有收到31号数据的确认,所以又要重新从31开始发送,这就是我们之前在连续ARQ协议提到的回退现象。这样就使我们在出现丢包的情况下发送效率大大降低,所以就可以使用SACK选项只发送丢失的数据包。当接收端收到31,32,33数据之后向发送端发送确认。
此时发送端的p1指针向前移动到34,但是仔细观察图p2的位置并没有发生变化,接收端的窗口通知大小也没有发生变化,所以p3向前移动到54。注意我们的可用窗口已经变大了,这里我们说的并不是整个发送窗口变大,而只是可用窗口那一部分。
再来观察接收端当前收到了37,38,40号数据,之前的数据并没有按序到达,但是从图中明显我们可以观察到41及之前的数据发送端已经全部发送,如果在一定时间内发送端没有收到确认那么就会触发超时重传机制,从此处我们就会发现TCP所有的机制共同保证了可靠传输这一特点。如果发送端已经将可用窗口的数据全部发送完,那么此时可用窗口就会为0:
到这里实际上我们就要更正上面说提到一个不严谨的点,如果我们从本质上来看,现在设计一个发送窗口俩个下标就不能满足了。因为我们要加一根指针区分可用窗口和已发送未收到确定的部分。
再谈窗口和缓存:
我们先来看看窗口和缓存的大体逻辑结构是什么样子的:
这里要说明俩点,第一点:虽然我们的示意图画的是一个长条状,但是因为缓存有大小限制的缘故,所以我们必须把缓存当作循环队列使用。第二点:图中为什么有一个发送程序?这是什么意思,其实我们之前写socket套接字时使用过write,read这些相关的函数,这些函数向socket对象写数据时是直接发给对方吗?不是的,其实这些接口说白了只负责将buf中的数据拷贝到此缓存中就返回了,其余的锅他便甩给TCP,这也就是图中程序存在的原因。
最后对于滑动窗口我们还需要做出以下几点的总结:
我们上一节中已经越过TCP协议中实现可靠传输的一座大山,而现在我们要来谈一谈TCP的流量控制。有的同学一看到流量控制会认为TCP设置这种机制是解决效率问题的,这么理解有问题,但不完全对。那你不控制流量一次将数据发完不是更快么?所以其实流量控制是防止发送端发送太快而导致接收端来不及接收所增加的一种机制,试想如果没有流量控制,那么接收端来不及接收就会将来不及接收的数据包丢弃。从可靠性角度来说,避免丢包就是一种可靠性的体现。但从效率角度来说也没有错,因为丢包后发送方就要重发,重发就占用了网络资源,而如果流量控制就能避免重发使其他数据更快到达,这也侧面解决了效率问题。
流量控制就是为了让发送方的发送速率不要太快,要让接收方来得及接收。 那么来看看TCP如何实现流量控制:
从图中我们能很清楚的观察到B主机也就是接收端向发送端A一共发送了3次确认,只有ACK为1时确认才有效。这三次确认每一次都对发送端进行了流量控制,我们一直在谈TCP头部窗口的选项,也就是图中rwnd。这个选项填的是自己当前接收缓冲区的大小,目的就是为了告诉发送方我现在接收缓冲还剩多大,你不能发送比窗口还大的数据。窗口的单位是字节。
可以看出,流量控制非常简单,仅仅使用TCP报头中的窗口选项就能实现流量控制。但是在控制的过程中还会遇到一些其他情况,假设某个情况下接收方的rwnd的大小为0,也就意味着发送方不能再发送数据了。此时进入发送暂停状态,这种状态直到接收方发送一个新的窗口值为止,但是不幸的是,这个响应可能在传输的过程中丢了,那么窗口现在虽然已经不是0了,但是对端直间仍然处于僵持状态。
为了解决这个问题,TCP为每个连接设有一个持续计时器。只要接收方发送了窗口为0的报文,那么发送方就启动这个计时器,当这个计时器过期,发送方就会主动向接收方发送一个探测报文,这个报文仅携带一个字节的数据。接收方收到这个报文就会向发送方发送当前窗口的大小。如果接收端的窗口不为0,那么僵局也就被打破。
TCP协议中使用窗口进行控制,不仅是一种简单的,并且可靠的流量控制方式。
拥塞现象是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。
TCP拥塞控制,我们要面对的另一座大山,从上面对于TCP拥塞控制一段简单的话可以看出,此时对于TCP拥塞控制解决的问题不在是点对点的问题,而是要解决整个网络状况的问题。那么网络的拥塞是怎么发生的呢:对网络中某一资源的的需求超过了该资源可提供的能力,也就是说对资源的需求大于可用的资源时就会发生拥塞,整个网络的性能也就会随着下降。
有的人说,解决拥塞问题为什么要TCP去控制,直接把网络中的资源变多不就行了么。比如将路由器中的节点缓存容量变大,这样就能缓存更多的资源。但是你想没想过虽然节点缓存变大了,但是路由器处理数据包的速度并未发生变化,所以大量的数据就要进行排队,这时超时重传就会发生,导致的结果是节点排队的数据越来越来,直到路由器再也处理不过来就发生了死锁。又有人说将路由器处理效率提高不久可以了,这确实是一个办法,但是即便你提高了处理速度,此时网络传输的瓶颈又会被转移到其他部分,这有点像木桶原理,决定事物好坏的上限永远和事物的短板紧密相连。更何况,计算机也像人一样欲望是无穷的,对于处理速度惊人的计算机来说,怎么才是最好呢?
所以我们这里重在讨论是控制的思想,不管你所拥有的资源有多少,合理的控制才是最关键的。TCP拥塞控制就是防止过多的数据注入到网络中。那么我们此时使用一张图看看注入网络的数据量(也叫做网络负载)和网络的吞吐量的关系:
从图中我们可以看出,在理想状态下未发生拥塞时图形程45度角,也就是说发出的数据接收端总可以收到。但在实际的拥塞控制中当到达某一阶段虽然网络吞吐量还未饱和却已经出现了丢包,这是发生了轻度的拥塞现象。如果不进行拥塞控制,由于重传数据包会使网络加重拥塞,最后到达某一个点时就会发生死锁。
实际上我们要设计一个完美的拥塞控制的方案非常的难,因为拥塞问题是一个动态的问题,他时刻要随着网络的状况进行变化。一般拥塞控制会有两种方式,一种是开环控制,另一种是闭环控制。
开环控制的意思就是说在设计网络之前就已经充分考虑到了网络中所有会出现的情况,并设计一种理想的拥塞控制方案。但是想都不用想,在这个动态的问题面前开环的方式是决对行不通的。
闭环控制的思想则更合理一些,他会检测网络发生拥塞在何处,并将拥塞情况发送到可采取行动的地方,并调整网络系统来解决网络出现的问题。然而这也并不是万全之策,因为总是频繁的调整网络系统会使网络系统变得不稳定。所以我们尽量要选择一种合适的方式来进行拥塞控制,那么一起看看TCP是怎么实现拥塞控制的。
TCP的拥塞控制的方法:
关于TCP的拥塞控制方式我们需要谈到4种算法,他们分别是慢开始算法,拥塞避免算法,快重传算法,快恢复算法。我们这里先来谈一谈慢开始算法个拥塞控制算法,他们俩结合起来就构成了我们最简单的拥塞控制机制。
这里还要强调的是,我们下面讨论的拥塞避免算法是基于窗口的拥塞控制,这又出现了一个新的概念叫做拥塞窗口,拥塞窗口的大小取决于网络的拥塞程度,网络未出现拥塞,此窗口则可以变得更大。相反,发生拥塞时此窗口就需要减小。这里我们可以先简单的理解为拥塞窗口的大小等于发送窗口的大小。
这里提到的第一种慢开始算法就是基于拥塞窗口实现的,慢开始算法的思想是这样的:既然我现在并不知道网络状况的是什么样子的,那我就开始先发少量的数据包,如果对方能够收到我们则发送更多的数据包。也就是将我们的发送窗口逐渐增大(这里其实是将我们的拥塞窗口增大),当发生拥塞后我们在将拥塞窗口减小。现在我们来看看拥塞窗口增大的规律是怎么样的。
从上图中我们可以看出,cwnd也就是拥塞窗口的大小从1~2, 2~4, 4~8进行指数级别的变化,每一轮次拥塞窗口都变为上次的2倍,但是我们发现如果拥塞窗口的大小按照指数级别增长的话那么很快拥塞窗口大的你就无法想象了举个很简单的栗子 2^32应该对于程序猿来说并不陌生,你想想假如按这种速度增长,32次之后这个窗口会变为多大。
暂时先不谈合不合理,有的同学认为这名字起的就有问题,这么快的增长速度为什么要叫做慢开始算法。其实这里慢开始算法指的并不是阻塞窗口增长的速率慢,而是TCP开始时先设置cwnd为1,探测以一下网络状态,这比第一次就发送一批数据要慢的多。
这里有一点非常重要,虽然上面图好像是每一个轮次之后才让窗口增大2倍但是实际TCP在设计时只要发送方接到一个新报文的确定就会让cwnd增大一。啊~,更本质的说,其实这里的窗口大小也不是增加一,他在底层肯定是一段长度,窗口增大其实就是给这段长度增大一个MSS+报头长的长度。这还不算完,初始的cwnd并非一定从1开始,这里只是为了更好的说明问题,TCP协议中规定初始cwnd大小不能超过4倍的MSS+TCP报头长度,也就是不能超过4倍的SMSS(最大报文长度),根据报文长度设置窗口大小为 2~4,有兴趣的同学可以参考一下计算机网络这本书。
我们上面已经明确的表明如果窗口大小一直按照指数级别增长那么反而会加重网络的拥塞,所以我们这里又要记住一个新名词:慢开始门禁,也叫慢开始阈值ssthresh,什么意思呢,就是当你的cwnd大于等于这个阈值之后就要执行我们接下来的下一种算法:拥塞避免算法。
拥塞避免算法其实本质上就是一种"加法增大"思想的算法,也就是说,当cwnd等于慢开始门禁之后就不再进行指数级别的增长,而是随后每一次只将cend增大1,但是也不要被他的名字所迷惑,拥塞避免不意味着真的可以避免拥塞,只是使网络比较不容易出现拥塞,另一个方面这种加法的思想也可能大体的计算出网络饱和时的cwnd大小。
我们这里将慢开始门禁的阈值设置为16,如下图中所示,当到cwnd到16时,我们便开始执行拥塞避免算法,当到24时网络出现了拥塞,注意注意。当出现拥塞后我们的阈值会减半为拥塞时窗口的一般,我们的cwnd又会重新从1开始指数增长重复我们上面的动作。这种阈值减半的思想也叫乘法减小思想。
然而聊了这么久,我们一直不停的在说网络拥塞后怎么怎么样,那么计算机是怎么判断网络拥塞的呢?其实在计算机中出现了超时他就认为网络已经发生了拥塞,这里值的强调的是我们这不考虑数据丢失的问题,因为当前技术已经比较成熟,丢失的概率不足百分之一,所以丢失的特例我们就不给予讨论了。
那既然发生了超时重传我们就会重新将cwnd设置为1,然后又开始执行之前的动作,那假如某段网络可能真的是线路的问题出现了数据包的频繁丢失,而事实网络并没有阻塞,这种错误的判断让网络的利用率大大降低。为了解决这个问题,我们的TCP协议在拥塞控制中又引入俩种新的机制,分别是快重传算法和快恢复算法。
先来聊一聊快重传算法,这种算法按照笔者的理解来看,他其实即使是不使用在拥塞控制这一块,也从很大的程度上解决了我们数据包重传效率的一种算法,之前我们的数据包重传都是使用超时重传算法,所以也叫自动ARQ。但是快重传不一样当一个数据丢失后他接下来的三个确认会重复确认丢失的那个数据,发送方接收到这三个连续的应答后就会立即重传此报文。那么有的同学这里会有疑问,有了快重传还要超时做什么?其实超时重传是重传机制最后的底线,如果真的发生了网络拥塞,那么这三个连续的应答可能也就丢失了,当没有收到这些应答时,发送端就会进行超时重传
那你讲拥塞控制,关我快重传什么事,你这快重传不是用来提高重传的效率的么,和拥塞控制又没关系:
别急,回头看我们的问题,我们的问题是假设没有发生网络拥塞,是数据包丢失了,发送端误认为超时了,所以将窗口重新设置为1,但是加入快重传之后,我们的发送方接收到了这些应答,那既然应答可以接收到,也就证明网络并不拥塞,所以我们就不必直接将cwnd设置为1重新开始,而是执行一种快恢复的算法。
快恢复算法指的是如果发送方收到了来自接收端的3个连续重复应答后并不将cwnd设置为1,而是直接设置为当前慢开始门禁大小的一半然后执行拥塞避免算法,因为既然能收到来自接收端的3个连续重复应答,说明要么是数据丢失,要么是轻度拥塞,没有必要直接将窗口设置为1。下图因为笔者没找到资源,所以用手机拍的照片,看起来怪怪的。
所以现在你必须明白,实际上TCP的拥塞控制有两条路可以走,发生超时重传就执行慢开始算法,如果发生快重传就执行快恢复算法,不同的算法搭配起来能够更高效的对网络进行拥塞控制:
现在我们在处理之前的一个遗留问题,之前我们说发送的窗口的大小不仅要根据接收窗口大小来设定,还要根据拥塞窗口的大小来设置,我们此时也就能明白发送窗口其实是选取接收窗口和拥塞窗口中的最小值来设定的。rwnd和cwnd控制了发送方的发送速率。
自此我们的拥塞控制也就讲解完了,这里其实思想很简单,大家只要读懂意思相信对拥塞控制会有更生一步的理解。
我们翻越了几座大山之后,现在就只剩几个TCP简单的特性,我们这里简单的一笔带过就好,并不是什么难的问题:
这些就是TCP其余的特点,但是我们关于TCP还有一个经典到不能再经典的问题,TCP3次握手和4次挥手,也就是TCP的运输连接管理,本篇博客我们就不介绍了,而是把他单独拿出来,一是这个问题实在太重要剩要我们要单独拿出来讨论;另一方面笔者的这篇文章已经快1.5w字了,再写多会让读者觉得厌烦,所以我们下篇博客总结TCP的运输连接管理。
这篇博客关于知识就不总结了,上面全都是重点,不要错过每一个信息。再次提醒读者们的是,本博客除了第一张图全都是来自谢希仁老师的《计算机网络》第五和第七版,笔者建议大家去读一读第七版,真的读完之后会有让你意想不到的收获和进步。