注:本文为 “TCP 快速重传原理” 相关文章合辑。
未整理去重。
TCP 的重传机制
guishangppy 于 2022-09-26 20:10:55 发布
我们都知道基于 TCP 协议的传输都是相对稳定和安全的,那么它是通过何种方法保证数据的正确性以及安全性呢,其中之一就是因为 TCP 具有重传机制。由于 TCP 是基于双方连接的,因此需要接收端和发送端保证连接之后才会发送有效信息,所以在介绍 TCP 的重传机制之前先介绍 TCP 是怎样保证连接性的,即TCP 的三次握手。
TCP 的三次握手:三次握手的目的是保证客户端和服务器端都同时具有发送和接收的功能,具体的步骤如下。
1,客户端发送连接请求包 syn 和一个序列号 seq 到服务器,自身进入发送请求状态
2,服务器端接收到客户端发送的 syn 请求包后必须进行确认,并回应一个 syn 包和确认包 ACK 以及序列号 seq+1,自身进入接收请求状态
3,客户端收到服务器的回应之后会发送一个 ACK 确认包,自身进入已连接状态
4,服务器端收到客户端发送的 ACK 确认包后自身进入已连接状态,完成三次握手
在上述描述中客户端是主动进行连接,而服务器端是在收到客户端的连接请求之后被动进入连接
具体解释图如下:
通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。三次握手的最主要目 的是保证连接是双工的,可靠更多的是通过重传机制来保证的,而所有的重传机制都是依赖于序列号 seq 以及确认应答 ACK。所以解释以下什么是序列号和 ACK。
在使用 TCP 进行数据传输时每个消息都有带有 TCP 协议的包头,在包头中就存在 seq 序列号和 ACK 确认包。
seq(Sequence Number):32bits
,表示这个 tcp
包的序列号。tcp
协议拼凑接收到的数据包时,根据 seq
来确定顺序,并且能够确定是否有数据包丢失。**
ack(Acknowledgment Number):32bits
,表示这个包的确认号。首先意味着已经收到对方了多少字节数据,其次告诉对方接下来的包的 seq
要从 ack
确定的数值继续接力。**
在三次握手中:
-
起始包的
seq
都等于0
-
三次握手中的
ack = 对方上一个的 seq+1
-
seq
等于对方上次的ack
号
常见的重传机制主要有以下几种:
超时重传
快速重传
SACK
D-SACK
例如在正常传输过程中过程如下:
客户端将带有用户数据的包发送给服务器,序列号 seq = 1,服务器收到该数据包后进行确认,ACK 的值为数据包的 seq 值 + 1 代表下一个包的序列号,因此 客户端需要发送的下一个数据包的 seq 值为服务器回应的 ACK+1。
超时重传
假如客户端一共发送了五个包,序列号分别为 1,2,3,4,5,假如第三个包在传输过程中失败了那么服务器只收到了 1,2,所以回应的 ACK 为 3,但是下次一收到的包的序列号 seq 为 4,该序列号显然不是服务器所期待的因此便不再回应 ACK 直到发送端发送期待的序列号为止。在发送端这边由于自己已经将序列号 seq = 3 的包发送出去因此一直在期待对方回应 ACK = 4,但是却没有收到,这时双方就开始摆烂了,两个都不发。因此重传就能够打破这种僵局,由发送端再发送一次丢失的包即可。而重发也需要一定的条件,在实际情况中在一定的时间间隔内不管是因为发送端的数据未到达或者是接收端未回应 ACK 都会造成消息的重传。如下图在 RTT 的时间间隔内如果发送端没有收到确认信息就会进行消息重传,如果 RTO 较长就会造成网络延迟,导致接收方在很久之后才会收到对应的包,但是在这个时间段内发送方由于长时间未接收到该包的 ACK,因此会进行重传,就会造成同一个包重复被发送。
这时又出现两种重传的情况:
(1) 发送端重传丢失的包即可
(2) 发送端重传丢失之后的所有包
因为发送端并没有接收到发送失败的那个包之后的 ACK,因此它就会认为所有的包都丢了,所以会将错误后的所有包都进行重传,但实际情况并不是它想的那样,而是只有一个包发生了错误。所以这种方法比较浪费资源,这种是比较简单的超时重传。
快速重传
快速重传不以时间作为重发的判断条件,而是以收到的 ACK 的重复次数作为判断条件。
如果接收端没有收到自己所期望数据包就会一直发送上一次的 ACK,发送三次后发送端便进行重传。例如,发送端发送分别发送 1,2,3,4,5 五个包,假如第二个包由于某种原因丢失了,那么由于第一个包是成功发送的所以接收端回应的 ACK 为 2,但是一直没有收到这个包,因此便会在发送端发送其他包过来时也一直回应 ACK = 2,直到发送了三次,发送端这才直到接收端是没有收到第二个包,于是便会重传,由于 3,4,5,在这个过程中已经发送到了接收端,接收端也已经收到了,所以在第二个包被重传之后接收端便会回应 ACK = 6。
但是也有一个问题就是重传的时候是否只需要重传出错的这个还是重传从错误的这个开始之后的所有数据,因为在收到接收端重复回应的 ACK 时并不知道是对哪一个包的回应。
SACK (selective Acknowledge) 选择性确认
SACK 在快速重传的基础上,接收端会返回最近收到的报文段的序列号范围,这样发送端就知道,哪些数据包已经到达接收端了。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200-299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
DSACK (Duplicate SACK) 重复发送 SACK
DSACK 这个机制是在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了。DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。
由于发送方之前发送的两个包都没有得到回应,当时间到达 RTT 后发送方便进行重发,但是接收方已经接收过该包了只是发送方没有收到 ACK 而已,因此接收方回应的 ACK 为最新数据的 ACK,这时发送方收到回应信息后便知道之前的消息对方是接收到了只是回应的 ACK 丢失了。
网络延迟:
由于 1000-14999 的数据包被网络延迟造成未按时到达,在其他的包到达时接收方便会一直回应 ACK = 1000 表示没收到这个包,因此发送方便会进行重传,但是当网络延迟结束之后 1000-1499 包才到达接收端,这时接收方回复的 ACK 已经到了 3000,明显这个包应该在之前就应该到达了,因此便发送了一个 SACK,因此该 SACK 是 DSACK,用于供发送方判断出现该情况的原因。
TCP - 超时重传和快速重传
置顶 kksilu 已于 2022-09-19 22:06:08 修改
超时重传
重传机制会设定一个 定时器
,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,这就是 超时重传
。
超时重传时间 RTO
假设在重传的情况下,超时时间 RTO 过长或过短时,会发生什么事情呢?
-
当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
-
当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
所以精确的测量超时时间 RTO 的值是非常重要的,这可让我们的重传机制更高效。
那 RTO 该如何设置最高效呢?
设置成略大于报文往返时间 RTT
具体做法:取多个 RTT 的取平均值
比如第一次 RTT 为 500ms,第二次 RTT 为 800ms,那么第三次发送时,各让一步取平均值 RTO 为 650ms。
重传二义性
重传必然会有 重传二义性
重传二义性:当重传一方收到重传请求中的某个请求的应答时,它不能区分该应答对应哪一次请求。
-
如果算作对第一次的应答,就会出现如下图 1 和图 2 中的问题,RTT 时间明显是大于实际值
-
如果算作对第二次的应答,,就会出现图 3 中的问题,RTT 时间明显小于实际值
那么如何解决呢?
在 TCP 头加入 重传序列号
,用序列号来唯一标识这次请求。
如果多次超时呢?—— 超时时间间隔加倍
如果超时重发的数据,再次超时的时候,TCP 的策略是 超时时间间隔加倍
。
也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的 两倍
。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传
超时重传
最大的问题就是太慢了,需要一直等到超时了才重传,
「快速重传」可以解决超时重发的时间等待问题。
快速重传机制是如何工作的?
快重传要求,接收者如果接收到一个 乱序的分组
的话,就返回对前一个正确分组的确认应答,
当发送方连续收到 三个冗余 ACK
,就会马上 快速重传丢失数据
,不必等到超时时间再重传。
图示如下:
超时重传和快速重传的对比
超时重传缺点是太慢了,RTO 的设置不好把握
快速重传解决了超时重传的慢速缺点,但是多发了好几个 ACK 会导致网络更加拥塞
计算机网络:运输层 —— TCP 的超时重传机制
ZachOn1y 于 2024-11-23 11:15:15 发布
TCP 的超时重传
TCP 的超时重传是保证数据可靠传输的重要机制之一
-保证数据可靠性:通过超时重传机制,即使在网络状况不佳,出现数据包丢失等情况时,也能够确保数据最终能够完整、准确地被接收方接收,从而保证了数据传输的可靠性。
-自适应网络变化:能够根据网络的实时状况动态调整超时时间和重传策略,较好地适应不同网络环境下的传输需求,提高网络资源的利用率和数据传输效率。
超时重传时间的选择
发送方在发送数据后会启动一个定时器,若在规定时间内未收到接收方的确认报文(ACK),则认为该数据丢失,进而触发重传机制,重新发送该数据,直至发送成功。
TCP超时重传时间 RTO的选择是 TCP 最复杂的问题之一,超时重传时间 RTO 应略大于往返时间 RTT。
TCP 下层是复杂的因特网环境:主机 A 所发送的报文段可能只经过一个高速率的局域网,也可能经过多个低速率的网络,并且每个 IP 数据报的转发路由还可能不同。
不能直接使用略大于某次测量得到的往返时间 RTT 样本的值作为超时重传时间 RTO。但是,可以利用每次测量得到的 RTT
样本,计算加权平均往返时间
R
T
T
S
RTT_S
RTTS,这样可以得到比较平滑的往返时间。
超时重传时间 RTO 的值应略大于加权平均往返时间
R
T
T
S
RTT_S
RTTS 的值(而不是某个 RTT
样本的值)
[RFC 6298]
建议使用下式来计算超时重传时间 RTO
:
在 TCP 连接建立初期,超时重传时间(RTO
)会被赋予一个初始值。例如,根据最初的简单公式,初始值可能为 1 秒;而依据修正公式,初始 RTO 应为
a
+
4
d
a+4d
a+4d,其中
a
、
d
a、d
a、d 为相关参数,如初始化
a
a
a 为 0,初始化 $ d$ 为 3 秒
如果所测量到的 RTT
样本不正确,那么所计算出的
R
T
T
S
RTT_S
RTTS 和 RTT
, 自然就不正确,进而所计算出的 RTO
也就不正确。然而,RTT
的测量确实是比较复杂的。
通过上述两个例子可以看出:当发送方出现超时重传后,收到确认报文段时,是无法判断出该确认到底是对原数据报文段的确认还是对重传数据报文段的确认,也就是无法准确测量出 RTT,进而无法正确计算 RTO。
基于以上问题,Karn
提出了一种算法,但该算法又引出了新问题,因此要对 Karn 算法
进行修正:
也就是说:报文段每重传一次,就把 RTO
增大一些。典型的做法是将新 RTO
的值取为 I 日 RTO
的2 倍。
在数据正常传输过程中,发送方会根据往返时间(RTT
)不断更新 RTO
的值。RTT
是指数据包从发送方发送出去到接收方返回确认报文所需的时间 。发送方通常使用特定的公式来综合旧的 RTT
值和新测量到的 RTT
值,以更准确地估计当前网络状况下合适的 RTO
。
但在重传情况下,RTO
不使用上述正常传输时的公式计算,而是采用指数退避的方式。比如,当 RTO
为 1 秒时发生数据重传,下一次重传的 RTO
会变为 2 秒,再下次变为 4 秒,以此类推,直至达到 64 秒。
重传策略
-普通超时重传:发送方等待 ACK 的定时器超时后,会重传未被确认的数据报文段。并且,每次重传后会重新启动定时器,等待接收方的确认。
-快速重传:当接收方收到失序的报文段时,会立即发送一个重复的 ACK。如果发送方连续收到三个重复的ACK,就可推断有一个报文段丢失,此时发送方不必等待定时器超时,会立即重传丢失的报文段,这就是快速重传机制。快速重传能够有效减少因等待超时带来的延迟,更快地恢复数据传输。
-选择性确认重传:这是一种 TCP 扩展机制,接收方可以通过选择性确认(SACK
)告知发送方哪些数据包已成功接收,哪些需要重传。发送方根据这些信息,仅重传丢失的数据包,避免了不必要的全量重传,提高了传输效率.
与拥塞控制的关联
相关阅读:
- 计算机网络:运输层 —— TCP 的拥塞控制
https://blog.csdn.net/Zachyy/article/details/143836244
-慢启动阶段:当发生超时重传时,通常意味着网络可能出现了拥塞或其他问题。此时,会将慢启动门限(ssthresh
)设置为当前拥塞窗口(cwnd
)的一半,同时将 cwnd
重新设置为 1 个报文段大小,重新进入慢启动阶段,以较慢的速度重新探测网络的承载能力,避免再次造成网络拥塞
-快速恢复阶段:在快速重传之后,发送方进入快速恢复阶段。此时,ssthresh
同样被设置为当前 cwnd
的一半,而 cwnd
则被设置为 ssthresh
加上 3 倍的报文段大小。之后,发送方每收到一个新的 ACK
,就将 cwnd
增加 1 个报文段大小并发送 1 个分组,直到再次检测到拥塞。
TCP 协议 - TCP 超时重传机制
yunfan188 于 2021-08-04 20:49:48 发布
一、前言
在 TCP 通信中,既要保证在网络正常的情况下提供可靠的交付服务,又要保证在网络异常的情况下也提供可靠的交付服务。而 TCP 的超时重传机制就是解决在网络异常情况下的可靠传输问题的。
二、通过序列号和确认应答提供可靠性
在 TCP 通信中,当发送端的数据到达接收端时,接收端会返回一个已收到消息的通知。这个通知消息叫做确认应答 (ACK)。
ACK (Positive Acknowledgement) 意指已经接收。
2.1 正常数据传输过程
正常数据传输
可以看到,TCP 通过确认应答机制实现可靠的数据传输服务。
2.2 异常数据传输过程
当发送端将数据发出后,会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。否则,数据可能已经丢失。
在一定时间内没有等到确认应答,发送端会认为数据已经丢失,并进行重发。这样,即使有丢包,仍能保证数据达到对端,实现可靠传输。
数据丢失的情况
发送端未收到确认应答,不一定是数据丢失。也可能是对端已经收到数据,只是返回的确认应答在途中丢失。这种情况也会导致发送端因没有收到确认应答,而认为数据没有到达目的地,从而进行重发。
确认应答丢失的情况
此外,也有可能是因为一些其他原因导致确认应答延迟到达,在发送端重发数据后才收到。此时,源发送方只要按照重发机制重发数据即可,但是对于接收方来说,它会反复收到相同的数据。而为了对上层应用提供可靠的传输,必须得放弃重复的数据报文段。
每一次传输数据时,TCP 报文段的首部都会标明本报文段的起始序列号,以便对方确认。当接收方收到数据后,会去查询数据报文段首部中的序列号和数据的长度,将自己下一步应该接收的起始序号作为确认应答回复给发送方。就这样,通过序列号和确认应答号,TCP 就可以实现可靠传输。
序列号
序列号是按顺序给 TCP 报文段的数据部分的每一个字节都标上号码的编号。
序列号的初始值并非是从 0 开始的,而是在建立连接时又随机数生成的。而后面的序号值计算则是对每一字节加 1。
确认号
TCP 首部有一个确认字段,该字段实现 TCP 传输过程中的确认功能,该字段有两个作用:
1)表示确认字段之前的序列号的字节流均已被成功接收。
2)表示期望收到的下一个报文段的序列号,即下一个报文段的数据部分的第一个字节的序号。
###三、TCP 超时重传
超时重传指的是在发送数据报文段后开始计时,到等待确认应答到来的那个时间间隔。如果超过这个时间间隔,仍未收到确认应答,发送端将进行数据重传。这个等待时间称为 RTO (Retransmission Time-Out,超时重传时间)。
还有一个时间叫 RTT (Round Trip Time,报文段的往返时间),这个时间间隔是指数据报文段发出的时间戳与收到确认应答的时间戳的时间之差。
3.1 超时重传时间的确定
超时重传的概念很简单,但是重传时间的选择却是 TCP 最复杂的问题之一。因为网络环境的不同,时间会有差异。如果把超时重传的时间间隔设置得太短,就会引起很多报文段的不必要的重传,使网络负荷增大;如果把超时重传设置得多长,则又使网络的空闲时间增大,降低了传输效率。
那么,TCP 的超时计时器的超时重传时间究竟应设置为多大呢?
TCP 采用了一种自适应的算法,它记录一个报文段发出的时间戳,以及收到相应的确认应答的时间戳。这两个时间戳之差就是该报文段的往返时间 RTT。
算法步骤如下:
(1)计算一个加权平均往返时间 RTTs(这也称为平滑的往返时间,s 表示 smoothed。因为进行的是加权平均,因此得到的结果更加平滑)。
第 1 次测量得到的 RTT 样本值,作为 RTTs 的初始值。从第 2 次开始,新的 RTTs 值使用如下的公式计算得到:
新的 RTTs = (1 - α) × (旧的 RTTs) + α × (新的 RTT 样本)
< 说明 1> 新的 RTT 样本需要重新测量得到。
< 说明 2> 系数 α 的取值范围为 [0, 1)。
< 说明 3> RFC 6298 文档推荐的 α 值为 1/8,即 0.125。
(2)基于计算得到的 RTTs 时间设置超时重传时间 RTO。显然,超时计时器设置的超时重传时间 RTO 应略大于上面得到的加权平均往返时间 RTTs。
RFC 6298 建议使用下式计算 RTO:
RTO = RTTs + 4 × RTTd
< 说明 > RTTd 是 RTT 的偏差的加权平均值,它与 RTTs 和 新的 RTT 样本之差有关。
RFC 6298 建议这样计算 RTTd。当第一次测量时,RTTd 的值取为测量到的 RTT 样本值的一半。在以后的测量中,则使用下式计算加权平均的 RTTd:
新的 RTTd = (1 - β) × (旧的 RTTd) + β × |RTTs - 新的 RTT 样本值 |
< 说明 1> 系数 β 的取值范围为 [0, 1),它的推荐值为 1/4,即 0.25。
3.2 RTT 样本的测量
上面所说的 RTT 样本的测量,实现起来相当复杂。示例如下:
如下图所示,发送方发出一个报文段后,设定的重传时间到了,还没有收到确认应答。于是重传报文段。经过一段时间后,收到了确认报文段。现在的问题是:**如何判定此确认报文段是对先发送的报文段的确认还是对后来重传的报文段的确认?**由于重传的报文段和原来的报文段完全一样,因此源主机在收到确认后,就无法做出正确的判断,而正确的判断对确定加权平均 RTTs 的值关系很大。
若收到的确认是对重传报文段的确认,但却被源主机当成是对原来的报文段的确认,则这样计算出的 RTTs 和超时重传时间 RTO 就会偏大。若后面再发送的报文段又是经过重传后才收到确认报文段,则按照此方法得出的超时重传时间 RTO 就会越来越长。
同样的道理,若收到的确认是对原来的报文段的确认,但被当成是对重传报文段的确认,则由此计算出的 RTTs 和 RTO 都会偏小。这就必然导致报文段过多地重传。这样就有可能使 RTO 的值越来越短。
根据以上所述,Karn 提出了一个算法:在计算加权平均 RTTs 时,只要报文段重传了,就不采用其往返时间样本。这样得出的加权平均 RTTs 和 RTO 就比较准确。
但是,这又可能引起新的问题。设想出现这样的情况:报文段的时延突然增大了很多。因此,在原来得出的重传时间内,不会收到确认报文段。于是就重传报文段。但根据 Karn 算法,不考虑重传的报文段的往返时间样本。这样,超时重传时间就无法更新。
因此需要对 Karn 算法进行修正。方法是:报文段每重传一次,就把超时重传时间 RTO 增大一些。典型的做法是取新的重传时间为旧的重传时间的 2 倍。当不再发生报文段的重传时,再根据上面给出的公式计算超时重传时间 RTO 的值。实践证明,这种策略较为合理。
总之,Karn 算法能够使 TCP 区分开有效的和无效的往返时间样本,从而改进了往返时间 RTT 的估测,使超时重传时间的计算更加合理。
当然,数据不会被无限、反复地重发。当达到一定重发次数后,如果仍然没有任何确认应答返回,就会判断为网络或对端主机发生了异常,TCP 模块就会强制关闭连接,并且通知上层应用通信异常强行终止。
3.3 Linux 系统中与超时重传相关的内核参数
Linux 系统有两个重要的内核参数与 TCP 超时重传相关:/proc/sys/net/ipv4/tcp_retries1 和 /proc/sys/net/ipv4/tcp_retries2。
查看这两个内核参数的默认值:
# sysctl -a |grep -e tcp_retries
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 15
前者指定在底层 IP 接管之前 TCP 最少执行的重传次数。默认值是 3。
后者指定连接放弃前 TCP 最多可以执行的重传次数。默认值是 15。(一般对应 13~30min)。
四、选择确认 SACK
问题:如果接收方收到的报文段无差错,只是未按序到达,中间还缺了一些序号的字节数据,那么能否设法只重传缺少的数据而不重传已经正确到达接收方的数据呢?
SACK (Selective ACKonwledgement,选择确认)
答案是可以的,选择确认就是一种可行的处理方法。下面我们用一个例子来说明选择确认的工作原理。
当 TCP 的接收方在接收对方发送过来的数据字节流的序号不连续时,结果就形成了一些不连续的字节块,如下图所示。
可以看出,序号 1 ~ 1000 已收到了,但序号 1001 ~ 1500 没有收到。接下来的字节流又收到了,可是又缺少了 3000 ~ 3500。再后面从 4501 起又没有收到。也就是说,接收方收到了和前面的字节流不连续的两个字节块。如果这些字节块都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据。
从上图可以看出,和前后字节不连续的每一个字节块都有两个边界:左边界和右边界。因此在图中用四个指针标记这些边界。请注意,第一个字节块的左边界 L1=1501,而右边界 R1 = 3001 而不是 3000。这就是说,左边界指出字节块的第一个字节的序号,但右边界减 1 才是字节块的最后一个字节序号。同理,第二个字节块的左边界 L2=3501,而右边界 R2 = 4501。
我们知道,TCP 报文段的首部没有哪个字段能够提供上述这些字节块的边界信息。RFC 2018 规定,如果要使用选择确认 SACK 功能,那么在建立 TCP 连接时,就要在 TCP 首部的选项字段(即 Options)中加上 “允许 SACK” 的选项,而通信双方必须都事先约定好。如果使用选择确认,那么原来首部中的 “确认号” 字段的用法仍然不变。只是以后在 TCP 报文段的首部中的 “选项” 字段中都增加了 SACK 选项,以便报告收到不连续的字节块的边界。
由于 TCP 首部的 “选项” 可选字段的长度最多只有 40 字节,而指明一个边界就要用掉 4 字节(因为序号有 32 位,需要使用 4 个字节表示),因此在选项中最多只能指明 4 个字节块的边界信息。这是因为 4 个字节块有 8 个边界,因为需要用 8 * 4 =32 个字节来描述。另外还需要两个字节,一个字节用来指明是 SACK 选项,另一个字节是指明这个 SACK 选项要占用多少字节。如果要报告五个字节块的边界信息,那么至少需要 42 个字节。这就超过了选项字段的 40 字节的上限。具体是如何规定的,可以参考 RFC 2018。示例表示如下:
kind = 4:占 1 字节,表示选项类别为 SACK。length:占 1 字节,表示 SACK 选项总共占用的字节数。该长度包括了 kind 字段和 length 字段占据的 2 个字节。
kind = 5:占 1 字节,表示这是 SACK 实际工作的选项。legnth:N 表示字节块数量;每个字节块有两个边界信息,因此需要 8 字节;+2 表示加上 kind 和 length 这两个字节。
在 Linux 系统中,有一个内核参数 /proc/sys/net/ipv4/tcp_sack 用来表示 SACK 选项是否启用或是关闭。默认值是 1,即启用 SACK 选项。
查看该内核参数的命令如下:
# sysctl -a | grep tcp_sack
net.ipv4.tcp_sack = 1
如果需要关闭 SACK 选项,可以使用如下命令修改:
sysctl -w net.ipv4.tcp_sack = 0
或者在 /etc/sysctl.conf 配置文件中永久修改,添加内容如下:
net.ipv4.tcp_sack = 0
参考
《图解 TCP_IP (第 5 版)》第 6 章 - TCP 与 UDP
《计算机网络 (第 7 版 - 谢希仁)》第 5 章 - 运输层
《Linux 高性能服务器编程》第 3 章 - TCP 协议详解
TCP 快速重传为什么是三次冗余 ACK,这个三次是怎么定下来的?
车小胖
两次 duplicated ACK 肯定是乱序造成的!
丢包肯定会造成三次 duplicated ACK!
假定通信双方如下,A 发送 4 个 TCP Segment 给 B,编号如下,N-1 成功到达,因为 A 收到 B 的 ACK (N),其它按照到达顺序,分别收到 ACK (N) 的数目:
A ---------> B
A 方发送顺序 N-1,N,N+1,N+2
B 方到达顺序
N-1,N,N+1,N+2
A 收到 1 个 ACK (N)
N-1,N,N+2,N+1
A 收到 1 个 ACK (N)
N-1,N+1,N,N+2
A 收到 2 个 ACK (N)
N-1,N+1,N+2,N
A 收到 3 个 ACK (N)
N-1,N+2,N,N+1
A 收到 2 个 ACK (N)
N-1,N+2,N+1,N
A 收到 3 个 ACK (N)
如果 N 丢了,没有到达 B
N-1,N+1,N+2
A 收到 3 个 ACK (N)
N-1,N+2,N+1**
A 收到 3 个 ACK (N)
TCP segment 乱序 有 2/5 = 40% 的概率会造成 A 收到三次 duplicated ACK (N);
而如果 N 丢了,则会 100% 概率 A 会收到三次 duplicated ACK (N);
基于以上的统计,当 A 接收到三次 duplicated ACK (N) 启动 Fast Retransmit 算法是合理的,即立马 retransmit N,可以起到 Fast Recovery 的功效,快速修复一个丢包对 TCP 管道的恶劣影响。
而如果 A 接收到二次 duplicated ACK (N),则一定说明是乱序造成的,即然是乱序,说明 数据都到达了 B,B 的 TCP 负责重新排序而已,没有必要 A 再来启动 Fast Retransmit 算法。
补充阅读
--------------------------------------------
TCP segment 乱序的由来
TCP segment 封装在 IP 包里,如果 IP 包乱序,则相应 TCP 也会乱序,乱序的原因一般如下:
1)ECMP 负载均衡
多路径的负载均衡,基于 per-packet load balance,比如 packet 1,3,5… 走路径 1,packet 2,4,6… 走路径 2,很难保证 packet 1 在 packet 2 之前到达目的地。
Per-session load balance 会基于 TCP 五元组来负载均衡,同一个 TCP 会话会走同一条路径,克服多路径造成的乱序。
2)路由器内部流量调度
有些路由器采用多个流量处理单元,比如 packet 1,3,5… 由处理单元 1 来处理,packet 2,4,6… 由处理单元 2 来处理,也很难保证 packet 1 在 packet 2 之前到达目的地。
TCP 接收到乱序的 segment,会放在自己的接收缓冲区,等所有乱序的 segment 都顺利到达,TCP 重新排序,并将数据提交给 application。
乱序的 segment 会占用接收缓冲区,直接造成 B advertised window size 变小,造成对方 A 发送 window 一直在变小,影响 A 发送效率。
即使 A 不快速重传,最后也会由 retransmit timer timeout 超时重传,但这个时候 A 的发送 window 非常小,发送速率也从天上掉到了地下。
----------///--------
@鹿阳
分析的很好,在没有 fast retransmit /recovery 算法之前,重传依靠发送方的 retransmit timeout,就是在 timeout 内如果没有接收到对方的 ACK,默认包丢了,发送方就重传,包的丢失原因 1)包 checksum 出错 2)网络拥塞 3)网络断,包括路由重收敛,但是发送方无法判断是哪一种情况,于是采用最笨的办法,就是将自己的发送速率减半,即 CWND 减为 1/2,这样的方法对 2 是有效的,可以缓解网络拥塞,3 则无所谓,反正网络断了,无论发快发慢都会被丢;但对于 1 来说,丢包是因为偶尔的出错引起,一丢包就对半减速不合理。于是有了 fast retransmit 算法,基于在反向还可以接收到 ACK,可以认为网络并没有断,否则也接收不到 ACK,如果在 timeout 时间内没有接收到 > 2 的 duplicated ACK,则概率大事件为乱序,乱序无需重传,接收方会进行排序工作;而如果接收到三个或三个以上的 duplicated ACK,则大概率是丢包,可以逻辑推理,发送方可以接收 ACK,则网络是通的,可能是 1、2 造成的,先不降速,重传一次,如果接收到正确的 ACK,则一切 OK,流速依然(包出错被丢)。而如果依然接收到 duplicated ACK,则认为是网络拥塞造成的,此时降速则比较合理。
小林 coding
这个其实是一个经验值,有句老话,事不过三。
既然聊到 TCP 重传,我把超时重传、快速重传、sack 重传等也说一遍吧。
TCP 实现可靠传输的方式之一,是通过序列号与确认应答。
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。
但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?
所以 TCP 针对数据包丢失的情况,会用重传机制解决。
接下来说说常见的重传机制:
-
超时重传
-
快速重传
-
SACK
-
D-SACK
超时重传
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据,也就是我们常说的超时重传。
TCP 会在以下两种情况发生超时重传:
-
数据包丢失
-
确认应答丢失
超时时间应该设置为多少呢?
我们先来了解一下什么是 RTT
(Round-Trip Time 往返时延),从下图我们就可以知道:
RTT
指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。
超时重传时间是以 RTO
(Retransmission Timeout 超时重传时间)表示。
假设在重传的情况下,超时时间 RTO
「较长或较短」时,会发生什么事情呢?
上图中有两种超时时间不同的情况:
-
当超时时间RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
-
当超时时间RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
精确的测量超时时间 RTO
的值是非常重要的,这可让我们的重传机制更高效。
根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。
至此,可能大家觉得超时重传时间 RTO
的值计算,也不是很复杂嘛。
好像就是在发送端发包时记下 t0
,然后接收端再把这个 ack
回来时再记一个 t1
,于是 RTT = t1 – t0
。没那么简单,这只是一个采样,不能代表普遍情况。
实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。
我们来看看 Linux 是如何计算 RTO
的呢?
估计往返时间,通常需要采样以下两个:
-
需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。
-
除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。
RFC6289 建议使用以下的公式计算 RTO:
其中 SRTT
是计算平滑的 RTT ,DevRTR
是计算平滑的 RTT 与 最新 RTT 的差距。
在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。别问怎么来的,问就是大量实验中调出来的。
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。
也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?
于是就可以用「快速重传」机制来解决超时重发的时间等待。
快速重传
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 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。
举个例子,假设发送方发了 6 个数据,编号的顺序是 Seq1 ~ Seq6 ,但是 Seq2、Seq3 都丢失了,那么接收方在收到 Seq4、Seq5、Seq6 时,都是回复 ACK2 给发送方,但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的, 那是选择重传 Seq2 一个报文,还是重传 Seq2 之后已发送的所有报文呢(Seq2、Seq3、 Seq4、Seq5、 Seq6) 呢?
-
如果只选择重传 Seq2 一个报文,那么重传的效率很低。因为对于丢失的 Seq3 报文,还得在后续收到三个重复的 ACK3 才能触发重传。
-
如果选择重传 Seq2 之后已发送的所有报文,虽然能同时重传已丢失的 Seq2 和 Seq3 报文,但是 Seq4、Seq5、Seq6 的报文是已经被接收过了,对于重传 Seq4 ~Seq6 折部分数据相当于做了一次无用功,浪费资源。
可以看到,不管是重传一个报文,还是重传已发送的报文,都存在问题。
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK
方法。
SACK 方法
还有一种实现重传机制的方式叫:SACK
( Selective Acknowledgment),选择性确认。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
如果要支持 SACK
,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack
参数打开这个功能(Linux 2.4 后默认打开)。
# Duplicate SACK
Duplicate SACK 又称 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
下面举例两个栗子,来说明 D-SACK
的作用。
栗子一号:ACK 丢包
-
「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
-
于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着
D-SACK
。 -
这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。
栗子二号:网络延时
-
数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。
-
而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;
-
所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。
-
这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。
可见,D-SACK
有这么几个好处:
-
可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
-
可以知道是不是「发送方」的数据包被网络延迟了;
-
可以知道网络中是不是把「发送方」的数据包给复制了;
在 Linux 下可以通过 net.ipv4.tcp_dsack
参数开启 / 关闭这个功能(Linux 2.4 后默认打开)。
“三次握手,四次挥手” 你真的懂吗?
Stefno
记得刚毕业找工作面试的时候,经常会被问到:你知道 “3 次握手,4 次挥手” 吗?这时候我会 “胸有成竹” 地 “背诵” 前期准备好的 “答案”,第一次怎么怎么,第二次…… 答完就没有下文了,面试官貌似也没有深入下去的意思,深入下去我也不懂,皆大欢喜!
作为程序员,要有 “刨根问底” 的精神。知其然,更要知其所以然。这篇文章希望能抽丝剥茧,还原背后的原理。
什么是 “3 次握手,4 次挥手”
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的 “连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 ip 地址、端口号等。
TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 4 次挥手来关闭一个连接。
TCP 服务模型
在了解了建立连接、关闭连接的 “三次握手和四次挥手” 后,我们再来看下 TCP 相关的东西。
一个 TCP 连接由一个 4 元组构成,分别是两个 IP 地址和两个端口号。一个 TCP 连接通常分为三个阶段:启动、数据传输、退出(关闭)。
当 TCP 接收到另一端的数据时,它会发送一个确认,但这个确认不会立即发送,一般会延迟一会儿。ACK 是累积的,一个确认字节号 N 的 ACK 表示所有直到 N 的字节(不包括 N)已经成功被接收了。这样的好处是如果一个 ACK 丢失,很可能后续的 ACK 就足以确认前面的报文段了。
一个完整的 TCP 连接是双向和对称的,数据可以在两个方向上平等地流动。给上层应用程序提供一种 双工服务
。一旦建立了一个连接,这个连接的一个方向上的每个 TCP 报文段都包含了相反方向上的报文段的一个 ACK。
序列号的作用是使得一个 TCP 接收端可丢弃重复的报文段,记录以杂乱次序到达的报文段。因为 TCP 使用 IP 来传输报文段,而 IP 不提供重复消除或者保证次序正确的功能。另一方面,TCP 是一个字节流协议,绝不会以杂乱的次序给上层程序发送数据。因此 TCP 接收端会被迫先保持大序列号的数据不交给应用程序,直到缺失的小序列号的报文段被填满。
TCP 头部
源端口和目的端口在 TCP 层确定双方进程,序列号表示的是报文段数据中的第一个字节号,ACK 表示确认号,该确认号的发送方期待接收的下一个序列号,即最后被成功接收的数据字节序列号加 1,这个字段只有在 ACK 位被启用的时候才有效。
当新建一个连接时,从客户端发送到服务端的第一个报文段的 SYN 位被启用,这称为 SYN 报文段,这时序列号字段包含了在本次连接的这个方向上要使用的第一个序列号,即初始序列号 ISN
,之后发送的数据是 ISN 加 1,因此 SYN 位字段会 消耗
一个序列号,这意味着使用重传进行可靠传输。而不消耗序列号的 ACK 则不是。
头部长度(图中的数据偏移)以 32 位字为单位,也就是以 4bytes 为单位,它只有 4 位,最大为 15,因此头部最大长度为 60 字节,而其最小为 5,也就是头部最小为 20 字节(可变选项为空)。
ACK —— 确认,使得确认号有效。 RST —— 重置连接(经常看到的 reset by peer)就是此字段搞的鬼。 SYN —— 用于初如化一个连接的序列号。 FIN —— 该报文段的发送方已经结束向对方发送数据。
当一个连接被建立或被终止时,交换的报文段只包含 TCP 头部,而没有数据。
状态转换
三次握手和四次挥手的状态转换如下图。
为什么要 “三次握手,四次挥手”
三次握手
换个易于理解的视角来看为什么要 3 次握手。
客户端和服务端通信前要进行连接,“3 次握手” 的作用就是 双方都能明确自己和对方的收、发能力是正常的
。
第一次握手
:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手
:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。 从客户端的视角来看,我接到了服务端发送过来的响应数据包,说明服务端接收到了我在第一次握手时发送的网络包,并且成功发送了响应数据包,这就说明,服务端的接收、发送能力正常。而另一方面,我收到了服务端的响应数据包,说明我第一次发送的网络包成功到达服务端,这样,我自己的发送和接收能力也是正常的。
第三次握手
:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。 第一、二次握手后,服务端并不知道客户端的接收能力以及自己的发送能力是否正常。而在第三次握手时,服务端收到了客户端对第二次握手作的回应。从服务端的角度,我在第二次握手时的响应数据发送出去了,客户端接收到了。所以,我的发送能力是正常的。而客户端的接收能力也是正常的。
经历了上面的三次握手过程,客户端和服务端都确认了自己的接收、发送能力是正常的。之后就可以正常通信了。
每次都是接收到数据包的一方可以得到一些结论,发送的一方其实没有任何头绪。我虽然有发包的动作,但是我怎么知道我有没有发出去,而对方有没有接收到呢?
而从上面的过程可以看到,最少是需要三次握手过程的。两次达不到让双方都得出自己、对方的接收、发送能力都正常的结论。其实每次收到网络包的一方至少是可以得到:对方的发送、我方的接收是正常的。而每一步都是有关联的,下一次的 “响应” 是由于第一次的 “请求” 触发,因此每次握手其实是可以得到额外的结论的。比如第三次握手时,服务端收到数据包,表明看服务端只能得到客户端的发送能力、服务端的接收能力是正常的,但是结合第二次,说明服务端在第二次发送的响应包,客户端接收到了,并且作出了响应,从而得到额外的结论:客户端的接收、服务端的发送是正常的。
用表格总结一下:
四次挥手
TCP 连接是双向传输的对等的模式,就是说双方都可以同时向对方发送或接收数据。当有一方要关闭连接时,会发送指令告知对方,我要关闭连接了。这时对方会回一个 ACK,此时一个方向的连接关闭。但是另一个方向仍然可以继续传输数据,等到发送完了所有的数据后,会发送一个 FIN 段来关闭此方向上的连接。接收方发送 ACK 确认关闭连接。注意,接收到 FIN 报文的一方只能回复一个 ACK, 它是无法马上返回对方一个 FIN 报文段的,因为结束数据传输的 “指令” 是上层应用层给出的,我只是一个 “搬运工”,我无法了解 “上层的意志”
。
“三次握手,四次挥手” 怎么完成?
其实 3 次握手的目的并不只是让通信双方都了解到一个连接正在建立,还在于利用数据包的选项来传输特殊的信息,交换初始序列号 ISN。
3 次握手是指发送了 3 个报文段,4 次挥手是指发送了 4 个报文段。注意,SYN 和 FIN 段都是会利用重传进行可靠传输的。
三次握手
- 客户端发送一个 SYN 段,并指明客户端的初始序列号,即 ISN ©.
- 服务端发送自己的 SYN 段作为应答,同样指明自己的 ISN (s)。为了确认客户端的 SYN,将 ISN ©+1 作为 ACK 数值。这样,每发送一个 SYN,序列号就会加 1. 如果有丢失的情况,则会重传。
- 为了确认服务器端的 SYN,客户端将 ISN (s)+1 作为返回的 ACK 数值。
四次挥手
- 客户端发送一个 FIN 段,并包含一个希望接收者看到的自己当前的序列号 K. 同时还包含一个 ACK 表示确认对方最近一次发过来的数据。 2. 服务端将 K 值加 1 作为 ACK 序号值,表明收到了上一个包。这时上层的应用程序会被告知另一端发起了关闭操作,通常这将引起应用程序发起自己的关闭操作。 3. 服务端发起自己的 FIN 段,ACK=K+1, Seq=L 4. 客户端确认。ACK=L+1
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
这是因为服务端在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,把 ACK 和 SYN 放在一个报文里发送给客户端。而关闭连接时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方 ACK 和 FIN 一般都会分开发送。
“三次握手,四次挥手” 进阶
ISN
三次握手的一个重要功能是客户端和服务端交换 ISN (Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果 ISN 是固定的,攻击者很容易猜出后续的确认号。
ISN = M + F (localhost, localport, remotehost, remoteport)
M 是一个计时器,每隔 4 毫秒加 1。 F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 hash 算法不能被外部轻易推算得出。
序列号回绕
因为 ISN 是随机的,所以序列号容易就会超过 2^31-1. 而 tcp 对于丢包和乱序等问题的判断都是依赖于序列号大小比较的。此时就出现了所谓的 tcp 序列号回绕(sequence wraparound)问题。怎么解决?
/*
* The next routines deal with comparing 32 bit unsigned ints
* and worry about wraparound (automatic with unsigned arithmetic).
*/
static inline int before (__u32 seq1, __u32 seq2)
{
return (__s32)(seq1-seq2) < 0;
}
#define after (seq2, seq1) before (seq1, seq2)
上述代码是内核中的解决回绕问题代码。**s32 是有符号整型的意思,而 **u32 则是无符号整型。序列号发生回绕后,序列号变小,相减之后,把结果变成有符号数了,因此结果成了负数。
假设 seq1=255, seq2=1(发生了回绕)。
seq1 = 1111 1111 seq2 = 0000 0001
我们希望比较结果是
seq1 - seq2=
1111 1111
-0000 0001
-----------
1111 1110
由于我们将结果转化成了有符号数,由于最高位是 1,因此结果是一个负数,负数的绝对值为
0000 0001 + 1 = 0000 0010 = 2
因此 seq1 - seq2 < 0
syn flood 攻击
最基本的 DoS 攻击就是利用合理的服务请求来占用过多的服务资源,从而使合法用户无法得到服务的响应。syn flood 属于 Dos 攻击的一种。
如果恶意的向某个服务器端口发送大量的 SYN 包,则可以使服务器打开大量的半开连接,分配 TCB(Transmission Control Block), 从而消耗大量的服务器资源,同时也使得正常的连接请求无法被相应。当开放了一个 TCP 端口后,该端口就处于 Listening 状态,不停地监视发到该端口的 Syn 报文,一 旦接收到 Client 发来的 Syn 报文,就需要为该请求分配一个 TCB,通常一个 TCB 至少需要 280 个字节,在某些操作系统中 TCB 甚至需要 1300 个字节,并返回一个 SYN ACK 命令,立即转为 SYN-RECEIVED 即半开连接状态。系统会为此耗尽资源。
常见的防攻击方法有:
无效连接的监视释放
监视系统的半开连接和不活动连接,当达到一定阈值时拆除这些连接,从而释放系统资源。这种方法对于所有的连接一视同仁,而且由于 SYN Flood 造成的半开连接数量很大,正常连接请求也被淹没在其中被这种方式误释放掉,因此这种方法属于入门级的 SYN Flood 方法。
延缓 TCB 分配方法
消耗服务器资源主要是因为当 SYN 数据报文一到达,系统立即分配 TCB,从而占用了资源。而 SYN Flood 由于很难建立起正常连接,因此,当正常连接建立起来后再分配 TCB 则可以有效地减轻服务器资源的消耗。常见的方法是使用 Syn Cache 和 Syn Cookie 技术。
Syn Cache 技术
系统在收到一个 SYN 报文时,在一个专用 HASH 表中保存这种半连接信息,直到收到正确的回应 ACK 报文再分配 TCB。这个开销远小于 TCB 的开销。当然还需要保存序列号。
Syn Cookie 技术
Syn Cookie 技术则完全不使用任何存储资源,这种方法比较巧妙,它使用一种特殊的算法生成 Sequence Number,这种算法考虑到了对方的 IP、端口、己方 IP、端口的固定信息,以及对方无法知道而己方比较固定的一些信息,如 MSS (Maximum Segment Size,最大报文段大小,指的是 TCP 报文的最大数据报长度,其中不包括 TCP 首部长度。)、时间等,在收到对方 的 ACK 报文后,重新计算一遍,看其是否与对方回应报文中的(Sequence Number-1)相同,从而决定是否分配 TCB 资源。
使用 SYN Proxy 防火墙
一种方式是防止墙 dqywb 连接的有效性后,防火墙才会向内部服务器发起 SYN 请求。防火墙代服务器发出的 SYN ACK 包使用的序列号为 c, 而真正的服务器回应的序列号为 c’, 这样,在每个数据报文经过防火墙的时候进行序列号的修改。另一种方式是防火墙确定了连接的安全后,会发出一个 safe reset 命令,client 会进行重新连接,这时出现的 syn 报文会直接放行。这样不需要修改序列号了。但是,client 需要发起两次握手过程,因此建立连接的时间将会延长。
连接队列
在外部请求到达时,被服务程序最终感知到前,连接可能处于 SYN_RCVD 状态或是 ESTABLISHED 状态,但还未被应用程序接受。
对应地,服务器端也会维护两种队列,处于 SYN_RCVD 状态的半连接队列,而处于 ESTABLISHED 状态但仍未被应用程序 accept 的为全连接队列。如果这两个队列满了之后,就会出现各种丢包的情形。
查看是否有连接溢出
netstat -s | grep LISTEN
半连接队列满了
在三次握手协议中,服务器维护一个半连接队列,该队列为每个客户端的 SYN 包开设一个条目 (服务端在接收到 SYN 包的时候,就已经创建了 request_sock 结构,存储在半连接队列中),该条目表明服务器已收到 SYN 包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于 Syn_RECV 状态,当服务器收到客户的确认包时,删除该条目,服务器进入 ESTABLISHED 状态。
目前,Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s, 2s, 4s, 8s, 16s, 总共 31s, 称为
指数退避
,第 5 次发出后还要等 32s 才知道第 5 次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s, TCP 才会把断开这个连接。由于,SYN 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给 Server (俗称 SYN flood 攻击),用于耗尽 Server 的 SYN 队列。对于应对 SYN 过多的问题,linux 提供了几个 TCP 参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。
全连接队列满
当第三次握手时,当 server 接收到 ACK 包之后,会进入一个新的叫 accept 的队列。
当 accept 队列满了之后,即使 client 继续向 server 发送 ACK 的包,也会不被响应,此时 ListenOverflows+1,同时 server 通过 tcp_abort_on_overflow 来决定如何返回,0 表示直接丢弃该 ACK,1 表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout
或者 connection reset by peer
。另外,tcp_abort_on_overflow 是 0 的话,server 过一段时间再次发送 syn+ack 给 client(也就是重新走握手的第二步),如果 client 超时等待比较短,就很容易异常了。而客户端收到多个 SYN ACK 包,则会认为之前的 ACK 丢包了。于是促使客户端再次发送 ACK ,在 accept 队列有空闲的时候最终完成连接。若 accept 队列始终满员,则最终客户端收到 RST 包(此时服务端发送 syn+ack 的次数超出了 tcp_synack_retries)。
服务端仅仅只是创建一个定时器,以固定间隔重传 syn 和 ack 到服务端
命令
netstat -s 命令
[root@server ~]# netstat -s | egrep “listen|LISTEN” 667399 times the listen queue of a socket overflowed 667399 SYNs to LISTEN sockets ignored
比如上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。
[root@server ~]# netstat -s | grep TCPBacklogDrop 查看 Accept queue 是否有溢出
ss 命令
[root@server ~]# ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 :6379 : LISTEN 0 128 :22 : 如果 State 是 listen 状态,Send-Q 表示第三列的 listen 端口上的全连接队列最大为 50,第一列 Recv-Q 为全连接队列当前使用了多少。 非 LISTEN 状态中 Recv-Q 表示 receive queue 中的 bytes 数量;Send-Q 表示 send queue 中的 bytes 数值。
小结
当外部连接请求到来时,TCP 模块会首先查看 max_syn_backlog,如果处于 SYN_RCVD 状态的连接数目超过这一阈值,进入的连接会被拒绝。根据 tcp_abort_on_overflow 字段来决定是直接丢弃,还是直接 reset.
从服务端来说,三次握手中,第一步 server 接受到 client 的 syn 后,把相关信息放到半连接队列中,同时回复 syn+ack 给 client. 第三步当收到客户端的 ack, 将连接加入到全连接队列。
一般,全连接队列比较小,会先满,此时半连接队列还没满。如果这时收到 syn 报文,则会进入半连接队列,没有问题。但是如果收到了三次握手中的第 3 步 (ACK),则会根据 tcp_abort_on_overflow 字段来决定是直接丢弃,还是直接 reset. 此时,客户端发送了 ACK, 那么客户端认为三次握手完成,它认为服务端已经准备好了接收数据的准备。但此时服务端可能因为全连接队列满了而无法将连接放入,会重新发送第 2 步的 syn+ack, 如果这时有数据到来,服务器 TCP 模块会将数据存入队列中。一段时间后,client 端没收到回复,超时,连接异常,client 会主动关闭连接。
“三次握手,四次挥手”redis 实例分析
- 我在 dev 机器上部署 redis 服务,端口号为 6379,
- 通过 tcpdump 工具获取数据包,使用如下命令
tcpdump -w /tmp/a.cap port 6379 -s0
-w 把数据写入文件,-s0 设置每个数据包的大小默认为 68 字节,如果用 - S 0 则会抓到完整数据包
- 在 dev2 机器上用 redis-cli 访问 dev:6379, 发送一个 ping, 得到回复 pong
- 停止抓包,用 tcpdump 读取捕获到的数据包
tcpdump -r /tmp/a.cap -n -nn -A -x| vim -
(-x 以 16 进制形式展示,便于后面分析)
共收到了 7 个包。
抓到的是 IP 数据包,IP 数据包分为 IP 头部和 IP 数据部分,IP 数据部分是 TCP 头部加 TCP 数据部分。
IP 的数据格式为:
它由固定长度 20B + 可变长度构成。
10:55:45.662077 IP dev2.39070 > dev.6379: Flags [S], seq 4133153791, win 29200, options [mss 1460,sackOK,TS val 2959270704 ecr 0,nop,wscale 7], length 0
0x0000: 4500 003c 08cf 4000 3606 14a5 0ab3 b561
0x0010: 0a60 5cd4 989e 18eb f65a ebff 0000 0000
0x0020: a002 7210 872f 0000 0204 05b4 0402 080a
0x0030: b062 e330 0000 0000 0103 0307
对着 IP 头部格式,来拆解数据包的具体含义。
剩余的数据部分即为 TCP 协议相关的。TCP 也是 20B 固定长度 + 可变长度部分。
可变长度部分,协议如下:
这样第一个包分析完了。dev2 向 dev 发送 SYN 请求。也就是三次握手中的第一次了。
SYN seq (c)=4133153791
第二个包,dev 响应连接,ack=4133153792. 表明 dev 下次准备接收这个序号的包,用于 tcp 字节注的顺序控制。dev(也就是 server 端)的初始序号为 seq=4264776963, syn=1. SYN ack=seq (c)+1 seq (s)=4264776963
第三个包,client 包确认,这里使用了相对值应答。seq=4133153792, 等于第二个包的 ack. ack=4264776964. ack=seq (s)+1, seq=seq (c)+1
至此,三次握手完成。接下来就是发送 ping 和 pong 的数据了。
接着第四个包。
10:55:48.090073 IP dev2.39070 > dev.6379: Flags [P.], seq 1:15, ack 1, win 229, options [nop,nop,TS val 2959273132 ecr 3132256230], length 14
0x0000: 4500 0042 08d1 4000 3606 149d 0ab3 b561
0x0010: 0a60 5cd4 989e 18eb f65a ec00 fe33 5504
0x0020: 8018 00e5 4b5f 0000 0101 080a b062 ecac
0x0030: bab2 6fe6 2a31 0d0a 2434 0d0a 7069 6e67
0x0040: 0d0a
tcp 首部长度为 32B, 可选长度为 12B. IP 报文的总长度为 66B, 首部长度为 20B, 因此 TCP 数据部分长度为 14B. seq=0xf65a ec00=4133153792 ACK, PSH. 数据部分为 2a31 0d0a 2434 0d0a 7069 6e67 0d0a
0x2a31 -> *1
0x0d0a -> \r\n
0x2434 -> $4
0x0d0a -> \r\n
0x7069 0x6e67 -> ping
0x0d0a -> \r\n
dev2 向 dev 发送了 ping 数据,第四个包完毕。
第五个包,dev2 向 dev 发送 ack 响应。 序列号为 0xfe33 5504=4264776964, ack 确认号为 0xf65a ec0e=4133153806=(4133153792+14).
第六个包,dev 向 dev2 响应 pong 消息。序列号 fe33 5504,确认号 f65a ec0e, TCP 头部可选长度为 12B, IP 数据报总长度为 59B, 首部长度为 20B, 因此 TCP 数据长度为 7B. 数据部分 2b50 4f4e 470d 0a, 翻译过来就是 +PONG\r\n
.
至此,Redis 客户端和 Server 端的三次握手过程分析完毕。
欢迎一起交流~~
参考
【redis】https://segmentfault.com/a/1190000015044878
【tcp option】https://blog.csdn.net/wdscq1234/article/details/52423272
【[滑动窗口] https://www.zhihu.com/question/32255109
【全连接队列】http://jm.taobao.org/2017/05/25/525-1/
【client fooling】 https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071
【backlog RECV_Q】http://blog.51cto.com/59090939/1947443
【定时器】https://www.cnblogs.com/menghuanbiao/p/5212131.html
【队列图示】https://www.itcodemonkey.com/article/5834.html
【tcp flood 攻击】https://www.cnblogs.com/hubavyn/p/4477883.html
【MSS MTU】https://blog.csdn.net/LoseInVain/article/details/5369426
编辑于 2020-08-21 17:37
via:
-
TCP 的重传机制 - CSDN 博客
https://blog.csdn.net/guishangppy/article/details/127055770 -
TCP - 超时重传和快速重传 - CSDN 博客
https://blog.csdn.net/qq_40337086/article/details/112544412 -
计算机网络:运输层 —— TCP 的超时重传机制_tcp 超时重传机制 - CSDN 博客
https://blog.csdn.net/Zachyy/article/details/143988973 -
TCP 协议 - TCP 超时重传机制 - CSDN 博客
-
TCP 流量控制与拥塞控制 - CSDN 博客
https://blog.csdn.net/Drizzlejj/article/details/109293790 -
TCP 协议中的丢包检测与重传策略 - CSDN 博客
https://blog.csdn.net/weixin_41374218/article/details/122225990 -
TCP 快速重传为什么是三次冗余 ACK,这个三次是怎么定下来的? - 知乎 车小胖 编辑于 2020-08-21 17:37
https://www.zhihu.com/question/21789252 -
“三次握手,四次挥手” 你真的懂吗? - 知乎 Stefno 发布于 2022-10-27 09:33
https://zhuanlan.zhihu.com/p/53374516