tcp 状态转化

305 阅读17分钟

TCP状态变迁图

FIN_WAIT_1: 当发起端发送FIN消息后,发起端就进入了FIN_WAIT_1状态,这个状态一直会持续到发起端收到对端的ACK消息。

FIN_WAIT_2: 当发起端收到对端的ACK消息后,进入了FIN_WAIT_2状态,至此表示发起端这边的发送通道全部关闭,注意,仅仅是发送通道关闭,读取通道依然开启,因为此时对端并没有发出结束请求,不能贸然关闭读取通道。

TIME_WAIT: 当发起端收到接收端发起的FIN请求后,发起端就会进入一个等待状态,同时回复对方ACK消息表示自己已经接收到对端的FIN请求。为什么要有这么一个状态,可以这样想一下,在接收端发送“FIN”,消息的时候,接收端所能知道只是前面他想要发出的消息都发出去了(在这个阶段应该都是ACK消息,因为发起端的发送窗口已经关闭了,这个时候主动向发起端发消息,发起端是不会有任何反应的并且会报错,这个在下一篇会详细说明所有的出错情况怎么样),至于对方有没有收到,接收端并不能知道。所以这个时候发起端采取了一个最简单的方法,等待一段时间以保证所有消息都接收到,那么这个TIME_WAIT时间多长呢?多长才能确认信道中已经不可能再存在有还没有发送过来的消息呢? 在TCP中定义了一个概念叫做MSL,Maximum Segment Lifttime,表示一个包在网络中能生存的最大时间 ,在TCP标准中,定义是2分钟,linux下一般是定义为30秒。有了这个概念之后,很简单的就能得到TIME_WAIT的值应该是多少,从发送端发送到接收端最大可允许的时间是MSL,从接收端发回来的ACK最大允许时间也是MSL,所以如果经历了2*MSL的时间就可以确定网络中不会有还没有到达的包了,因为在这个时间之前一次往返的包应该都消失了。

CLOSED: 在这一切结束后,发起端就可以放心的关闭读和写两个通道完成关闭了。

在接收端,其状态和状态之间的转换如下:

CLOSE_WAIT:接收端在接收到对端的FIN请求后,发送ACK的同时进入CLOSE_WAIT状态,这个状态就是等待自己这一端的上层在一切完毕的时候发送结束请求。

LASK_ACK:表示接收端最后一个ACK已经发送,也就是最后一条需要发送的消息已经发送。

CLOSED: 在这一切结束后,接收端就可以放心的关闭读和写两个通道完成关闭了。

状态机图主要有两个部分组成,框框表示某种状态,框框之间用线条连接,标识状态的变迁,而在这些线条之上往往会写着一些说明,这些说明被誉为激励条件,一个状态只有经过了某种条件的刺激或者激励才能变迁为另一个状态。打个比方吧,比如你在发呆,你处于空闲状态,这个时候我打你一下,给你一个刺激,你就会从空闲状态变迁为疼痛状态了。

所有的状态都从Closed开始,在这个图的叙述中,有两条说明,实现表示客户,虚线表示服务器,准确的来说,这种说法并不严谨,因为在TCP的通信过程中其实没有服务器和客户之分,两边都在同等的发送数据。而在这个图或者一般的表述中,主动发起连接的那一方一般会被称之为客户端,被动接受的那一段是服务器端。在了解了这些先决条件之后,那么就来一起找找这个图中的三次握手在哪里吧。

首先看Closed状态中的实线,也就是表示客户的状态,在右边,主动发送SYN之后,进入SYN_SENT状态,三次握手第一步完成。而在这之后,收到SYN和ACK包,然后发送ACK之后客户这边就进入了连接状态。

然后再看看另一条路,从Closed状态虚线进入listen状态,这里不需要任何激励条件就能完成,因为默认服务器一直在监听客户发来的请求,不然那你这个图就画不下去了。在收到SYN,发送SYN和ACK之后进入SYN_RCVD状态,也就是三次握手的第二步。然后就是继续收到对端的ACK之后,进入连接状态。

每个状态机图都能很方便的变成很多独立的小单元,这也是一个状态机图绘画的好与不好的一个标志。好的状态机图状态之间变迁关系清晰,彼此耦合性很小。而差的你就感觉在这种状态变化的指示下只会变成一团混乱。

仔细看一下这个状态机图,你会发现很多前面并没有提及的有趣的状态,他们中很多都是很边缘的情况,但是作为一个严谨的协议设计者,应该最大可能留下最小的漏洞。比如图中你会发现从listen状态可以直接变成SYN_SEND状态,回想一下前面说了服务器端会默认为是listen状态从而可以监听对端来的请求。但是如果这个时候服务器端主动发送了SYN,那么他其实就是我们定义中的“客户”了,所以图中用实线标识这种情况下会SYN_SEND状态。

另外一个在发起连接阶段的时候的特殊情况就是SYN_SEND和SYN_RCVD中间那条线,如果一个客户在进入SYN_SEND同时收到对端发送回来的SYN请求,通俗点说就是两端同时发起了连接,两边都是客户了,这个时候TCP不会就此终止,而是进入SYN_RCVD,然后再经历后面的过程之后,两边都进入了Established状态。

RST状态变迁图

对比上面的图,这张图用红色的部分标识了发送RST的情况,但是也简化了一些上面有的状态变迁,不过这对于了解整个TCP过程中RST的状态变迁没有那么重要了。

从这个图中第一个可以学到的知识就是经历所有RST状态之后,连接都会进入Closed状态,也就是结束。RST的状态基本分成两种,超时和包丢失。

首先看看超时的情况,在图中就三种包重传超时,SYN,FIN和数据包,可以看到超时都是某种“主动的动作”。比如在三次握手的第二个阶段,重传SYN+ACK一直超时,服务器端就只能发送一个RST到对端了。同理在FIN超时和本身传输数据超时之后,RST也会发送到对方。

第二类是丢失,其实本质上上和上一种情况是一直的,在ACK丢失的情况下,在对端看来就是发送包超时,所以在这种情况下主要是接收RST。

那么在现实生活中,什么样的情况会导致超时呢?其实很多情况都可能发生,比如某一端程序突然崩溃,某一端突然没电了等等,这种情况下都会引起超时,从而会导致RST包的发送。如果用官方的语言总结下,RST包会在以下三种情况下被发送:

到达的端口不存在,比如说在客户端发送SYNC想建立连接的时候,对端并没有任何监听端口,那么就会发生超时,从而发送RST包。而在UDP这种没有连接的协议中,一个ICMP端口不可达消息就能解决这个问题(如果你对这些名词很陌生,可以看看前面的文章了)。

连接被异常终止, 比如说应用程序崩溃了。

检测半关闭连接,这个半关闭的概念其实在上一篇的时候有所提及,但是不详细,这里正好说明一下。半关闭连接主要发生在某一段已经关闭或者异常终止但是另外一段并不知道的情况下。比如在通信的过程中,突然拔掉服务器那边的网线,然后再重启服务器和其应用程序。这个时候连接就处于一个半关闭状态,此时客户机再向对方发一个消息,服务器端会回复一个RST消息,本端就知道刚才发生了什么。

说了这么多,言而总之其实RST主要作用有两个,报告本端异常,告知对方错误,记住这两句话就差不多记住了RST包的作用。

滑动窗口

作用 便于管理以及流量控制。接收端通过告知发送端自己的接收能力避免发送端一次性发送过多的包到对面,这样只会造成大量的重传,从而进一步加重网络负担,雪上加霜这个成语可以完美的解释这一现象。这个过程用一张图就可以清晰的表示出来:

假设每次传输的时候都是传输100包的数据,rwnd表示接收端可接受的窗口大小,就是上一节所描述的接收端的蓝色部分。假设在最开始的时候接受窗口是500个包长,那么当其接收到200个包后,在回复的TCP包头中,接收端会填上已经接收到的最后一个包的序号加上我还能接收多少数据包的窗口大小,这样一个智慧的发送端就不会发送比能接受的数据包还要多的数据包。于是就完成了两边同步的过程。

如果发生了丢失会怎么样呢?比如说301到400的数据包在传输中丢失了,那么按照前面一篇已经学习过的逻辑,对端只能确认到最先发生丢失的包,所以虽然401到500的包已经到达对端,但是在重传计时器到期之后,发送端还是得传输301到500的所有的包。这里主要需要注意的是,每次传输完毕,接收端都会相应的更新自己尚能接收的接收窗口大小,发送端是不可以一次性发送比接收窗口更大的数据包的。而在接收窗口空间全部使用完毕的时候,接收窗口会告知对方,自己的接收窗口为0,这个0意味着作为发送端的对方不能再发送任何数据了。

这里自然的会出现一个问题,既然接收端通知发送端窗口大小为0以使得发送端无法发送数据了,那么什么时候发送端才能再一次的发送数据呢?总不能一直这样僵持下去吧,发送端会发出包含很小字节的探测包(通常是一个字节),如果对端此时回复的接收窗口大小大于0了,那么很显然就可以正常的发包了,如果还是0,一般发送端会尝试三次,如果三次还不行,一般发送端就会发送RST包终结这个链接了。

模拟滑动窗口:www.ccs-labs.org/teaching/rn…


前面提过的滑动窗口是TCP实现流量控制的一个主要手段,而接下来的四个概念是TCP实现拥塞控制的主要方法。很多人会分不清流量控制和拥塞控制的区别,两者看起来都和保持信道的通畅的方法,但是对象确是不同的,如何区分这两个概念,其实只要记住一点就可以了。流量控制考虑的是端对端的通畅,而拥塞控制是考虑的全局的通畅。下面就具体介绍下拥塞控制的四大金刚吧。

在介绍具体的概念之前,首先我要提到的是TCP中对于拥塞控制的最高思想,主要有三条,那就是既要尽量快的利用信道的最大能力,不要浪费;又要谨慎的控制信道的负载,不要拥堵;同时如果发生了拥堵,必须能够自我调节,不要崩溃。根据这个最高思想,TCP设计了一系列拥塞避免的算法。

慢启动

在一个网络环境下,信道是共享的,也就是说所有发送的数据都会在这个信道上穿梭。这个道理和高速公路是一样的,你只是高速公路上车流中的一辆车。所以说,当你准备开始发送数据的时候,你不能仅仅做到不管三七二十一,就先按自己的节奏发出自己的数据。如果这个时候信道已经很拥挤,你发出的数据一定也会堵在路上,从而造成进一步的重传,这些重传更加加重了网络的负担,本来拥堵不堪的道路只会越来越差。

所以TCP的设计者们设计了一个保守的策略,这种策略带有浓浓的工科生特点,就是先谨慎的试试,再大胆的前进,实践检验效果。具体的说就是TCP的设计者们采用了一个新的窗口,称之为拥塞窗口,记为cwnd,这个cwnd初始设置为一个one segment的大小,在这里我们简单的理解为TCP报文一次允许发送的最大的大小以方便后面叙述。当发出这个一个报文之后,如果发送端能够收到对应的ACK。那么下一次就会发送2个segment的报文,接着如果还是收到这2个segment的ACK,那么说明这个信道还是可以承受当前大小的包的,那么接着进行试探,发送4个segment的包,收到4ACK,就发送8个segment,以此指数级增长的类推。

简单的说,慢启动就是一句话,大胆尝试,小心求证。而且在慢启动算法中,报文的增长速度是很快的,所以说这个算法的过程并不符合这个算法的名称,之所以称之为慢启动,是他小心的去试探网络情况的哲学上的"慢慢来"。

拥塞避免

上面的慢启动很明显可以在很短的时间内让信道的利用率达到一个比较理想的值,由于前面一直强调过,信道的资源总是有限的,所以到某一个阶段,这些快速增长的报文必定会充满信道使得信道变得开始拥挤。TCP的设计者们为了不要使得慢启动算法变成一个只有理论意义,而在很短的时间内又会造成网络崩溃从而导致本末倒置。所以说在慢启动的基础上,设计者们做了一些现实的改进与妥协。除了cwnd这个概念,设计者们还定义了一个叫做ssthresh的概念,slow start thresh, 中文一般翻译为慢启动阈值,比如65535个字节。如果cwnd的大小达到了这个阈值,那么就不采用指数增长这样的比较暴力的方法,而是采用每次收到一个ACK,cwnd就扩大1个单位的方法,这样比较保守,目的之一就是慢慢而可靠的试出来信道所能承受的最大的负载,符合前面所说的哲学的第一条。

但是信道资源总是有限的,而cwnd在前面描述的过程中无论是怎样的方式,他一直递增的,所以触碰到天花板是必然,按照前面的“三条不要”的最高思想,在遇到信道已经塞满的情况下,需要做的就是快速的减少对信道的负担并且能够快速的恢复。

TCP怎么知道信道拥堵了呢?反过来问,信道拥堵会给发送端带来什么呢?第一个明显的效应就是发送出去的消息收不到ACK,也就是前面说的超时了,如果连续几个消息都没有收到ACK而不停的发生重传,那么就可以判定为信道已经拥堵了。这个时候,拥塞避免算法会进入恢复阶段,其方法很简单:

  • ssthresh设置为当前发生拥塞cwnd的一般,如果cwnd在30个字节的时候(当然不可能只有这么小)发生了拥堵,那么新的ssthresh就设置为15。
  • TCP重新进入慢启动阶段,也就是将cwnd设置为1,指数增长知道达到新的ssthresh的15,然后再重新按照拥塞避免第一个阶段,线性增长。

这个过程可以用图表示:

快速重传

网络是一个很复杂的环境,如果有这么一种情况,网络发生了拥堵但是又那么的拥堵,这种情况的表现是什么呢?按照前面介绍过的滑动窗口,TCP不是one by one的发送数据包的,如果发送的数据包是1,2,3,1和3已经到,但是2没有到,由于拥堵在网络中丢失了,那么接收端会不断告诉发送端下一个需要的报文是2号报文,即使你后面的报文都到了,在2号报文没有收到的情况下,会一直发送对1号报文的ACK,表示需要的是2号报文。如果连续收到三个连续的ACK,就认为网络发生了拥堵。用语言描述有点绕,用图来表示就比较清晰。

这种情况说明了两种情况:网络确实发生了拥堵,但是又没有完全拥堵。因为如果完全拥堵了,那么发送端也不会受到三个ACK数据报文,所以这种情况没有必要从头再来,因为最高思想的第二点让我们最大的利用信道的能力。按照这个,设计者们又改进了上面的算法,提出了一个快速重传的方案,其思想如下:

  • ssthresh设置为cwnd的一半
  • cwnd设置为ssthresh的值
  • 不需要重新进入慢启动阶段而是进入拥塞避免阶段

用图来表示这个过程如下:

快速恢复

快速重传算法已经尽力快的恢复对于网络的传送,但是设计们本着"面包里面抠面粉"的原则,在上面的快速重传算法中尝试想想有没有进步空间,在全面分析之后,提出了快速恢复的算法,其具体做法如下:

  • 在收到3个重复的ACK之后,ssthresh设置为cwnd的一半,然后把cwnd设置为ssthresh加3个单位的大小,接着重传丢失的报文段,如果用前面的例子来举例就是重传2号报文。
  • 如果这个时候再次收到重传的ACK,那么拥塞窗口增加1。
  • 如果收到的是新的数据包的ACK,把cwnd设置为第一步的ssthresh的值。为什么这么做,因为如果收到的新的ACK,说明网络已经恢复了,可以进入拥塞避免的线性增长阶段了。

第一个例子里为什么加3呢,因为这个时候连续的收到3个ACK包,那么可以认为网络还有3个单位大小的余额,同时也可以这么想,说明有3个“老”的数据包已经从网络上离开了。

这就是拥塞控制的四个基本算法,当然在实际中还有很多更加联系实际的算法,用兴趣可以搜索Reno,NewReno之类的关键词,慢慢探索,可以得到很深入的研究。