TCP的可靠传输(ARQ、滑动窗口、拥塞控制、流量控制、三次握手和四字挥手)
TCP 连接设有两个窗口:发送窗口和接收窗口。TCP 的可靠传输机制用字节的序号进行控制,它所有的确认都是基于序号而不是基于报文段。TCP协议通过使用连续ARQ协议和滑动窗口协议,来保证数据传输的正确性,从而提供可靠的传输。
1 ARQ协议
ARQ协议(Automatic Repeat-reQuest,自动重传请求)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时重传这两个机制,在不可靠服务的基础上实现可靠的信息传输。ARQ包括停止等待ARQ协议和连续ARQ协议,拥有错误检测(Error Detection)、正面确认(Positive Acknowledgment)、超时重传(Retransmission after Timeout)和 负面确认及重传(Negative Acknowledgment and Retransmission)等机制。
1.1 停止等待ARQ协议
数据包在网络上传输,存在2类可能。其一,在无差错的理想情况下,发送方传输数据,接收方发送确认,发送方收到确认后进行下轮数据传输实现可靠传输;
其二,因为拜占庭问题会导致发送方和确认发都没法保持可靠传输,那么确认和重传就能保存可靠传输。
如果客户端发送的包一段时间没有收到接收方的响应,则进行超时重传,这里又有2中情况:
- 服务端收到了客户端的分组包,发送的确认包丢失了导致客户端没有收到。(服务端收到客户端的重发包,直接丢弃,重传确认包)
- 服务端没有收到客户端的数据包 。(客户端因超时重新发送分组包)
超时重传:在可靠的传输协议下,如果发送方只要超过了一段时间仍然没有收到确认,就认为刚才发送的分组丢失了,它会重传刚刚的发送过的分组。 超时重传的原理也很简单:发送方发送完一个分组后,就会设置一个超时计时器,如果超时计时器到期之前没有收到接收方发来的确认信息,则会重发刚发送过的分组;如果收到确认信息,则撤销该超时计时器。
超时重传考虑的三点:
- 既然发送方发送的分组可能丢失或者有差错,可能需要重传,那么它必须暂时保留已发送的分组副本,只有收到确认后,才清除这个副本。
- 分组和确认分组信息都应该有各自的编号,用来标示每一个分组和确认信息。(这样才知道需要发送哪个分组,收到了哪个分组的确认信息)
- 超时计时器设置的时间应该略长于分组传送往返时间。
默认情况下TCP采取的是累积确认机制,这时如果发生了报文乱序到达,接收方只会重复确认最后一个按序到达的报文段,为此发送方的处理只能是重复按序到达接收方的报文段之后的那个报文段,因而它无法准确知道哪些报文段到达了,哪些没有到达。SACK是TCP选项,它使得接收方能告诉发送方哪些报文段丢失,哪些报文段重传了,哪些报文段已经提前收到等信息。根据这些信息TCP就可以只重传哪些真正丢失的报文段。需要注意的是只有收到失序的分组时才会可能会发送SACK,TCP的ACK还是建立在累积确认的基础上的。也就是说如果收到的报文段与期望收到的报文段的序号相同就会发送累积的ACK,SACK只是针对失序到达的报文段的。
SACK包括了两个TCP选项,一个选项用于标识是否支持SACK,是在TCP连接建立时时发送;另一种选项则包含了具体的SACK信息。SACK(Selective Acknowledgment)
选择性确认重传,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。
选项字段
- PTR(Pointer Record):指针记录,PTR记录解析IP地址到域名
- TTL(Time to live):存活时间,限制数据包在网络中存在的时间,防止数据包不断的在IP互联网络上循环,初始值一般为64,每经过一个路由减去1。
- 通过TTL过滤运营商劫持包,假的包是抢先应答的,所以和真实包的TTL可能不同(例如ip.ttl == 54)
- Seq:数据段的序号,当接收端收到乱序的包,就能根据此序号重新排序,当前Seq等上一个Seq号与长度相加获取到
- Len:数据段的长度,这个长度不包括TCP头
- Ack:确认号,接收方向发送方确认已经收到了哪些字节
- RTT(Round Trip Time):也就是一个数据包从发出去到回来的时间
- RTO(Retransmission TimeOut):超时重传计数器,描述数据包从发送到失效的时间间隔,是判断数据包丢失与否及网络是否拥塞的重要参数
- MTU(Maximum Transmit Unit):最大传输单元
- MSS(Maximum Segment Size):最长报文段,TCP包所能携带的最大数据量,不包含TCP头和Option。一般为MTU值减去IPv4头部(至少20字节)和TCP头部(至少20字节)得到。
- Win(Window Size):声明自己的接收窗口
- TCP Window Scale:窗口扩张,放在TCP头之外的Option,向对方声明一个shift count,作为2的指数,再乘以TCP定义的接收窗口,得到真正的TCP窗口
- DF(Don't fragment):在网络层中,如果带了就丢弃没带就分片
- MF(More fragments):0表示最后一个分片,1表示不是最后一片
过滤表达式
- 握手请求被对方拒绝:tcp.flags.reset === 1 && tcp.seq === 1
- 重传的握手请求:tcp.flags.syn === 1 && tcp.analysis.retransmission
- 过滤延迟确认:tcp.analysis.ack_rtt > 0.2 and tcp.len == 0
上面的停止等待ARQ协议的优点是简单,但也有很严重的确定,就是信道利用率太低
信道利用率:
1.2. 连续ARQ协议
由于停止等待ARQ协议会连续发送一组数据包,然后再等待这些数据包的ACK。发送方连续发送多个分组,不必每发完一个分组就停下来等待对方确认。如下图所示:
连续ARQ协议通常是结合滑动窗口协议来使用的,发送方需要维持一个发送窗口,如下图所示
图(a)是发送方维持的发送窗口,它的意义是:位于发送窗口内的5个分组都可以连续发送出去,而不需要等待对方的确认,这样就提高了信道利用率。
连续ARQ协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。例如上面的图(b),当发送方收到第一个分组的确认,就把发送窗口向前移动一个分组的位置。如果原来已经发送了前5个分组,则现在可以发送窗口内的第6个分组。
接收方一般都是采用累积确认的方式。也就是说接收方不必对收到的分组逐个发送确认。而是在收到几个分组后,对按序到达的最后一个分组发送确认。如果收到了这个分组确认信息,则表示到这个分组为止的所有分组都已经正确接收到了。
累积确认的优点是容易实现,即使确认丢失也不必重传。但缺点是,不能正确的向发送方反映出接收方已经正确收到的所以分组的信息。比如发送方发送了前5个分组,而中间的第3个分组丢失了,这时候接收方只能对前2个发出确认。而不知道后面3个分组的下落,因此只能把后面的3个分组都重传一次,这种机制叫Go-back-N(回退N),表示需要再退回来重传已发送过的N个分组。
2. 滑动窗口协议
滑动窗口协议在在发送方和接收方之间各自维持一个滑动窗口,发送发是发送窗口,接收方是接收窗口,而且这个窗口是随着时间变化可以向前滑动的。它允许发送方发送多个分组而不需等待确认。TCP的滑动窗口是以字节为单位的。
- 发送窗口:已发送并收到确认的数据(不在发送窗口和发送缓冲区之内)、已发送但未收到确认的数据(位于发送窗口之内)、允许发送但尚未发送的数据(位于发送窗口之内)、发送窗口之外的缓冲区内暂时不允许发送的数据。
- 接收窗口:已发送确认并交付主机的数据(不在接收窗口和接收缓冲区之内)、未按序收到的数据(位于接收窗口之内)、允许的数据(位于接收窗口之内)、不允许接收的数据(位于发送窗口之内)。
规则:
- 凡是已经发送过的数据,在未收到确认之前,都必须暂时保留,以便在超时重传时使用。
- 只有当发送方收到了接收方的确认报文段时,发送方窗口才可以向前滑动几个序号。
- 当发送方A发送的数据经过一段时间没有收到确认(由超时计时器控制),就要使用回退N步协议,回到最后接收到确认号的地方,重新发送这部分数据。
3 TCP拥塞控制
3.1 TCP拥塞控制
在某段时间,若对网络中的某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变化,这种情况叫做拥塞。
3.1.1 拥塞控制设计
拥塞控制是一个动态的问题,从控制理论的角度来看拥塞控制这个问题,可以分为开环控制和闭环控制两种方法。开环控制就是在设计网络时事先将有关拥塞发生的所有因素考虑周到,一旦系统运行起来就不能在中途改正。闭环控制是基于反馈环路的概念,包括如下措施:监测网路系统以便检测拥塞在何时何地发生、把拥塞发生的信息传送到可采取行动的地方、调整网络系统的行动以解决出现的问题。
3.1.2 拥塞控制方法
因特网建议标准RFC2581定义了进行拥塞控制的四种算法,即慢开始(Slow-start),拥塞避免(Congestion Avoidance),快重传(Fast Restrangsmit)和快恢复(Fast Recovery)。
我们假定:1. 数据是单方向传送,而另外一个方向只传送确认;2. 接收方总是有足够大的缓存空间,因为发送窗口的大小由网络的拥塞程度来决定。
拥塞窗口概念
发送报文段速率的确定,既要根据接收端的接收能力,又要从全局考虑不要使网络发生拥塞,这由接收窗口和拥塞窗口两个状态量确定。接收端窗口(Reciver Window)又称通知窗口(Advertised Window),是接收端根据目前的接收缓存大小所许诺的最新窗口值,是来自接收端的流量控制。拥塞窗口cwnd(Congestion Window)是发送端根据自己估计的网络拥塞程度而设置的窗口值,是来自发送端的流量控制。
常用方法
a. 慢启动和拥塞控制
当主机开始发送数据时,如果立即将较大的发送窗口的全部数据字节都注入到网络中,那么由于不清楚网络的情况,有可能引其网络拥塞。比较好的方法是试探一下,即从小到达逐渐增大发送端的拥塞控制窗口数值,通常在刚刚开始发送报文段时可先将拥塞窗口cwnd(拥塞窗口)设置为一个最大报文段的MSS的数值。在每收到一个对新报文段确认后,将拥塞窗口增加至多一个MSS的数值,当rwind(接收窗口)足够大的时候,为了防止拥塞窗口cwind的增长引起网络拥塞,还需要另外一个变量,慢开始门限ssthresh。
具体过程:
- TCP连接初始化,将拥塞窗口设置为1
- 执行慢开始算法:cwind按指数规律增长,知道cwind == ssthress开始执行 拥塞避免算法:cwnd按线性规律增长
- 当网络发生拥塞,把ssthresh值更新为拥塞前ssthresh值的一半,cwnd重新设置为1,按照步骤(2)执行。
下图是慢启动和拥塞避免的一个可视化描述。我们以段为单位来显示cwnd和ssthresh,但它们实际上都是以字节为单位进行维护的。
TCP Tahoe版本已经废弃。
b. 快重传和快恢复
一条TCP连接有时会因等待重传计时器的超时而空闲较长的时间,慢开始和拥塞避免无法很好的解决这类问题,因此提出了快重传和快恢复的拥塞控制方法。
快重传算法并非取消了重传机制,只是在某些情况下更早的重传丢失的报文段(如果当发送端接收到三个重复的确认ACK时,则断定分组丢失,立即重传丢失的报文段,而不必等待重传计时器超时)。
例如:M1,M2,M3 -----> M1,M3,缺失M2,则接收方向发送方持续发送M2重复确认,当发送方收到M2的三次重复确认,则认为M2报文丢失,启动快重传机制,重传数据,其他数据发送数据放入队列,待快重传结束后再正常传输。
快恢复算法有以下两个要点:
- 当发送方连续收到接收方发来的三个重复确认时,就执行“乘法减小”算法,把慢开始门限减半,这是为了预防网络发生拥塞。
- 由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是把cwnd(拥塞窗口)值设置为慢开始门限减半后的值,然后开始执行拥塞避免算法,使拥塞窗口的线性增大。
4. 流量控制
所谓流量控制就是让发送发送速率不要过快,让接收方来得及接收。
解法:利用滑动窗口机制就可以实施流量控制。
原理:运用TCP报文段中的窗口大小字段来控制,发送方的发送窗口不可以大于接收方发回的窗口大小。
考虑一种特殊的情况,就是接收方若没有缓存足够使用,就会发送零窗口大小的报文,此时发送放将发送窗口设置为0,停止发送数据。之后接收方有足够的缓存,发送了非零窗口大小的报文,但是这个报文在中途丢失的,那么发送方的发送窗口就一直为零导致死锁。
解决这个问题,TCP为每一个连接设置一个持续计时器(persistence timer)。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小(注意:TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段)。
TCP利用滑动窗口协议来进行流量控制
5. TCP三次握手和四次挥手的过程
5.1 三次握手
- 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
- 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。
5.2 四次分手
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次分手”。
- 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
- 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
- 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
- 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
6. 深入讨论:
6.1. 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
建立连接时,ACK和SYN可以放在一个报文里来发送。而关闭连接时,被动关闭方可能还需要发送一些数据后,再发送FIN报文表示同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
为什么要三次握手
在谢希仁著《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。在另一部经典的《计算机网络》一书中讲“三次握手”的目的是为了解决“网络中存在延迟的重复分组”的问题。
在谢希仁著《计算机网络》书中同时举了一个例子,如下:
“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
这就很明白了,防止了服务器端的一直等待而浪费资源。
为什么要四次分手
那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。如果要正确的理解四次分手的原理,就需要了解四次分手过程中的状态变化。
- FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
- CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
- LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
- TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
- CLOSED: 表示连接中断。
6.2 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
两个存在的理由:1、无法保证最后发送的ACK报文会一定被对方收到,所以需要重发可能丢失的ACK报文。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。2、关闭链接一段时间后可能会在相同的IP地址和端口建立新的连接,为了防止旧连接的重复分组在新连接已经终止后再现。2MSL足以让分组最多存活msl秒被丢弃。
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
6.3 为什么必须是三次握手,不能用两次握手进行连接?
记住服务器的资源宝贵不能浪费! 如果在断开连接后,第一次握手请求连接的包才到会使服务器打开连接,占用资源而且容易被恶意攻击!防止攻击的方法,缩短服务器等待时间。两次握手容易死锁。如果服务器的应答分组在传输中丢失,将不知道S建立什么样的***,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。