目录
一、前言
二、重传机制
1. 超时重传
2. 快速重传
3. SACK
4. D-SACK
三、滑动窗口
1. 发送方的滑动窗口
2. 程序是如何表示发送方的四个部分
3. 接收方的滑动窗口
四、流量控制
五、拥塞控制
1. 慢启动
2. 拥塞避免
3. 拥塞发生
4. 快速恢复
六、TCP粘包问题
七、TCP异常情况
八、TCP小结
一、前言
在之前的博客中,我们简单介绍了TCP通过序列号和确认应答号以及连接管理(三次握手和四次挥手) 机制来是实现可靠性传输,除了这些,还有下面要介绍的其他机制,如:重发控制、流量控制、滑动窗口和拥塞控制。
二、重传机制
TCP 实现可靠传输的方式之一,是通过序列号与确认应答。在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?所以 TCP 针对数据包丢失的情况,会用重传机制解决。
常见的重传机制如下:
- 超时重传
- 快速重传
- SACK
- D-SACK
1. 超时重传
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。
TCP在两种情况下会触发 超时重传:
- 确认应答丢失
- 数据包丢失
- 当出现丢包时,客户端(发送方)是无法辨别是发送的数据包丢失了,还是对方发来的确认应答报文丢失了,因为这两种情况下发送方都收不到对方发来的响应报文,此时客户端(发送方)就只能进行超时重传。
- 如果是服务器(接收方)确认应答报文丢失而导致发送方进行超时重传,此时服务器(接收方)就会再次收到一个重复的报文数据,但此时也不用担心,接收方可以根据报头当中的32位序列号来判断曾经是否收到过这个报文,从而达到报文去重的目的。
- 需要注意的是,当发送缓冲区当中的数据被发送出去后,操作系统不会立即将该数据从发送缓冲区当中删除或覆盖,而会让其保留在发送缓冲区当中,以免需要时进行超时重传,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除或覆盖。
超时时间是如何确定的呢?
我们先来了解一下什么是 RTT(Round Trip Time),它是指报文的往返时间。
超时重传时间是以 RTO(Retransmission Timeout 超时重传时间)表示。
如果超时重传的时间过长或过短会发生什么情况呢?
- 超时重传的时间设置的太长,会导致丢包后对方长时间收不到对应的数据,进而影响整体重传的效率。
- 超时重传的时间设置的太短,会导致对方收到大量的重复报文,可能对方发送的响应报文还在网络中传输而并没有丢包,但此时发送方就开始进行数据重传了,并且发送大量重复报文会也是对网络资源的浪费。
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增。
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
这里可以推荐看看这篇博客,写的很详细:
2. 快速重传
TCP 还有另外一种快速重传机制,它不以时间为驱动,而是以数据驱动重传。
快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。为了解决不知道该重传哪些 TCP 报文,于是就有
SACK
方法。
3. SACK
还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认)。
这种方式需要在 TCP 头部选项字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
4. D-SACK
其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。
1. ACK丢失
- 接收方 发给 发送方 的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第⼀个数据包(3000 ~ 3499)
- 于是接收方发现数据是重复收到的,于是回了⼀个 SACK = 3000~3500,告诉发送方 3000~3500的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个SACK 就代表着 D-SACK 。
- 这样发送方就知道了,数据没有丢,是接收方的 ACK 确认报文丢了。
2. 网络延迟
- 数据包(1000~1499) 被网络延迟了,导致发送方没有收到 Ack 1500 的确认报文。
- 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了接收方;
- 所以接收方回了⼀个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。
- 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。
可见, D-SACK 有这么几个好处:
- 可以让发送方知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是发送方的数据包被网络延迟了;
- 可以知道网络中是不是把发送方的数据包给复制了;
三、滑动窗口
我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。 如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。
- 所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。
- 为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。
- 那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
- 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已
- 发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以 通过「下一个确认应答进行确认」。如下图:
图中的 ACK 600 确认应答报⽂丢失,也没关系,因为可以通过下⼀个确认应答进行确认,只要发送方收到了 ACK
700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。
窗口大小由哪一方决定?
TCP 头里有一个字段叫 Window ,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以,通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
1. 发送方的滑动窗口
我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:
- #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 个字节的数据了。
2. 程序是如何表示发送方的四个部分
TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。
- SND.WND :表示发送窗口的大小(大小是由接收方指定的);
- SND.UNA :是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
- SND.NXT :也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
- 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向#4 的第一个字节了。
那么可用窗口大小的计算就可以是:可用窗口大小 = SND.WND - ( SND.NXT - SND.UNA )
3. 接收方的滑动窗口
接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:
- #1 + #2 是已成功接收并确认的数据(等待应用进程读取);
- #3 是未收到数据但可以接收的数据;
- #4 是未收到数据并不可以接收的数据;
其中三个接收部分,使用两个指针进行划分:
- RCV.WND :表示接收窗口的大小,它会通告给发送⽅。
- RCV.NXT :是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
- 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向#4 的第一个字节了。
四、流量控制
- 发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
- 如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
- 为了解决这种现象发生,TCP提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
假设以下场景:客户端是接收方,服务端是发送方假设接收窗口和发送窗口相同,都为 200 假设两个设备在整个传输过程中都保持相同的窗口大小,不受外界影响
根据上图的流量控制,说明下每个过程:
- 客户端向服务端发送请求数据报文。这里要说明下,本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。
- 服务端收到请求报文后,发送确认报⽂和 80 字节的数据,于是可用窗口 Usable 减少为 120 字节,同时SND.NXT 指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。
- 客户端收到 80 字节数据后,于是接收窗口往右移动 80 字节, RCV.NXT 也就指向 321,这意味着客户端期望的下⼀个报文的序列号是 321,接着发送确认报⽂给服务端。
- 服务端再次发送了 120 字节数据,于是可用窗口耗尽为 0,服务端无法再继续发送数据。
- 客户端收到 120 字节的数据后,于是接收窗口往右移动 120 字节, RCV.NXT 也就指向 441,接着发送确认报文给服务端。
- 服务端收到对 80 字节数据的确认报文后, SND.UNA 指针往右偏移后指向 321,于是可用窗口 Usable 增大到 80。
- 服务端收到对 120 字节数据的确认报文后, SND.UNA 指针往右偏移后指向 441,于是可用窗口 Usable 增大到 200。
- 服务端可以继续发送了,于是发送了 160 字节的数据后, SND.NXT 指向 601,于是可用窗口 Usable 减少到 40。
- 客户端收到 160 字节后,接收窗口往右移动了 160 字节, RCV.NXT 也就是指向了 601,接着发送确认报文给服务端。
- 服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是 SND.UNA 指针偏移了160 后指向 601,可用窗口 Usable 也就增大至了 200。
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
五、拥塞控制
为什么要有拥塞控制呀,不是有流量控制了吗?
- 前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。
- ⼀般来说,计算机网络都处在⼀个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
- 在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是⼀重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大;
- 所以,TCP 不能忽略网络上发生的事,它被设计成⼀个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。
- 于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
- 为了在「发送方」调节所要发送数据的量,定义了⼀个叫做「拥塞窗口」的概念。
什么是拥塞窗口?和发送窗口有什么关系呢?
- 只要网络中没有出现拥塞, cwnd 就会增大;
- 但网络中出现了拥塞, cwnd 就减少;
那么怎么知道当前网络是否出现了拥塞呢?
其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
拥塞控制有哪些控制算法?
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
1. 慢启动
TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是⼀点⼀点的提高发送数据包的数量, 如果一上来就发大量的数据,这不是给网络添堵吗?慢启动的算法记住⼀个规则就行:当发送⽅每收到⼀个 ACK,拥塞窗口 cwnd 的大小就会加 1。这里假定拥塞窗口 cwnd 和发送窗口 swnd 相等,下面举个栗子:
- 连接建立完成后,⼀开始初始化 cwnd = 1 ,表示可以传⼀个 MSS 大小的数据。
- 当收到⼀个 ACK 确认应答后,cwnd 增加 1,于是⼀次能够发送 2 个
- 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这⼀次能够发送 4 个
- 当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwndcwnd 增加 4,于是就可以比之前多发4 个,所以这⼀次能够发送 8 个。
可以看出慢启动算法,发包的个数是指数性的增长。
那慢启动涨到什么时候是个头呢?
- 当 cwnd < ssthresh 时,使⽤慢启动算法。
- 当 cwnd >= ssthresh 时,就会使⽤「拥塞避免算法」。
2. 拥塞避免
前⾯说道,当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。⼀般来说 ssthresh 的大小是 65535 字节。那么进入拥塞避免算法后,它的规则是:每当收到⼀个 ACK 时,cwnd 增加 1/cwnd。
接上前面的慢启动的例子,现假定 ssthresh 为 8 :
当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd ⼀共增加 1,于是这⼀次能够发送 9 个 MSS 大小的数据,变成了线性增⻓。
所以,我们可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了⼀些。
就这么⼀直增长着后,网络就会慢慢进⼊了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进⾏重传。当触发了重传机制,也就进入了「 拥塞发生算法 」。
3. 拥塞发生
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:
- 超时重传
- 快速重传
这两种使用的拥塞发送算法是不同的,接下来分别来说说。
发生超时重传的拥塞发生算法
当发⽣了「超时重传」,则就会使用拥塞发生算法。这个时候,ssthresh 和 cwnd 的值会发生变化:
- ssthresh 设为 cwnd/2 ,
- cwnd 重置为 1
接着,就重新开始慢启动,慢启动是会突然减少数据流的。这真是⼀旦「超时重传」,⻢上回到解放前。但是这种⽅式太激进了,反应也很强烈,会造成⽹络卡顿。
发生快速重传的拥塞发生算法
还有更好的方式,前⾯我们讲过「快速重传算法」。当接收⽅发现丢了⼀个中间包的时候,发送三次前⼀个包的ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为⼤部分没丢,只丢了⼀⼩部分,则 ssthresh 和 cwnd 变化如下:
- cwnd = cwnd/2 ,也就是设置为原来的⼀半;
- ssthresh = cwnd ;
- 进⼊快速恢复算法
4. 快速恢复
快速重传和快速恢复算法⼀般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明⽹络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。正如前⾯所说,进⼊快速恢复之前, cwnd 和 ssthresh 已被更新了:
- cwnd = cwnd/2 ,也就是设置为原来的⼀半;
- ssthresh = cwnd ;
然后,进入快速恢复算法如下:
- 拥塞窗⼝ cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
- 重传丢失的数据包;
- 如果再收到重复的 ACK,那么 cwnd 增加 1;
- 如果收到新数据的 ACK 后,把 cwnd 设置为第⼀步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进⼊拥塞避免状态;
六、TCP粘包问题
什么是粘包?
- 首先要明确,粘包问题中的“包”,是指的应用层的数据包。
- 在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段。
- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
- 但站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
如何解决粘包问题?要解决粘包问题,本质就是要明确报文和报文之间的边界。
- 对于定长的包,保证每次都按固定大小读取即可。
- 对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如HTTP报头当中就包含Content-Length属性,表示正文的长度。
- 对于变长的包,还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可。
UDP是否存在粘包问题?
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层的,有很明确的数据边界。
- 站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况。
- 因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
七、TCP的异常情况
进程终止:当客户端正常访问服务器时,如果客户端进程突然崩溃了,此时建立好的连接会怎么样?
当一个进程退出时,该进程曾经打开的文件描述符都会自动关闭,因此当客户端进程退出时,相当于自动调用了close函数关闭了对应的文件描述符,此时双方操作系统在底层会正常完成四次挥手,然后释放对应的连接资源。也就是说,进程终止时会释放文件描述符,TCP底层仍然可以发送FIN,和进程正常退出没有区别。
机器重启:当客户端正常访问服务器时,如果将客户端主机重启,此时建立好的连接会怎么样?
当我们选择重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。
机器掉电/网线断开:当客户端正常访问服务器时,如果将客户端突然掉线了,此时建立好的连接会怎么样?
当客户端掉线后,服务器端在短时间内无法知道客户端掉线了,因此在服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的。
- 服务器会定期客户端客户端的存在状况,检查对方是否在线,如果连续多次都没有收到ACK应答,此时服务器就会关闭这条连接。
- 此外,客户端也可能会定期向服务器“报平安”,如果服务器长时间没有收到客户端的消息,此时服务器也会将对应的连接关闭。
其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。
参考:https://blog.csdn.net/qq_34827674/article/details/105606205很详细,非常值得学习