zoukankan      html  css  js  c++  java
  • TCP是如何保证可靠传输的?

    相信大家都知道 TCP 是一个可靠传输的协议,那它是如何保证可靠的呢?

    为了实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、 重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。

    那么,TCP 是通过序列号、确认应答、 重发控制、连接管理以及窗口控制等机制实现可靠性传输的。

    重传机制

    TCP 实现可靠传输的方式之一,是通过序列号与确认应答。

    在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。

    TCP 针对数据包丢失的情况,会用重传机制解决。

    接下来说说常⻅的重传机制:超时重传、 快速重传、 SACK、 D-SACK
    1.超时重传
    重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会 重发该数据,也就是我们常说的超时重传。
    TCP 会在以下两种情况发生超时 重传: 数据包丢失、确认应答丢失
    超时时间应该设置为多少呢?
    RTT (Round-Trip Time 往返时延),就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间
    RTO (Retransmission Timeout 超时 重传时间)
    超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。

    2.快速重传

    TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。

     

    在上图,发送方发出了 1,2,3,4,5 份数据:

    第一份 Seq1 先送到了,于是就 Ack 返回 2;
    结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
    后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
    发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。

    最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

    所以,快速 重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前, 重传丢失的报文段。

    快速 重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重 传之前的一个,还是重传所有的问题。
    比如对于上面的例子,是 传 Seq2 呢?还是 传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的 三个 Ack 2 是谁传回来的。

    根据 TCP 不同的实现,以上两种情况都是有可能的。可⻅,这是一把双刃剑。 为了解决不知道该 传哪些 TCP 报文,于是就有 SACK 方法。

     3.SACK

    还有一种实现 重传机制的方式叫: SACK ( Selective Acknowledgment 选择性确认)。

    这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东⻄,它可以将缓存的地图发送给发送方,这样发送 方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。

    如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速 重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则 发时,就只选择了这个 TCP 段进行重复。

    如果要支持 SACK ,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

     4.Duplicate SACK

    Duplicate SACK 又称 D-SACK ,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

    滑动窗口

    窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

    假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以 通过「下一个确认应答进行确认」。如下图:

    图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫
    累计确认或者累计应答。
    窗口大小由哪一方决定?

    TCP 头里有一个字段叫 Window ,也就是窗口大小。 这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收

    端处理不过来。所以,通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

    发送方的滑动窗口
    我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送 窗口,紫色方框是可用窗口:
     
    #1 是已发送并收到 ACK确认的数据:1~31 字节
    #2 是已发送但未收到 ACK确认的数据:32~45 字节
    #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
    #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后
    在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

     

    在下图,当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往 右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了
    可用窗口,那么后续也就 可以发送 52~56 这 5 个字节的数据了。
    程序是如何表示发送方的四个部分的呢?
    TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特 定的序列号),一个是相对指针(需要做偏移)。

     

    SND.WND :表示发送窗口的大小(大小是由接收方指定的);
    SND.UNA :是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一 个字节。

    SND.NXT :也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个 字节。

    指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移 ,就可以指向 #4 的第一个字节了。

    可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)

    接收方的滑动窗口

     接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:

    #1 + #2 是已成功接收并确认的数据(等待应用进程读取);

    #3 是未收到数据但可以接收的数据;
    #4 未收到数据并不可以接收的数据;

    其中三个接收部分,使用两个指针进行划分:
    RCV.WND :表示接收窗口的大小,它会通告给发送方。

    RCV.NXT :是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字 节。

    指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移 ,就可以指向 #4 的第一个字节了。

    流量控制

    发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
    如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发 发机制,从而导致网络流 的无端的浪费。
    为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量, 这就是所谓的流量控制
    窗口关闭
    在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流 控制。
    如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
    接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。

    那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口 的 ACK 报文在网络中丢失了,那麻烦就大了

    这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待 的过程,会造成了死锁的现象 

    TCP 是如何解决窗口关闭时,潜在的死锁现象呢? 

    为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持 续计时器。

    如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

    如果接收窗口仍然为 0,那么收到这个报文的一方就会 新启动持续计时器; 如果接收窗口不是 0,那么死锁的局面就可以被打破了。 

     

    窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的 话,有的 TCP 实现就会发 RST 报文来中断连接。 

    拥塞控制

     为什么要有拥塞控制啊,不是有流量控制了嘛?

    前面的流量控制是避免发送方的数据填满接受方的缓存,但是并不知道网络中发生了什么。

    一般来说,计算机网络都处在一个共享的环境,因此也有可能会因为其他主机之间的通信使得网络拥堵。

    在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是 一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,

    这个情况就会进入恶性循环被不断地放大.... 

    为了在「发送方」调节所要发送数据的 ,定义了一个叫做「拥塞窗口」的概念。 

    拥塞窗口 cwnd是发送方维护的一个的状态变 ,它会根据网络的拥塞程度动态变化的。 

    我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值 

    拥塞窗口 cwnd 变化的规则: 

    只要网络中没有出现拥塞, cwnd 就会增大; 

    但网络中出现了拥塞, cwnd 就减少; 

    其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞 。

    拥塞控制有哪些算法?

    • 慢启动
    • 拥塞避免
    • 拥塞发生
    • 快速恢复

    慢启动

    TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数 , 如果一上来就发大量的数据,这不是给网络添堵吗?

    慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。

    这里假定拥塞窗口 cwnd 和发送窗口 swnd 相等,下面举个栗子:

    连接建立完成后,一开始初始化 cwnd = 1 ,表示可以传一个 MSS 大小的数据。

    当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个

    当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个

    当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。 

     

    可以看出慢启动算法,发包的个数是指数性的增⻓。 

    那慢启动涨到什么时候是个头呢?

    有一个叫慢启动⻔限 ssthresh (slow start threshold)状态变 。

    当 cwnd < ssthresh 时,使用慢启动算法。

    当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。 

    拥塞避免算法

    前面说道,当拥塞窗口 cwnd 「超过」慢启动⻔限 ssthresh 就会进入拥塞避免算法。 一般来说 ssthresh 的大小是 65535 字节。 那么进入拥塞避免算法后,它的规则是:每当收到一个

    ACK 时,cwnd 增加 1/cwnd。

    接上前面的慢启动的栗子,现假定 ssthresh 为 8 :当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9个 MSS 大小的数据,变成了线性增⻓。 

    所以我们可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度慢了一些,就这么一直增长着,网络就会慢慢进入了拥塞的状况了,于是就会出现

    丢包现象,这时就需要对丢失的数据包进行重传,当触发了重传机制,也就进入了拥塞发生算法。

    拥塞发生

    当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

    • 超时重传
    • 快速重传

    当发生了「超时重传」,则就会使用拥塞发生算法。 这个时候,ssthresh 和 cwnd 的值会发生变化:

    ssthresh 设为 cwnd/2 , cwnd 置为 1 

    还有更好的方式,前面我们讲过「快速 传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。 

    TCP 认为这种情况不严 ,因为大部分没丢,只丢了一小部分,则

    cwnd = cwnd/2 ,也就是设置为原来的一半;

    ssthresh = cwnd ;

    进入快速恢复算法 ;

    快速恢复

    快速 传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟 糕,所以没有必要像 RTO 超时那么强烈。

    正如前面所说,进入快速恢复之前, cwnd 和 ssthresh 已被更新了: cwnd = cwnd/2 ,也就是设置为原来的一半;

    ssthresh = cwnd ; 然后,进入快速恢复算法如下:

    拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了); 传丢失的数据包;
    如果再收到重复的 ACK,那么 cwnd 增加 1;

    如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说 明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,

    可以回到恢复之前的状态了,也即再次进 入拥塞避免状态; 

    也就是没有向超时重传一夜回到解放前,而是还在比较高的值,后续呈线性增长。

     
  • 相关阅读:
    使用contentProvider
    创建Sqlite数据库(一)
    AIDL实现进程间通信
    Messenger实现进程间通信(IPC)
    Serializable使用
    Parcelable使用(二)
    STAR法则
    Python系列-------基本语法
    前端随心记---------面试题集
    前端随心记---------惟客科技面试
  • 原文地址:https://www.cnblogs.com/kumufengchun/p/14732949.html
Copyright © 2011-2022 走看看