TCP 三握四挥 重传机制 滑动窗口 流量控制 拥塞控制

TCP 三握四挥 重传机制 滑动窗口 流量控制 拥塞控制


TCP(Transmission Control Protocol)传输控制协议 虽然是块硬骨头,但是耐心去查、认真去理解,还是能啃下来的。本篇以小白到中级菜鸟的路线梳理了 TCP 基本通信流程(三握四挥)、TCP 如何实现可靠传输(重传机制、滑动窗口、流量控制、拥塞控制等机制)。

我们先瞟一眼 TCP 的标志位,方便下文引用讲解。

TCP 的 6 种标志位

  • SYN(synchronous建立联机)
  • ACK(acknowledgement 确认)
  • PSH(push传送)
  • FIN(finish结束)
  • RST(reset重置)
  • URG(urgent紧急)
  • Sequence number(顺序号码)
  • Acknowledge number(确认号码)

三握四挥

3次握手

  • 客户端–发送带有 SYN 标志的数据包–一次握手–服务端
  • 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
  • 客户端–发送带有 ACK 标志的数据包–三次握手–服务端

类比打电话栗子:1、听得到吗?2、听得到,你呢?3、我也听到了。然后才开始真正对话

在这里插入图片描述

为什么要 3 次握手、而不是 2 次嘞

因为双方都要确认自己与对方的发送与接收是正常的。

  • 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
  • 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
  • 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

所以三次握手就能确认双发收发功能都正常,缺一不可。

为什么要传回 SYN

接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。

传了 SYN,为什么还要传 ACK 呢

双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明 发送方接收方 的通道没有问题,但是 接收方发送方 的通道还需要 ACK 信号来进行验证。

4次挥手

  • 客户端 告诉 服务器端我要关闭连接
  • 服务器端 响应 说 我知道了;不再接收新的东西
  • 服务器端 说 我现在要关闭连接了
  • 客户端说 可以关闭了; ----》 通道关闭

类比下课栗子:1、学生:老师,下课了。2、老师:好,我知道了,我说完这点。3、老师:好了,说完了,下课吧。4、学生:谢谢老师,老师再见。

在这里插入图片描述

为什么握手 3 次,却要挥手 4 次

因为服务端第二次挥手后,虽然客户端不再发送数据,但是服务端仍可继续发送数据,客户端依然要接收。直到服务端第三次挥手,才是说明服务端也不再发送数据了。就像下课时老师说我知道下课了,但我说完这点哈(第二次挥手)。balabalabala…说完了,下课(第三次挥手)。
在这里插入图片描述
更专业的解释
建立连接时, 服务端在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,把 ACKSYN 放在一个报文里发送给客户端。
而关闭连接时,服务端收到客户端的 FIN 报文时,仅仅表示客户端不再发送数据了但是还能接收数据,而服务端也未必全部数据都发送给客户端了,所以服务端可以立即关闭,也可以发送一些数据给客户端后,再发送 FIN 报文给客户端来表示同意现在关闭连接,因此,服务端 ACKFIN 分开发送,从而导致多了一次。

为什么客户端最后还要等待2MSL

MSL(Maximum Segment Lifetime) 最大报文生存时间,TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个 ACK 报文能够到达服务端,因为这个 ACK 报文可能丢失,站在服务端的角度看来,我已经发送了 FIN + ACK 报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务端重新发送一次,而客户端就能在这个 2MSL 时间段内收到这个重传的报文,接着给出回应报文,并且会重启 2MSL 计时器。
第二,保证本次连接的重复数据段从网络中消失。如果客户端直接 CLOSED ,然后又再向服务端发起一个新连接,我们不能保证这个新连接与刚关闭连接的端口号是不同的。特殊情况:假设新连接和已经关闭的老连接端口号是一样的,如果老连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达服务端,TCP协议判断不同连接的依据是 socket pair,于是认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在 TIME_WAIT 状态等待 2MSL,这样可以保证本次连接的所有数据都从网络中消失。

看完三握四挥,是不是感觉略干,但还是懂了。建议客官休息一下下,喝杯水再继续阅读,不然可能失水过多(胡说八道 )…

在这里插入图片描述

2 minutes later…
接下来开始进阶中级菜鸟了。
在这里插入图片描述

重传机制

超时重传

  • TCP 实现可靠传输的方式之一,是通过序列号与确认应答
  • 在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据

发生超时重传的情况

TCP 会在以下两种情况发生超时重传

  • 数据包丢失
  • 确认应答丢失

RTT(Round-Trip Time 往返时延)

数据从网络一端传送到另一端所需的时间,也就是包的往返时间
在这里插入图片描述

RTO(Retransmission Timeout 超时重传时间)

  • 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差
  • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发
  • 超时重传时间 RTO 的值应该略大于报文往返 RTT 的值
预估往返时间采样

预估往返时间,通常需要采样以下两个

  • 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个 平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化
  • 除了采样 RTT,还要采样 RTT 的波动范围 ,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。
每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送

存在的问题

超时周期可能相对较长.可以用「快速重传」机制来解决超时重发的时间等待.

快速重传(Fast Retransmit)

不以时间为驱动,而是以数据驱动重传

工作方式

当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段

问题

重传的时候,是重传之前的一个,还是重传所有的。
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法

SACK(Selective Acknowledgment 选择性确认)

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

开启

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

D-SACK (Duplicate SACK)

使用了 SACK 来告诉「发送方」有哪些数据被重复接收了

作用栗子

  • ACK 丢包
    在这里插入图片描述

  • 网络延时
    在这里插入图片描述

好处

  • 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了
  • 可以知道是不是「发送方」的数据包被网络延迟了
  • 可以知道网络中是不是把「发送方」的数据包给复制了

开启

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)

滑动窗口

引入窗口概念的原因

TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。
– 缺点:数据包的往返时间越长,通信的效率就越低。
为解决这个问题,TCP 引入了窗口。可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。

窗口的实现

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

窗口大小由哪一方决定?

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

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

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

发送方的滑动窗口

四个部分

  • 已发送并收到 ACK 确认的数据
  • 已发送但未收到 ACK 确认的数据
  • 未发送但总大小在接收方处理范围内(接收方还有空间)
  • 未发送但总大小超过接收方处理范围(接收方没有空间)
    在这里插入图片描述

程序是如何表示发送方的四个部分的呢?

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)
在这里插入图片描述

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的)
  • SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.NXT 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了
  • 可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)

接收方的滑动窗口

三个部分

1、#1 + #2 是已成功接收并确认的数据(等待应用进程读取)
2、#3 是未收到数据但可以接收的数据
3、#4 未收到数据并不可以接收的数据
在这里插入图片描述

使用两个指针进行划分

  • RCV.WND:表示接收窗口的大小,它会通告给发送方
  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

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

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

流量控制

TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制

操作系统缓冲区与滑动窗口的关系

实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。
当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响

防丢包

TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间在减少缓存,这样就可以避免了丢包情况

窗口关闭

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

窗口关闭潜在的危险

当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不不采取措施,这种相互等待的过程,会造成了死锁的现象。

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

  • TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
  • 如果持续计时器超时,就会发送窗口探测 Window probe 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了

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

糊涂窗口综合症

  • 如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
  • 到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。

发生

  • 接收方可以通告一个小的窗口
  • 发送方可以发送小数据

解决

  • 让接收方不通告小窗口给发送方
  • 让发送方避免发送小数据

怎么让接收方不通告小窗口呢?

接收方通常的策略如下:

  • 当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS1/2 缓存大小 中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
  • 等到接收方处理了一些数据后,窗口大小 >= MSS ,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

怎么让发送方避免发送小数据呢?

发送方通常的策略:
使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

  • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
  • 收到之前发送数据的 ACK 回包

Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnetssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

拥塞控制

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

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

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

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大…

  • TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络

拥塞窗口 cwnd

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

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

拥塞窗口 cwnd 变化的规则

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

怎么知道当前网络是否出现了拥塞呢?

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

拥塞控制有哪些控制算法?

慢启动

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

算法规则
  • 当发送方每收到一个 ACK ,拥塞窗口 cwnd 的大小就会加 1
  • 发包的个数是指数性的增长
那慢启动涨到什么时候是个头呢?
  • cwnd < ssthresh 时,使用 慢启动算法
  • cwnd >= ssthresh 时,就会使用 拥塞避免算法

拥塞避免

一般来说 ssthresh 的大小是 65535 字节。

算法规则
  • 进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd
  • 线性增长

拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
当触发了重传机制,也就进入了「拥塞发生算法」。

拥塞发生

发生超时重传的拥塞发生算法

当发生了「超时重传」,就会使用拥塞发生算法。

sshreshcwnd 的值会发生变化

  • ssthresh 设为 cwnd/2
  • cwnd 重置为 1
    在这里插入图片描述

重新开始慢启动,慢启动是会突然减少数据流的。这真是一旦「超时重传」,马上回到解放前。但是这种方式太激进了,反应也很强烈,会造成网络卡顿

发生快速重传的拥塞发生算法

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

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下

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

快速恢复

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

正如前面所说,进入快速恢复之前,cwndssthresh 已被更新了

  • cwnd = cwnd/2 ,也就是设置为原来的一半
  • ssthresh = cwnd
算法规则
  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了)
  • 重传丢失的数据包
  • 如果再收到重复的 ACK ,那么 cwnd 增加 1
  • 如果收到新数据的 ACK 后,设置 cwndssthresh,接着就进入了拥塞避免算法
    在这里插入图片描述

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

巨人的肩膀

[1] 趣谈网络协议专栏.刘超.极客时间

[2] Web协议详解与抓包实战专栏.陶辉.极客时间

[3] TCP/IP详解 卷1:协议.范建华 译.机械工业出版社

[4] 图解TCP/IP.竹下隆史.人民邮电出版社

[5] The TCP/IP Guide.Charles M. Kozierok.

[6] TCP那些事(上).陈皓.酷壳博客.

[7] TCP那些事(下).陈皓.酷壳博客.

[8] 你还在为 TCP 重传、滑动窗口、流量控制、拥塞控制发愁吗?

[9] 两张动图-彻底明白TCP的三次握手与四次挥手

结语

非常感谢这些巨人的肩膀,他们写得很不错,看多几篇、看多几遍,巨复杂的东西也会渐渐变得清晰明朗。尤其是 滑动窗口流量控制 ,他们画了很多图有助理解,但是我认真看了有些小错误,就不 copy 过来啦(自己又懒得画喔,真是讨厌 )。遂整理了能唤醒记忆、通过想象构图的骨架以及概念、应用,整体偏骨感,亦能引发思考(嗯?自卖自夸? )。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值