今天来讲一下TCP是如何保证可靠传输的。这也是面试常问的一个题目,这个问题不单止能看出你是否真的了解TCP原理,更看出你是否有一个总结的能力。
我们从三个部分来讲TCP是如何实现可靠传输的。
滑动窗口
首先是讲TCP中的滑动窗口,它和TCP的可靠传输息息相关。TCP的滑动窗口是以字节为单位的,假设现在发送端收到了接收端发送过来的确认报文段,其中窗口值给予的是14字节,确认号为36(表明接收端期望收到下一个序号是36,35为止的数据都已经收到了)。根据接收端的确认报文段,发送方可以维护自己的发送窗口(注意前沿和后沿的位置!!!)
发送端可以连续把窗口内的数据都发送出去,发送出去后,如果没有收到确认,那么都必须暂时的保留,便于在超时重传(后面会讲到)时使用。假设接收端接收到了39-49序号,但是36没有收到,那么滑动窗口就不会向前移动,而是等到后沿的字节被确认接收到后,那么滑动窗口才会开始移动。
发送窗口越大,发送方就可以在接收方确认之前连续发送更多的数据,因而可能获得更高的传输效率。发送窗口后沿后面的部分表示已经发送并且已经确认收到,所以这些数据显然不需要了;发送窗口前沿的部分表示不允许发送,因为接收方没有为这部分数据保留存放的缓存空间。
滑动窗口的大小由前沿和后沿共同决定,发送窗口的后沿变化情况有两种:前移和不动。
发送窗口的后沿是不可能向后移动的,因为不能撤销掉已经确认的数据。前移代表接收到了靠近后沿处的数据,所以滑窗向前移,不移动代表还没有收到后沿数据新的确认。
发送窗口的前沿有三种情况,前移,保持不动,后移。前移好理解,可能是确认报文段顺带的窗口信息告诉发送方,窗口变大了,或者是收到了后沿处的ACK,都会往前移动。保持不动分为两种情况,一种是没有收到新的确认,接收端发送的窗口信息也不改变;第二种是收到了新的确认,但是对方通知的窗口缩小,所以滑窗前沿会保持不变。滑窗的前沿也有可能向后收缩,这发生在没有收到新的字节确认报文段(发送方和接收方还是有交互,只是没有ack没有更新,还是之前的值),并且窗口缩小了。TCP强烈不赞成这样做,因为可能导致某些问题。
TCP规定接收方必须有积累确认的能力,这样可以减少传输的开销。接收方可以在适合的时候去发送确认,或者在自己有数据要发送的时候顺带去发送确认信息。但是接收方不能过分的推迟发送确认,否则会导致发送方不必要的重传,浪费网络资源。TCP规定确认推迟的时间不应该超过0.5秒。如果接收方收到了一连串具有最大长度的报文段,那么每隔一个报文段就要发送一个确认。
因为TCP的通信是全双工的,所以在通信的每一方都有发送和接收窗口。
相关视频推荐
支撑互联网的基石 tcp/ip,5个方面全面解析
网络原理tcp/udp,网络编程epoll/reactor,面试中正经“八股文”
100行代码开启自己的协议栈,《tcp/ip详解》的代码注解|
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
超时重传时间
第二点是讲超时的重传时间,之前我们讲到过,TCP发送方如果在规定的时间内没有收到确认报文就要重传已发送的报文段。这个重传概念说起来是很简单,但是重传时间的选择可以说是TCP中最复杂的问题之一。
TCP的下一层是IP层,发送的IP数据报可能经过高速率网络,也可能经过低速率网络,每个数据报选择的路由可能不一样,如果把超时重传时间设置的太短,会引起不必要的重传,增大网络负荷;如果把超时时间设置的过长,降低了传输效率,使得网络空闲时间增大。
TCP采用了一种自适应算法,它会去记录一个报文段发出的时间,以及收到确认的时间,这两个时间的差即报文段往返时间RTT(Round-Trip Time),同时TCP会去保留RTT的加权平均往返时间RTT[S]。第一次测量时,RTT[s] = RTT,随后每次测量到一个新的RTT样本,就会按照下面的公式重新计算一次RTT[s]。
新的RTT[s] = (1 - α) x (旧RTT[s]) + α x (新RTT)
上面的表达式α是[0, 1)的,如果α接近于0,那么表示新的RTT样本对RTT[s]影响不大,如果α接近于1,那么新的RTT样本对RTT[s]影响较大。建议标准推荐将α设置为1/8,即0.125。
所以说超时计时器设置的超时重传时间RTO(RetransmissionTime-Out)要略微的大于加权平均往返时间RTT[s]。
RTO=RTT[s] + 4 x RTT[d]
RTT[d]是RTT的偏差的加权平均值,大家可以做个了解:
新的RTT[d] = (1-β) x (旧RTT[D]) + β x |RTT[s] - 新RTT样本|
β推荐为1/4,即0.25。
现在来考虑这么一个问题,假设发送方发出了一个报文段,设定的重传时间到了,没有收到确认报文,于是重传了报文段,经过了一段时间后,收到了确认报文段。那么这个确认报文段是对第一次发送的报文段的确认呢,还是对第二次发送的报文段确认呢?而这个的判断就会影响到当前的RTT,导致影响RTT[s]偏大或者偏小。
根据这种情况,Karn提出,如果报文段重传了,那么我们就不去记录它的往返时间样本,这样得出来的RTT[s]和RTO就会比较的准确。
但是这也会引发一个新的问题,如果报文段的时延突然增大了很多,在原来设置的重传时间内没有收到确认报文,那么就重传报文段,但因为对重传的数据不做样本,所以超时重传时间不会更新。
所以对Karn的算法做修正,每超时重传一次,就会将RTO增大些,一般是取旧时间的2倍,当不再发生报文段重传的时候,再通过之前的公式去计算,使得计算结果更加合理。
确认SACK
第三点是确认SACK,很多人可能不了解这个。我来举一个场景,当主机A和主机B在交互,此时此刻,主机A充当发送端,主机B充当接收端。
接收端接收到了主机A发送过来的数据字节流,但是目前接收到的是不连续的序号,最前方的我们叫做连续的字节块1-500,第一个字节块与连续的字节块之间还有序号为501-100没有收到,第一个字节块与第二个字节块中间有1501-2000序号没有收到。
因为TCP的机制,确认报文段中的ACK位 = 1,ack字段为501,是接收端期望收到发送端下一个数据序号501。虽然它收到了1001-1500的数据字节,但是TCP机制只会让它告诉给发送端,最接近窗口后沿处未收到的字节序号。
那么这个时候,如果发送方发现接收方ack = 501,就知道501之前的数据接收成功了,500之后的数据没有收到,就会重新发送数据,所以第一个字节块的内容可能重复的去发送了。那么怎么去解决这个问题呢?
TCP首部中有一个选项字段,这个字段最大为40个字节。通信的双方在事先需要商定好,允许使用SACK选项,这样的话,在选项中,我们可以传递收到不连续的字节块信息。因为一个序号需要占用4个字节,指明你收到一个字节块的范围需要用到8个字节来表示,因为需要把左右边界告诉给发送方,正常来说是可以传输5个字节块信息,因为5 x 8 = 40字节,但是由于需要用1个字节来指明SACK选项,一个字节说明这个选项要占用几个字节,所以只能传输4个字节块的信息。
这一块可以做了解,因为大多数的实现还是重传所有没有被确认的数据块。好啦,本期TCP如何实现可靠传输就说到这里啦。