TCP是如何保证可靠传输的

1.重传机制

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

在TCP中,发送端发送的数据到达接收端后,接收端会响应回一个Ack确认应答消息,表示已经收到了数据。

但在错综复杂的网络传输中,万一发送出去的数据在传输过程中丢失了该怎么办?

1.1 超时重传:

超时重传是指:在发送端在发送数据后开启一个定时器,当超过定时器规定的时间后,如果还没有收到接收端的Ack应答报文,发送端就会重新发送数据包。

超时重传的可能会在两种情况下发生:

  • 发生的数据包丢失:当发送的数据包丢失后,由于接收端接收不到数据包,不会发送响应Ack;发送端发现迟迟收不到Ack,就会触发超时重传
  • 响应的Ack丢失:接收端在接收到数据包后发送出的响应包丢失了,发送端迟迟收不到响应,也会进行超时重传

那么超时的时间应该设置为多少?

RTT(Round-Trip Time 往返时延):发送端发送数据的时刻~发送端接收到响应的时刻的差值

RTO(Retransmission Timeout ):超时重传时间

当RTO较大时,触发重发就慢,没有效率;当RTO较小,可能数据包并没有丢失就触发重传,重发得快,容易造成网络拥塞,导致更多的重传,恶性循环。

所以,超时重传RTO的值应该设置为略大于RTT的值

1.2 快速重传

快速重传(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 报文时,会在定时器过期之前,重传丢失的报文段。

快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。

假设发送方发送6个数据,其中Seq2和Seq3都丢失了,在快速重传时接收端响应Seq456时发送的是连续的Ack2,但发送端并不知道这些Ack2是接收方哪个收到哪个报文而回复的(是Seq356?还是Seq456?亦或是Seq346?)

  • 如果指重传Seq2,那么对于丢失的Seq3后续还要走一遍重传Seq3
  • 如果重传Seq2~6,那么对于Seq4 ~ 6又发了一遍,浪费资源

所以在连续丢失多个包的情况下,快速重传是有缺陷的。

1.3 SACK方法

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

SACK方法是在快速重传的基础上对接收端发送的响应报文的字段里加了一个SACK的东西,SACK字段里存的是本次接收端接收到的数据信息,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

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

1.4 Duplicate SACK

D-SACK和SACK的操作类似,只是换一种思路,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

ACK 丢包

  • [接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
  • 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK
  • 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。

2.滑动窗口

如果没有滑动窗口,TCP的发送数据和响应数据都是“一问一答”的形式,如果某个数据包的处理时间过长,就会阻塞后面的数据包处理,通信效率就会很低。

有了滑动窗口,指定窗口的大小:发送端无需等待应答,可以继续发送数据的最大值

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

TCP 头里有一个字段叫 Window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

所以,通常窗口的大小是由接收方的窗口大小来决定的。

发送端的滑动窗口

当我们把滑动窗口里的46~51数据发送出去之后,可用窗口就为0了,在接收到Ack之前发送端不能再发送数据

image-20230423113432290

当发送端接收到对32~36的响应后,窗口滑动5个字节,可用窗口扩为5个

image-20230423113423938

接收方的窗口

image-20230423114224034

接收窗口和发送窗口的大小是相等的吗?

并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

3.流量控制

TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。

窗口关闭:当窗口的可用大小为0时,就会阻止发送方发送数据,直到窗口变为非0

但是窗口关闭存在着一些危险

当接收方发生窗口关闭后,等到接收方处理完数据窗口扩大,接收方会发送一个通知窗口非0的ACK报文时,如果这个报文丢失了,发送方并不不知道被允许发送数据了,而接收方又一直在等待对方的数据发送,这样就会导致死锁。

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

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

这样如果一方的非0窗口报文丢失了,另一方触发计时器超时发送窗口探测报文获取对方的现有窗口大小,这样就可以打破死锁的局面了。

窗口探测的次数一般为3次,每次30-60秒,如果3次过后收到的窗口大小还是0的话,就会发送RST报文来中断连接。

4.拥塞控制

流量控制能够避免发送方的数据填满接收方的缓存,但我们并不知道网络中发生了什么

网络是一个所有计算机共享的环境,因此有可能其他的主机间的通信也会造成网络拥堵

在网络拥堵的情况下,我们发生的数据包可能会发生时延或者丢失,就会触发重传,但是一旦重传就会加剧网络的拥塞,造成恶性循环被不断放大

所以当网络发送拥塞时,TCP连接会降低自己的数据发送量,避免自己发送的数据塞满整个网络

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

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

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

拥塞控制的算法,主要有四个:

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

慢启动:

TCP连接刚建立完成后,首先有个慢启动的过程:先一点一点提高发送数据包的数量

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

  • 假设连接建立完成后,一开始初始化cwnd = 1,表示可用传一个MSS大小的数据
  • 当收到一个ACK应答后,cwnd += 1,于是可以一次发送2个
  • 当收到2个的ACK应答后,cwnd += 2,于是可以一次发送4个
  • 当收到2个的ACK应答后,cwnd += 4,于是可以一次发送8个

可见,慢启动的算法是 cwnd = 2^n,是一个指数级的增长

但是由于指数级增长是十分快的,需要设置一个阈值,也就是慢启动门限ssthresh(slow start threshold)

  • 一般来说,ssthresh的大小是 65535 字节(2^16)
  • 当cwnd < ssthresh时,就会使用慢启动算法
  • 当cwnd > ssthresh时,就会使用拥塞避免算法

拥塞避免算法

拥塞算法的规则就是:每当收到一个ACK时,cwnd增加1/cwnd。

当8个ACK应答确认到来时,每个确认增加1/8,8个ACK确认cwnd+=1,于是下一次能一次发送9个MSS大小的数据,呈现出线性增长

一直不断增长下去,网络就会慢慢进入拥塞的状况了,开始出现丢包的现象,需要重传。

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

当触发了重传机制,也就进入了拥塞发送算法

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
  • 进入快速恢复算法:

快速恢复算法:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

image-20230423160428651


 上一篇
Redis的相关特性 Redis的相关特性
1.redis的单线程怎么理解?redis的单线程指的是客户端接收请求-解析请求-进行数据读写操作-返回数据给客户端这个过程都是由一个主线程进行的,对于命令的执行是由单线程进行的。但是redis还会开启三个后台线程 三个后台线程分别处理:
2023-05-17
下一篇 
HTTP、HTTPS区别 HTTP、HTTPS区别
HTTP/1.1 和HTTP/1.0 的区别?HTTP/1.1 相比 HTTP/1.0 性能上的改进: 使用长连接的方式改善了1.0中的短连接所带来的性能开销 通过管道网络传输,发送端可以不必等接收
2023-04-10
  目录