一、第一次握手丢失会怎么样?
1. 触发超时重传
当客户端发送 SYN 报文后,会进入SYN_SENT状态。此时客户端会启动一个重传定时器。如果在规定时间内没有收到服务端的确认(SYN-ACK),客户端就会认为报文丢失,从而重新发送 SYN 报文。
2. 重传的时间间隔(指数退避)
TCP 不会无限制地立即重传,而是采用指数退避算法。在大多数操作系统(如 Linux)中,重传的策略通常如下:
第一次重传:在 1 秒后(通常初始 RTO 为 1s)。
第二次重传:如果还没收到,等待 2 秒后重传。
后续重传:等待时间翻倍(4s, 8s, 16s...)。
3. 最大重传次数
重传并不会永远持续下去。在 Linux 系统中,受内核参数tcp_syn_retries的限制。
默认值通常为5 次或6 次。
如果达到最大次数后依然没有收到响应,客户端就会放弃连接,并向应用层返回“连接超时”的错误。
4. 整体等待时间计算
假设tcp_syn_retries为 5 次,重传序列通常是: 1s + 2s + 4s + 8s + 16s + 32s =63秒。 这意味着,如果第一次握手丢失且服务端一直没响应,用户可能需要等待一分钟左右才会看到最终的报错。
二、第二次握手丢失会怎么样?
1. 双重重传机制
由于第二次握手(SYN-ACK)具有“承上启下”的作用,它既是对客户端 SYN 的确认,也是服务端发起的连接请求。如果它丢失了:
客户端(认为自己的 SYN 没传到):客户端迟迟收不到确认,会触发重传,再次发送SYN报文。
服务端(认为自己的 SYN-ACK 没传到):服务端发送 SYN-ACK 后会进入
SYN_RCVD状态。由于没有收到客户端的 ACK(第三次握手),服务端也会触发超时重传,再次发送SYN-ACK报文。
2. 重传次数与参数控制
两端的重传逻辑由不同的内核参数控制:
| 角色 | 动作 | 控制参数 | 默认值 (Linux) |
| 客户端 | 重传SYN | tcp_syn_retries | 通常为 5-6 次 |
| 服务端 | 重传SYN-ACK | tcp_syn_retries_2(或tcp_synack_retries) | 通常为 5 次 |
3. 指数退避与超时
和第一次握手类似,两端都会遵循指数退避算法(1s, 2s, 4s...)。
最终结果:如果网络一直不通,两端都会在达到最大重传次数后放弃。
状态清理:服务端在放弃前,会维持一个半连接(Half-open connection)。如果丢失过多,可能会填满服务端的SYN 队列,导致所谓的“SYN Flood”攻击效果,使正常用户无法建立连接。
4. 关键区别:谁先“放弃”?
在实际环境中,客户端和服务端的重传次数设置可能不同。
如果服务端先达到上限,它会关闭半连接并释放资源。
如果客户端先达到上限,它会直接报错给应用程序(如 Browser 报错连接超时)。
总结
第二次握手丢失是最浪费资源的情况,因为它迫使通信双方都在不断尝试重发。
三、第三次握手丢失会怎么样?
1. 双方状态的“不对等”
当第三次握手丢失时,客户端和服务端处于完全不同的认知状态:
客户端(Client):在发送完 ACK 后,状态立即变为
ESTABLISHED。它认为连接已经建立,可以开始发送数据了。服务端(Server):没收到 ACK,状态依然维持在
SYN_RECV。它认为握手还没完成。
2. 服务端的反应:超时重传
服务端因为没有收到最后的确认包,会触发超时重传机制。
重发 SYN+ACK:服务端会认为自己之前的第二次握手包丢了,于是重新发送
SYN+ACK给客户端。次数限制:服务端会尝试多次(通常是 5 次,取决于系统设置),每次间隔时间倍增(1s, 2s, 4s...)。
关闭连接:如果重传达到上限依然没有收到客户端的 ACK,服务端会自动关闭这个连接并回收资源。
3. 客户端的反应:视情况而定
客户端的后续表现取决于它是否立即发送数据:
场景 A:客户端只握手,不发数据客户端处于
ESTABLISHED,但在收到服务端重传的SYN+ACK时,会意识到之前的 ACK 可能丢了,于是再次发送 ACK给服务端。如果这次送达了,双方都进入正常状态。场景 B:客户端立即发送数据包
客户端认为连接已通,直接发送携带载荷(Payload)的数据包。
关键点:在 TCP 协议中,数据包本身也带有 ACK 确认信息。
当服务端收到这个“带数据的包”时,它能从包头发现 ACK 号是正确的,于是直接从
SYN_RECV转为ESTABLISHED状态,并正常处理数据。这种情况下,丢失的第三次握手被后续的数据包“补偿”了。
场景 C:连接被服务端关闭后,客户端才发数据如果服务端重传多次失败已经关闭了连接,而客户端此时才发送数据。服务端会因为找不到对应的连接记录,直接回执一个RST(重置)包,告诉客户端:“我这没这个连接,请重连。”
四、第一次挥手丢失了会怎么样?
1. 客户端:进入超时重传
当客户端发送 FIN 包后,状态会由ESTABLISHED变为FIN_WAIT_1。
等待 ACK:客户端会等待服务端的确认(ACK)。如果由于网络原因 FIN 包丢失,客户端迟迟收不到 ACK。
超时重传:客户端会触发超时重传(Retransmission)机制,重新发送这个 FIN 包。
退避算法:重传的时间间隔通常会翻倍(例如 1s, 2s, 4s...),以减轻网络拥堵。
重传次数限制:客户端会重传若干次(由内核参数
tcp_orphan_retries控制,通常为 5-8 次)。
2. 服务端:毫无察觉,保持连接
由于第一次挥手(FIN)压根没到服务端,服务端并不知道对方想要断开连接:
状态不变:服务端依然维持在
ESTABLISHED状态。继续等待:服务端会认为连接依然正常,继续等待接收数据或发送数据。
3. 最终结局:静默关闭 或 RST
如果客户端重传多次 FIN 后依然没有任何回应(服务端宕机或网络彻底中断),客户端将不再尝试:
客户端直接关闭:客户端达到最大重传次数后,会强制释放连接资源,直接进入
CLOSED状态。服务端超时释放(Keepalive):* 此时服务端还傻傻地开着连接。
如果服务端开启了TCP Keepalive(保活机制),在一段很长的时间(默认通常是 2 小时)没有数据往来后,服务端会发送探测包。
因为客户端已经关闭了连接,客户端可能会回一个RST(重置)包。
服务端收到 RST 后,也会意识到连接已断开,从而释放资源。
总结
| 阶段 | 现象 | 结果 |
| 客户端 (Client) | 状态变为FIN_WAIT_1,没收到 ACK | 超时重传FIN 包;若多次重传失败,直接强制关闭。 |
| 服务端 (Server) | 没收到 FIN 包 | 状态保持ESTABLISHED;直到保活探测超时或收到 RST 后才关闭。 |
简单来说:第一次挥手丢失并不可怕,客户端会通过“反复横跳”(重传)来尝试修复;如果实在连不上,客户端会自己先“撒手不管”(直接关闭),而服务端最终也会因为超时而释放资源。
五、第二次挥手丢失了会怎么样?
1. 双方状态的“信息差”
客户端(主动关闭方):发送了 FIN,正在
FIN_WAIT_1状态死等 ACK。服务端(被动关闭方):收到了 FIN,已经回了 ACK,状态变为
CLOSE_WAIT。它认为自己已经告诉客户端“我知道你要断开了”,正在处理剩下的任务(比如发完最后的缓冲区数据)。
2. 会发生什么?(双重重传机制)
由于 ACK 报文本身是没有确认机制的(即 ACK 包丢了,接收方不会回一个 ACK 来确认收到了 ACK),系统只能靠“超时”来解决问题:
A. 客户端:重传 FIN
客户端在FIN_WAIT_1状态下,如果收不到 ACK,它会认为自己的第一次挥手(FIN)丢了。
动作:客户端会再次发送 FIN 包。
后果:只要服务端还活着,它每收到一个重传的 FIN,就会再次触发一个第二次挥手(ACK)。
B. 服务端:等待应用层指令
服务端此时处于CLOSE_WAIT状态。这个状态很特殊,它在等待应用程序调用close()函数来发送第三次挥手(FIN)。
如果服务端应用程序处理得很快,在客户端重传 FIN 之前就发出了第三次挥手(FIN),那么这个新的 FIN 包其实兼具了 ACK 的功能。
客户端收到这个 FIN 后,会意识到连接已经可以进入下一阶段。
3. 最极端的有趣情况:如果客户端一直收不到 ACK
如果网络环境极差,导致所有的 ACK 都丢了,或者服务端处理极慢:
客户端心灰意冷:客户端在重传多次 FIN 失败后,会直接认为连接已死,强行关闭(进入
CLOSED)。服务端“守活寡”:服务端可能还停留在
CLOSE_WAIT状态,等待它自己的应用程序关闭。如果应用程序不调用
close(),这个CLOSE_WAIT会永久存在。这就是为什么在排查服务器问题时,如果看到大量
CLOSE_WAIT,通常意味着程序代码有 Bug,忘记关闭连接了。
4. 总结对比
| 视角 | 认知 | 反应 |
| 客户端 | “我的 FIN 丢了吧?怎么没回音?” | 重传 FIN,直到收到 ACK 或达到重传上限。 |
| 服务端 | “我已经答应要关了,等我忙完。” | 每次收到重传的 FIN,就补发一个 ACK。 |
有趣的一点是:
如果第二次挥手(ACK)丢失,但紧接着第三次挥手(FIN)发过来了,客户端可以直接从 FIN_WAIT_1 跳过中间状态,直接进入确认阶段。TCP 协议的设计非常“务实”,只要目的达到了,过程中的小波折可以通过重传来修补。
六、第三次挥手丢失了会怎么样?
1. 服务端的反应:它是真的“着急”
在第三次挥手(服务端发送 FIN)时,服务端的状态由CLOSE_WAIT转为LAST_ACK。
重传责任:因为这是一个带有
FIN标志的报文,它必须得到客户端的确认(第四次挥手 ACK)。动作:服务端如果收不到第四次挥手的 ACK,它会认为自己的 FIN 包丢了。
后果:服务端会开启超时重传,反复发送这个 FIN 包。
结局:如果重传多次(由
tcp_orphan_retries参数决定)依然无果,服务端会彻底死心,强制进入CLOSED状态并释放内存资源。
2. 客户端的反应:它是真的“无助”
客户端在收到第二次挥手(ACK)后,状态进入了FIN_WAIT_2。
死等的困境:在这个状态下,客户端已经完成了它的关闭任务,它在等待服务端的 FIN 包。
没有重传权:客户端此时是“接收方”,它没法主动发什么东西来催促服务端,只能干等。
超时机制:* 如果客户端是通过调用
close()关闭的(这种连接叫孤儿连接,Orphan Socket),内核会给FIN_WAIT_2设置一个定时器(通常由tcp_fin_timeout参数决定,默认 60s)。如果 60s 内没收到 FIN 包,客户端会直接断开连接。
3. 谁先“着急”?(场景推演)
通常情况下,服务端会先表现出“着急”的状态,因为它在不断地重传 FIN 包:
如果网络恢复:服务端重传的 FIN 包终于到了客户端。客户端回一个 ACK(第四次挥手),双方圆满结束。
如果网络彻底断掉:
客户端:盯着表看,60 秒一到,准时走人(进入
CLOSED),释放资源。服务端:还在那儿一次又一次地重传 FIN,直到达到最大重传次数(可能需要几分钟),才最终放弃。
4. 总结对比
| 角色 | 状态 | 心理活动 | 最终结局 |
| 服务端 (Server) | LAST_ACK | “我把最后的话 (FIN) 发了,对方怎么不理我?重发一次试试!” | 多次重传,最后实在没反应就强制关闭。 |
| 客户端 (Client) | FIN_WAIT_2 | “我等着听他最后一句告别,等不到我可就走了。” | 静默等待,超时(通常 60s)后直接闪人。 |
七、第四次挥手丢失会怎么样?
1. 服务端的反应:我认为你没收到
服务端发送了第三次挥手(FIN)后,进入了LAST_ACK状态。
没收到回应:服务端在一段时间内没收到客户端回传的 ACK,它会认为:“糟了,我的 FIN 包可能丢了,或者客户端没收到。”
动作:服务端会重传第三次挥手(FIN)。
循环:只要没收到 ACK,服务端就会一直重传 FIN,直到达到最大重传次数后强行关闭。
2. 客户端的反应:我得等一会儿再走
客户端在发送完第四次挥手(ACK)后,并没有立即进入CLOSED状态,而是进入了著名的TIME_WAIT状态。
收到重传的 FIN:客户端在
TIME_WAIT期间,如果收到了服务端重传的 FIN 包,它会意识到:“看来我刚才发的 ACK 丢了。”动作:客户端会再次发送 ACK,并重新启动
TIME_WAIT定时器。
3. 核心机制:为什么要等待 2MSL?
客户端在TIME_WAIT状态下需要等待2MSL(Maximum Segment Lifetime,报文最大生存时间,通常是 1 到 2 分钟)才会进入CLOSED。这样做有两个核心目的:
原因一:确保可靠地关闭连接
如上所述,为了保证服务端能收到最后的 ACK。如果客户端发完 ACK 就直接闪人(进入CLOSED),而 ACK 恰好丢了,服务端重传 FIN 时,客户端已经不在了。此时客户端的内核会回一个RST包,服务端收到后会认为连接出了异常(报错),而不是正常的“优雅关闭”。
原因二:防止“已过时”的数据包捣乱
网络中可能存在因为绕路而“迟到”的旧数据包。
等待 2MSL 可以保证在这个连接中产生的所有报文都在网络中消失。
如果没有这个等待时间,客户端立即用相同的端口号开启一个新连接,那些迟到的旧报文可能会被误认为是新连接的数据,导致数据乱序或逻辑错误。
4. 总结:如果第四次挥手真的丢了
| 角色 | 状态变化 | 结果 |
| 服务端 (Server) | LAST_ACK重传 FIN | 只有收到 ACK 后才会进入CLOSED。 |
| 客户端 (Client) | TIME_WAIT | 只要收到重传的 FIN,就不断补发 ACK,直到 2MSL 定时器耗尽。 |
趣味思考:
虽然TIME_WAIT保证了可靠性,但在高并发服务器(如爬虫或大量短连接服务)上,大量的TIME_WAIT会占用掉所有可用的端口号,导致无法建立新连接。