详细说一下TCP的三次握手机制
TCP的三次握手机制是为了在两个主机之间建立可靠的连接,这个机制确保两端的通信是同步的,并且在开始传输数据前,双方都做好了要通信的准备。
说说SYN的概念?
SYN 是 TCP 协议中用来建立连接的一个标志位,全称为 Synchronize Sequence Numbers,也就是同步序列编号。 SYN设置为1标识启动序列号同步过程,你可以把发送 SYN 包理解为打电话时说:“喂,你好,我想和你通话,我们从这里开始记录对话内容吧!” 这个“开始记录对话内容”就类似于同步序列号的过程。
TCP握手为什么规定必须是三次?为什么不能是两次?四次?
三次握手的目的是建立一个可靠的连接,双方都准备好了要进行通信,并同步双方的序列号。
为什么TCP握手不能是两次?
服务器回复的ACK丢失,客户端没有确认机制,导致服务器单方面认为连接成功
还有一种情况是:由于网络阻塞,一个旧的、延迟的连接请求(SYN=1且序列号为旧的)被服务器接受,导致服务器错误地开启一个不再需要的连接。
为什么不是四次?
三次已经足够创建可靠连接了
什么是泛洪攻击?
攻击者发送大量伪造的连接请求,导致服务器资源耗尽,无法处理正常的连接请求。也叫半连接服务拒绝。
所谓的半连接就是指在 TCP 的三次握手过程中,当服务器接收到来自客户端的第一个 SYN 包后,它会回复一个 SYN-ACK 包,此时连接处于“半开”状态,因为连接的建立还需要客户端发送最后一个 ACK 包。
如果让你重新设计,你会如何重新设计?
引入SYN Cookies,这种技术通过在 SYN-ACK 响应中编码连接信息,并作为初始序列号返回给客户端,从而在不占用大量资源的情况下验证客户端。发送完 SYN-ACK
后,服务器丢弃关于这个 SYN
请求的任何特定状态信息。它就像“忘记”了这个请求一样,因为它把所有需要恢复的信息都“藏”在了发给客户端的SYN Cookie里。服务器在收到最终的 ACK
并成功验证Cookie之前不分配连接状态。
三次握手中每一次没收到报文会发生什么?
第一次服务端未收到SYN报文
客户端第一次握手发送SYN报文后,如果在一定时间内没有收到服务端的回应,则会认为本次请求失败,重发一个SYN报文,如果仍然没有回应,会重复这个过程,直到发送次数超过最大重传次数限制,就会返回连接建立失败。
第二次客户端未收到服务端发送的SYN-ACK报文
客户端重传SYN / 服务端重传ACK-SYN,直到次数限制
第三次服务端未收到客户端的ACK
服务端重传ACK-SYN,如果重试次数超过限制,则 accept()调用返回-1。客户端会误认为连接以及建立了,开始发送数据,但此时服务端已经丢弃本次连接,服务端接收到客户端发送来的数据时会发送 RST 报文给客户端,消除客户端单方面建立连接的状态
如果服务端的重试次数还没有超过限制,仍然处于半连接状态,那么此时如果收到服务器发送的数据(数据包里有ACK且seq = y+1),则会视为握手的隐式完成
第三次握手可以携带数据?
可以,因为在客户端第三次握手发出时已经是ESTABLISHED状态,只要seq = x + 1就行。
第一次握手不能携带数据是出于安全的考虑,因为如果允许携带数据,攻击者每次在 SYN 报文中携带大量数据,就会导致服务端消耗更多的时间和空间去处理这些报文,会造成 CPU 和内存的消耗。
了解TCP半连接状态吗?
如果服务器回复了 SYN-ACK,但客户端还没有回复 ACK,该连接将一直保留在半连接队列中,直到超时或被拒绝。SYN_RCVD
状态确实一个“半连接”状态。
说说半连接队列?
TCP 进入三次握手前,服务端会从 CLOSED 状态变为 LISTEN 状态, 同时在内部创建了两个队列:半连接队列(SYN 队列)和全连接队列(ACCEPT 队列)。
顾名思义,半连接队列存放的是三次握手未完成的连接,全连接队列存放的是完成三次握手的连接。
- TCP 三次握手时,客户端发送 SYN 到服务端,服务端收到之后,便回复 ACK 和 SYN,状态由 LISTEN 变为 SYN_RCVD,此时这个连接就被推入了 SYN 队列,即半连接队列。
- 当客户端回复 ACK, 服务端接收后,三次握手就完成了。这时连接会等待被具体的应用取走,在被取走之前,它被推入 ACCEPT 队列,即全连接队列。
说说TCP四次挥手的过程
- 客户端向服务端发送FIN请求,表示客户端已经没有数据要发送了,并进入FIN_WAIT_1状态
- 服务端收到FIN请求后进入CLOSE_WAIT状态,并回复客户端ACK,表示收到服务端的FIN请求,同时服务端继续处理未处理完的数据,客户端收到FIN数据包后进入FIN_WAIT_2状态
- 服务端向客户端发送FIN数据包,进入LAST_ACK状态,表示服务端的数据已经全部处理完毕,没有进一步的数据发送了
- 客户端回复服务端ACK,表示收到服务端的FIN数据包,客户端进入TIME_WAIT状态等待2MSL后确保服务端收到ACK请求,然后关闭连接,服务端收到ACK后立即关闭连接
TCP挥手为什么需要四次?
因为TCP是全双工的通信协议,数据的发送和接受需要一来一回,优雅地关闭两个方向的数据流,确保双方都能正确关闭连接
TCP挥手过程中客户端为什么需要等待2MSL才关闭连接?
- 保证客户端的最后一个ACK能够到达服务端:客户端发送的ACK报文的最大生存时间是1MSL,服务端由于没有收到ACK,重发的FIN+ACK报文的最大生存时间也是1MSL,客户端就能在 2MSL 时间内(超时 + 1MSL 传输)收到这个重传的 FIN+ACK 报文段。接着客户端重传一次ACK报文,重新启动 2MSL 计时器。最后,客户端和服务器都正常进入到 CLOSED 状态。2MSL假设了一个比较坏的情况:客户端最后一次ACK经历了其最大生存时间1MSL后还没有发送到服务端,死亡,这是服务端才刚刚经历完RTO发出重传的FIN,然后这个FIN又经历了最大生存时间1MSL后才到达客户端
- 防止“已失效的连接请求报文段”出现在后续的新连接中:因为任何报文段在网络中的最大生存时间是MSL,一来一回就是2MSL的覆盖范围,确保双向的旧数据包都已失效
保活计时器有什么用?
除了TIME_WAIT阶段的等待重传计时器外,保活计时器是为了保证连接的某一方异常故障,无法再接受和发送消息时,另一方不会陷入无限器的等待,而是等待一段时间后(通常是两小时)发送探测报文段(每隔75秒),若连续十个报文段都都无响应,则关闭连接。
CLOSED-WAIT和TIME-WAIT的状态和意义?
- CLOSED-WAIT是指服务器收到客户端发来的FIN报文后,还不能立即关闭连接,进入最后的收尾工作,发送完未处理的数据。
- TIME-WAIT是指客户端发送完最后一个ACK报文后,进入2MLS的等待状态,一是确保ACK报文丢失的情况下能够收到服务端重传的FIN,二是确保旧的报文段全部死亡,避免影响下一次连接。
服务端TIME-WAIT状态过多会造成什么问题?
第⼀是内存资源占⽤,虽然该连接已经不会再用于数据传输,2MSL内仍然会占用内存;
第⼆是对端⼝资源的占⽤,⼀个 TCP 连接⾄少消耗⼀个本地端⼝;
如何解决?
- 服务器可以设置SO_REUSEADDR告诉内核,如果端口被占用,但是TCP连接处于TIME_WAIT状态时,可以重用端口。
- 可以使用长连接的方式来减少TCP的连接和断开,长连接的业务里往往不需要考虑TIME_WAIT
说说TCP报文头部的格式?
一个TCP报文段由头部Header和数据两部分组成,头部包含了确保数据可靠传输的各种控制信息,比如序列号、确认号、窗口大小。
- 源端口号:16位
- 目标端口号:16位
- 序列号:32位,用于标识发送者发送的数据字节流的第一个字节的顺序号,确保数据按顺序接收。
- 确认号:32位,如果ACK设置为1则有效,表示接收端 TCP 希望收到的下一个序列号。
- 首部长度/数据偏移:4位, 用于标识报文数据开始的位置(偏移首部长度)。
- 保留:6位,为将来的使用预留,目前必须为0
- 控制位:6位,包括URG(紧急指针字段是否有效)、PSH(提示接收方应尽快将数据交给应用层,不要为了等待额外数据缓冲)ACK(确认字段是否有效)、RST(重置连接)、SYN(建立连接时用于开始同步序列号)、FIN(结束发送数据)
- 窗口大小:16位,用于流量控制,表示接收端还可以接收多少字节数(基于接收缓冲区的大小)
- 校验和:16位,用于检查数据在传输过程中是否发生了改变
- 紧急指针:16位,当URG标志位设置为1时,用来标识紧急数据最后一个字节的位置
TCP为什么可靠?
首先是三次握手四次挥手确保连接的可靠性,然后通过:
- 校验和:校验范围为一个伪首部,首部和数据,用于检测TCP报文在传输过程中的任何变化,如果接收方检测到校验和错误,则丢弃该报文段。
- 序列号/确认机制:TCP将数据分为多个小段,每个小段都有自己的序列号,保证数据的顺序和完整性,如果超时没有收到接收方的确认应答,会重传数据。
- 流量控制:接收方会告诉发送方自己剩余的窗口大小(和接收缓冲区的大小有关),发送方会动态的调整数据的传输速率,避免缓冲区溢出丢包。
- 拥塞控制:TCP采用慢启动策略,一开始发的少,然后逐步增加,当检测到网络拥塞时,降低发送速率。拥塞缓解后,发送速率恢复。
来保证数据的可靠性。
说说TCP的滑动窗口?
TCP发送一个报文段,如果没有滑动窗口,那么发送方必须收到ACK确认后才能继续发送下一个报文段,这样的方式效率比较低,所以引入滑动窗口,根据接收方返回的win窗口大小,也就是接收端的接收缓存空间的剩余大小,只要发送方发送的数据小于接收方剩余空间大小,无需等待ACK便可以继续发送,如果接收窗口 win 变为 0,发送方停止发送,开启一个定时任务,每隔一段时间,就去询问接受方,直到 win 大于 0,才继续开始发送。这样就可以控制发送方发送数据的速度,从而达到流量控制的目的。
分为发送窗口和接收窗口。
发送窗口:
SND.WND表示发送窗口的大小,SND.NXT指向下一个可发送的位置 ,SND.UNA指向已发送但未收到ACK确认的第一个位置。
接收窗口:
REV.WND表示当前可接收窗口的大小,RCV.NEXT表示可以接受数据的第一个位置。
了解Nagel算法延迟确认吗?
当TCP报文承载的数据量比较小的时候,比如只有几个字节,这样的有效数据占比就比较低,因为TCP头部就已经至少占用了20个字节,也会有 20 个字节的 IP 头部,有两个策略来减少小报文的传输,分别是:
Nagel算法:
- 畅通时(无未确认数据): 立即发送。
- 等待时(有未确认数据): 累积数据直到达到MSS,或者等到之前的未确认数据得到确认为止,再发送累积的数据。
延迟确认:如果接收端返回ACK的同时没有数据携带,那么效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报⽂。
- 当有响应数据要发送时,ACK会随着响应数据一起发送
- 当没有响应数据要发送时,ACK 将会延迟⼀段时间,以等待是否有响应数据可以⼀起发送
- 当在延迟等待发送ACK期间,对方发送的第二个数据报文又到达了,此时就算没有响应数据也立即发送ACK
Nagel算法和延迟确认算法不能同时启用,否则延迟会变得不可接受。
说说TCP的拥塞控制?
不同于流量控制(基于可接收缓存窗口大小避免缓存溢出),拥塞控制面向的是网络,当⽹络拥塞时,TCP 会⾃我牺牲,降低发送的数据速率。发送方会维护一个拥塞窗口cwnd,调节要发送的数据的量。
什么是拥塞窗口,和发送窗口的关系?
拥塞窗口cwnd是根据网络拥塞情况动态变化的窗口,swnd = min(cwnd, rwnd)
- 只要⽹络中没有出现拥塞, cwnd 就会增⼤;
- 但⽹络中出现了拥塞, cwnd 就减少;
拥塞控制有哪些常用算法?
1. 慢启动:TCP连接建立完成后,一开始不要发送大量数据,而是试探网络的拥塞程度,由小到大不断增加拥塞窗口的长度:如果没有丢包,每成功收到n个ACK就将cwnd + n(单位是MSS最大报文长度), 则每轮发送窗口的大小翻倍,如果出现丢包,则拥塞窗口长度减半。
当cwnd的大小到达一个慢启动阈值65535,为了防止cwnd大小过大导致网络拥塞,则会触发拥塞避免算法
2. 拥塞避免:cwnd到达慢启动阈值后:
每收到一个ACK,cwnd = cwnd + 1 / cwnd,这显然是线性的。假定 ssthresh 为 8,当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd ⼀共增加 1,于是这⼀次能够发送 9 个 MSS ⼤⼩的数据,变成了线性增⻓。
3. 拥塞发生: 当数据包超时之后还没有收到ACK,就会发生RTO超时重传,使用的是拥塞发生算法:
- 慢启动阈值sshthresh = cwnd / 2
- cwnd重置为1
- 进入新的慢启动过程
还有更好的方式,就是快速重传。发送方连续收到三个相同的ACK时,代表发生了丢包,就会进行快速重传,不必等待RTO超时重传。
发生快速重传的拥塞发生算法:
- 慢启动阈值ssthresh = cwnd / 2
- 拥塞窗口大小cwnd = cwnd / 2
- 进入快速恢复算法:
4. 快速恢复:进入快速恢复算法之前已经执行了快速重传的拥塞发生算法
快速恢复算法如下:
- 3个重复的ACK表明有3个新数据段被发送且接收了,说明至少仍然还有3个承载能力,cwnd = ssthresh + 3
- 重传丢失的数据包
- 如果又收到了和之前一样的重复的ACK(接收方表明期望那个被丢掉的包),这个重复的ACK表明又有一个数据包离开了网络,被接收方缓存了,说明网络还不是那么的拥堵,仍然有一定的承载能力,那么cwnd = cwnd + 1(为了提高发送能力,尽快将丢掉的包发送出去)
- 如果收到新的ACK,表明快速恢复过程已经结束,cwnd = ssthresh,再次进入拥塞避免算法
说说TCP的重传机制?
在发送某个数据后开启一个计时器,如果在一定时间内没有得到发送数据报的 ACK 报文,就重新发送数据,直到发送成功为止。
包括RTO超时重传、快速重传、带选择确认的重传(SACK)和重复SACK四种。
RTO超时时间设置为多少?
RTO 有个标准方法的计算公式,叫 Jacobson / Karels 算法。
1. 首先计算SRTT,也就是平滑往返时间,以避免单次测量中的抖动影响重传时间
SRTT = (1 - α) * SRTT + α * RTT
α是一个常量,通常取值1/8,表示新测量值对SRTT的影响。
RTT是平均单次往返时间,也会不断更新
2. 计算 RTTVAR (RTT Variation,表示RTT的变化量,用于衡量RTT的波动)
RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|)
β 通常取值为 0.25(即1/4),表示对RTTVAR更新的权重。
3. 最后,得出最终的 RTO
RTO = SRTT + max(G, 4 x RTTVAR)
G是一个小的常量偏移,用来防止RTO过小,一般为1ms
RTO由于需要等待,效率不高,碰到超时还会加倍,所有引入快速重传。
快速重传的原理及局限性
发送⽅发出了 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 。
快速重传机制只解决了⼀个问题,就是超时时间的问题,但是它依然⾯临着另外⼀个问题。就是重传的时候,是重传之前的⼀个,还是重传所有的问题。
⽐如对于上⾯的例⼦,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。
带选择确认的重传(SACK)
就是在快速重传的基础上,接收方还返回最近接收到的报文段的序列号范围 ,这样发送方就知道哪些成功发出去了,哪些还没有,就知道该重传哪些包了。
什么是重复SACK(D-SACK)?
是SACK的扩展,主要用于接收方告诉发送方,哪些包自己重复接收了,目的是帮助发送方判断是否发生了包失序、ACK丢失、包重复或伪重传。
说说TCP的粘包和拆包?
TCP底层并不了解数据的具体含义,也不关心消息边界,它会根据缓冲区的实际情况进行包的划分,一个完整的包有可能被拆分成多个包发送,也有可能把多个包组合成一个大的数据包发送。造成的后果是应用层从缓冲区提取数据时不知道消息边界。
粘包有可能是因为:Naggel算法、缓冲区数据合并发送给应用层
拆包可能是因为:TCP数据部分大小超过MSS、发送缓冲区空间不足
怎么解决?
- 固定长度封装:发送端将每条消息封装为固定长度
- 用特殊字符分割
- 头部 + 内容体:将payload数据分割为两部分,一部分是固定大小的头部,说明数据长度;一部分是内容
这样应用层在提取缓冲区数据时就知道消息边界。
一次TCP连接可以发送多少次HTTP请求?
- HTTP 1.0默认不是持久连接,所以一次TCP连接只支持一次HTTP请求和响应,但可以通过keep-alive支持多个
- HTTP 1.1默认设置了connection: keep-alive来打开持久连接,一次TCP可以发送多次HTTP请求和响应,但是会有头阻塞
- HTTP 2.0支持多路复用,把每次请求响应分割成帧并通过流传输,每个流之间互相独立互不影响,一个TCP连接上可以有多个流同时传输