vlambda博客
学习文章列表

TCP协议保证数据传输可靠性的方式

TCP协议保证数据传输可靠性的方式主要有:

校验和
序列号
确认应答
超时重传
连接管理
流量控制
拥塞控制

复习下TCP报文首部:

1、检验和

TCP检验和的计算与UDP一样,在计算时要加上12byte的伪首部,检验范围包括TCP首部及数据部分,但是UDP的检验和字段为可选的,而TCP中是必须有的。

2、序列号

TCP将每个字节的数据都进行了编号,这就是序列号。

序列号的作用:
数据传输过程中的确认应答处理、重发控制以及重复控制等功能都可以通过序列号来实现。

  • TCP在发送数据时,并不是按顺序发送的,发送出去的数据包也不能保证按序到达(网络的不确定性)。接收端接收到数据之后,按序号排序,如果中间某个数据报丢失了,之后的数据报还是会被接收,但是不会对发送端返回之后的确认,而是会重复发送对丢失出之前的数据确认,保证发送端会对丢失的数据段进行重发。保证数据的按序组装

  • TCP规定,在确认报文里,若确认号=N,意思是告诉发送者,到序号N-1为止的所有数据都已经正确的收到,下次你从N开始发送

  • 建立连接时,双方发送的SYN报文和ACK报文段都是不携带数据的,但是会消耗一个序号,这个序号通常是随机值

  • TCP规定,首部中序号字段的值是本报文段发送数据的第一个字节的序号。

3、确认应答机制(ACK)

TCP通过确认应答机制实现可靠的数据传输。在TCP的首部中有一个标志位——ACK,此标志位表示确认号是否有效。接收方对于按序到达的数据会进行确认,当标志位ACK=1时确认首部的确认字段有效。进行确认时,确认字段值表示这个值之前的数据都已经按序到达了。而发送方如果收到了已发送的数据的确认报文,则继续传输下一部分数据;而如果等待了一定时间还没有收到确认报文就会启动重传机制。

正常情况下的应答机制:

4、超时重传机制

当报文发出后在一定的时间内未收到接收方的确认,发送方就会进行重传(通常是在发出报文段后设定一个定时器,到点了还没有收到应答则进行重传),其基本过程如下:


TCP协议保证数据传输可靠性的方式

当然,未收到确认不一定就是发送的数据包丢了,还可能是确认的ACK丢了:

当接收方接收到重复的数据时就将其丢掉,重新发送ACK。而要识别出重复的数据,就要用到前面提到的序列号了,利用序列号很容易就可以做到去重的效果。

重传时间的确定:报文段发出到收到应答中间有一个报文段的往返时间RTT,显然超时重传时间RTO会略大于这个RTT,TCP会根据网络情况动态的计算RTT,即RTO是不断变化的。在Linux中,超时以500ms为单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。其规律为:如果重发一次仍得不到应答,就等待2500ms后再进行重传,如果仍然得不到应答就等待4500ms后重传,依次类推,以指数形式递增,重传次数累计到一定次数后,TCP认为网络或对端主机出现异常,就会强行关闭连接。

超时重传的过程:
  1. 放置片段到重传队列中,启动计时器:TCP在发送包含数据的片段后,片段都会被复制一份并放在重传队列中,然后启动计时器。

  2. 确认处理:如果在计时器超时之前收到确认信息,就把该片段从重传队列中移除

  3. 超时重传:如果在计时器超时之前没有收到确认信息,则相应片段被重新发送给对方,即重传机制,但是TCP也不能保证重传报文的可靠性,所以该报文依然会处于重传队列中,并重新计时,如果还是超时,则重复这一动作,而且超时时间会设置的较之前长,但是TCP只会重传一定数量的次数,因此当超过这个次数时,TCP会检查故障并断开连接

  4. 这个等待的时间被称为RTO,RTO也是根据RTT(传输往返时间)来确定的,也和当时网络的状态有关系,需要通过具体算法实现,不是确定值

    如果超时时间设置的太长,会影响整体的重传效率
    如果超时时间设置的太短,会频繁发送很多重复的包

  5. 去重:当主机B的确认报文丢失时,主机A没有收到相应的确认报文,就会重传,主机B会收到重复的报文,TCP会根据报文中的序列号来移除重复收到的报文。

5、连接管理机制

连接管理机制即TCP建立连接时的三次握手和断开连接时的四次挥手。

首先三次握手:

TCP协议保证数据传输可靠性的方式

建立过程为:

(1)B首先建立传输控制块TCB,进入LISTEN(收听)状态,等待用户的连接请求。如有,则建立连接。(这个过程在套接字编程中为服务器端调用socket函数、bind函数和listen函数的过程)

(2)A建立传输控制块TCB,然后向B发送连接请求报文段,报文段中首部的同步位SYN=1,同时选择一个序列号seq=x(实际测试是0),TCP规定SYN报文段不携带数据,但要消耗一个序列号。然后A进入SYN-SENT(同步已发送)状态。(这个过程在套接字编程中为客户端调用socket函数和connect函数的过程)

(3)B收到请求后,如同意建立连接,就向A发送确认报文段。此时SYN=1、ACK=1,确认号ack=x+1,同时选择一个序列号seq=y,这个报文也不携带数据,但要消耗一个序列号。然后B进入SYN-RCVD状态(同步收到)。

(4)A收到B的确认后,还要向B发送确认。确认报文段的ACK=1,确认号ack=y+1,seq=x+1。TCP规定,ACK报文段可以携带数据,而如果不携带数据则不消耗序列号,此时下一个报文段的序列号仍为seq=x+1。这时,连接就建立成功了,A进入ESTABLISHED状态(已建立连接状态)。

(5)当B收到A的确认后,也进入ESTABLISHED状态,此时就可以进行数据传输了。

当然,在进行三次握手时不是仅进行连接,可能还会进行一些后续操作所需要的信息交流。

为何是三次握手而非两次?

三次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,假如S发给C的应答丢失了,C将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

C由于长时间收不到S的回应,再次发起连接请求,S收到了请求,给C发送应答,双方建立了连接。过了一段时间,C最开始的那个连接请求抵达了,那么S又该如何处理?可见两次握手有很多问题。

四次挥手:

在连接释放时,连接的两方都要同意才能够释放成功(就像情侣分手一样,分手时两个人的事儿)。连接的双方都可以提出释放连接,这里假设A先提出释放连接,首先双方都处于ESTABLISHED状态。
TCP协议保证数据传输可靠性的方式

(1)当A的数据传送完后,就可以向其TCP发起连接释放了,此后停止再发送数据,主动关闭TCP连接。首先A向B发送一个FIN报文段,报文段首部FIN=1,序列号seq=u(u为最后传送的数据的序列号加1),然后A进入FIN-WAIT-1(终止等待1)状态。FIN报文段不能携带数据,但要消耗一个序列号。

(2)B收到释放连接的报文段后即发出确认报文段,报文首部ACK=1,ack=u+1,seq=v(v等于B前面传送过的数据的序列号加1),然后B进入CLOSE-WAIT(关闭等待)状态。这时从A到B这个方向的连接就释放了,TCP连接就处于半关闭状态。(注意:此后A不能主动向B发送数据,但是A可以给B发送确认报文段,也就是说A仍要接收来自B的报文,因为从B到A这个方向的连接还没有关闭)

(3)当A收到B的确认报文后,就进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。

(4)当B的数据发送完毕后,其应用进程就通知TCP释放连接。B向A发送FIN报文,报文段首部FIN=1,ack=u+1(重复发送上一次已经发送过的确认号),seq=w(w为B最后发送报文段的序列号加1)。然后B进入LAST-ACK(最后确认)状态,等待A的确认。

(5)A在接收到B的连接释放报文后,必须进行确认。A向B发送的确认报文段中报文首部ACK=1,ack=w+1,seq=u+1。然后A进入TIME-WAIT(时间等待)状态(如果无差错,此状态时间为2MSL),注意,此时TCP连接还没有释放掉,必须经过TIME-WAIT设置的时间2MSL后,A撤销相应的传输控制块TCB,才进入CLOSED状态,结束了此次TCP连接。MSL叫做最长报文段寿命,RFC793建议设为2分钟,但在现在实际网络情况中,常用值有三种:30秒,1分钟,2分钟。必须要在A进入CLOSED状态后才能开始建立下一个新的连接。

(6)B收到A的确认报文后,也进入CLOSED状态,撤销相应的传输控制块TCB,此时,TCP连接全部断开。

这样TCP四次挥手完成。

TIME-WAIT存在的必要性:
(1)可靠地实现了TCP全双工连接的终止

第一是为了保证最后一个的ACK报文能到达B。这个ACK报文有可能丢失,因而使得处在LAST_ACK状态的B得不到对已发送的FIN+ACK报文的确认,B会超时重传这个FIN+ACk ,而A就能在这TIME_WAIT时间(2MSL)里收到这个重传的报文,A就可以重传一次确认,如果没有这个TIME_WAIT, 那B重传的FIN_ACK,可A早就走了,自然不会再重发确认,这样B就无法按照正常步骤进入CLOSE 状态。

另一种解释说词是(本质是一样的):
我们知道,TCP是比较可靠的。当TCP向另一端发送数据时,他要求对端返回一个确认(如同我们关闭时候的FIN和ACK)。如果没有收到确认,则会重发。

回忆一下我们最终的那个FIN与ACK,被动关闭方发送FIN,并等待主动关闭方返回的ACK。我们假设最终的ACK丢失,被动关闭方将需要重新发送它的最终那个FIN,主动关闭方必须维护状态信息(TIME_WAIT),以允许它重发最终的那个ACK。

如果没有了这个状态,当他第二次收到FIN时,会响应一个RST(也是一种类型的TCP分节),会被服务器解释成一个错误。

为了TCP打算执行必要的工作以彻底终止某个连接两个方向上的数据流(即全双工关闭),那么他必须要正确处理连接终止四个分节中任何一个分节丢失的情况。

(2)允许老的重复分节在网络中的消逝(为什么需要2MSL)

首先,存在这样的情况,某个路由器崩溃或者两个路由器之间的某个链接断开时,路由协议需要花费数秒到数分钟的时间才能稳定找出另一条通路。在这段时间内,可能发生路由循环(路由器A把分组发送给B,B又发送回给A),这种情况我们称之为迷途。假设迷途的分组是一个TCP分节,在迷途期间,发送端TCP超时并重传该分组,重传分组通过某路径到达目的地,而后不久(最多MSL秒)路由循环修复,早先迷失在这个循环中的分组最终也被送到目的地。这种分组被称之为重复分组或者漫游的重复分组,TCP必须要正确处理这些重复的分组。

我们假设ip1:port1和ip2:port2 之间有一个TCP连接。我们关闭了这个链接,过一段时间后在相同IP和端口之间建立了另一个连接。TCP必须防止来自之前那个连接的老的重复分组在新连接上出现。为了做到这一点,TCP将不复用处于TIME_WAIT状态的连接。2MSL的时间足以让某个方向上的分组存活MSL秒后被丢弃,另一个方向上的应答也最多存活MSL秒后被丢弃。

聪明的你会发现谁先关闭谁就有一个TIME_WAIT的状态;

在linux的网络编程中,如果服务器如果先关闭,你会发现,现在想要立马再次启动服务器,就会报错说这个端口号被占用着,那就是因为有这个TIME_WAIT,2msl的时间.那么怎么解决 ?

 
   
   
 
  1. setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,(const char*)&reuseFlag, sizeof(reuseFlag))

备注:
MSL
MSL全称是maximum segment lifetime,最长分节生命期。MSL是任何IP数据报能够在因特网存活的最长时间。我们知道,这个时间是有限的,因为每个数据报都含有一个限跳(hop limit)的8位字段,它的最大值是255(简单的讲就是不同经过超过255个路由器)。尽管这个跳数限制而不是真正的时间限制,我们仍然假设最大限跳的分组在网络中存在的时间不可能超过MSL秒。
主动关闭方
跟握手不同,挥手可以由客户端发起,也可以是服务端发起。发起关闭的一端我们称之为主动关闭方,另一端称之为被动关闭方。

6、流量控制

接收端处理数据的速度是有限的,如果发送方发送数据的速度过快,导致接收端的缓冲区满,而发送方继续发送,就会造成丢包,继而引起丢包重传等一系列连锁反应。

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制叫做流量控制。为了实现流量控制,引入了滑动窗口的概念。

滑动窗口:

  • 窗口大小是指无需等待确认应答而可以继续发送数据的最大值

  • 在发送数据时并不是一次发送窗口的大小,而是将其分段,当第一段的数据收到一个ACK应答时,窗口就会向后滑动,继续发送下一段数据

  • 操作系统内核为了维护滑动窗口,开辟了一个发送缓冲区,用来记录当前还有哪些数据没有应,只要没有应答过的数据,都要在缓冲区内保存,只有确认应答过的数据才能删除!

在TCP报文段首部中有一个16位窗口长度,当接收端接收到发送方的数据后,在应答报文ACK中就将自身缓冲区的剩余大小,放入16窗口大小中。这个大小随数据传输情况而变,窗口越大,网络吞吐量越高,而一旦接收方发现自身的缓冲区快满了,就将窗口设置为更小的值通知发送方。如果缓冲区满,就将窗口置为0,发送方收到后就不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。

注意:窗口大小不受16位窗口大小限制,在TCP首部40字节选项中还包含一个窗口扩大因子M,实际窗口大小是窗口字段的值*窗口扩大因子。

7、拥塞控制

流量控制解决了两台主机之间因传送速率而可能引起的丢包问题,在一方面保证了TCP数据传送的可靠性。然而如果网络非常拥堵,此时再发送数据就会加重网络负担,那么发送的数据段很可能超过了最大生存时间也没有到达接收方,就会产生丢包问题。

为此TCP引入慢启动机制,先发出少量数据,就像探路一样,先摸清当前的网络拥堵状态后,再决定按照多大的速度传送数据。

此处引入一个拥塞窗口的概念:

发送开始时定义拥塞窗口个数为1;
每次收到一个ACK应答,拥塞窗口加1;
而在每次发送数据时,发送窗口取拥塞窗口与接送端接收窗口最小者。

慢启动、乘法减小、快恢复:
  • (1)设置一个慢启动的阈值,在启动初期以指数增长方式增长(慢启动并非说增长速度慢,而是说起点低);

  • (2)当以指数增长达到阈值时就停止指数增长,按照线性增长方式增加;

  • (3)线性增长达到网络拥塞时立即“乘法减小”,拥塞窗口置回达到网络拥塞时的一半,同时新一轮的阈值变为原来的一半,窗口按照线性增长方式增加。

还有一个快重传的概念:

TCP协议保证数据传输可靠性的方式
当1001~2000这段报文段丢失后,发送端就会以指收到1001的ACK,它告诉发送端,我收到了1~1000的数据报,我想要1001,但是后面的报文段它也会接收,却仍然发送丢失之前的应答。

直到接收方连续三次收到同样一个“1001”这样的应答,就会将对应的数据“1001~2000”重新发送

接收端收到1001~2000之后,就会直接返回ACK=7000(因为2001~7000接收端之前都收到过了,都放在了接收缓冲区中),这就是快重传。

与快重传配合使用的还有快恢复算法,其过程有以下两点:

  • 当发送端连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半,为了防止网络发送拥堵

  • 窗口减半之后,发送端现在认为网络很有可能没有发生拥堵,因此与慢开始不同之处是现在不执行慢开始算法,而是将cwnd的值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大!

整个机制可用图表示:


每天进步一点点……
喜欢的看官点个在看