news 2026/9/22 19:09:13

qqqqqqqq速查手册

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
qqqqqqqq速查手册

面试必问:搞懂TCP三次握手底层,告别原理答不上来

面试被问“TCP为什么是三次握手”,你只背了“防止历史连接”,面试官追问“如果第二次握手丢失了怎么办”,你瞬间卡壳。这种尴尬,是无数转岗开发者的噩梦。TCP/IP 协议栈的面试必问考点,从来不是死记硬背流程,而是理解背后的状态机与资源开销。

今天不聊虚的,直接拆解 TCP 握手的底层逻辑。结合 RFC 793 规范与真实源码,把原理讲透。看完这篇,下次再被问原理,你能画出状态机,还能说出为什么不能两次或四次。

一句话原理:状态机同步与序号确认

TCP 握手的本质,不是简单的“打招呼”,而是同步初始序列号(ISN)确认双方收发能力

核心逻辑只有一句话:双方通过交换 SYN 包,让接收方知道“我准备好了,我的下一个期望字节是 ISN+1”,同时让发送方知道“你的 SYN 我收到了,我的下一个期望字节是 ISN+1”。

这里有个关键点:ISN(Initial Sequence Number)不是固定的。它是由内核根据时间、随机数生成的。为什么?为了防止旧连接的残留报文干扰新连接。如果 ISN 固定,一个延迟了 3 秒的旧 ACK 包,可能会被新连接误认为是有效确认,导致数据错乱。RFC 793 明确规定,ISN 必须具有足够的熵,以覆盖最大报文寿命(MSL, Maximum Segment Lifetime)。

类比解释:电话沟通与“忙线”保护

把 TCP 连接想象成打电话。

第一次握手(SYN):你(客户端)拨号,说:“我要跟你通话,我的编号是 1001。”(SYN, seq=1001) 第二次握手(SYN+ACK):对方(服务器)接起电话,说:“我听到了,你的编号 1001 我记下了。我的编号是 2002,我准备好了。”(SYN+ACK, seq=2002, ack=1002) 第三次握手(ACK):你回复:“收到,你的编号 2002 我记下了。通话开始。”(ACK, seq=1002, ack=2003)

为什么不能两次? 假设只有两次。你拨号(SYN),对方回复(SYN+ACK)。此时你挂了。如果对方没挂,对方处于“半打开”状态,一直等你说话。更糟糕的是,如果网络中有延迟的旧 SYN 包,对方收到后会回复 SYN+ACK,但你已经断开,不会回复 ACK。对方会一直重传 SYN+ACK,直到超时。这浪费了服务器资源,且无法区分是“新连接”还是“僵尸连接”。

为什么不能四次? TCP 是可靠传输,核心是效率。第三次 ACK 单独发送,会增加 RTT(往返时延)。在第三次握手中,客户端发送 ACK 的同时,就可以携带数据(Piggybacking)。如果拆成四次,纯 ACK 包不携带数据,浪费带宽。

关键细节:第三次握手的 ACK 包,不能携带数据。这是协议规定。如果携带数据,TCP 栈会如何处理?大多数实现会丢弃数据,或等待下次发送。所以,应用层不要依赖三次握手后立即发数据。

源码/伪代码片段:内核状态机流转

让我们看看 Linux 内核中 tcp_v4_connect 的简化逻辑。注意,这里关注状态变化,而非完整代码。

/* 简化版 TCP 连接建立逻辑 */
void tcp_connect(struct sock *sk, struct sockaddr *uaddr) {struct tcp_sock *tp = tcp_sk(sk);/* 1. 生成 ISN */tp->snd_una = tp->snd_nxt = 4; /* 初始序列号 *//* 2. 发送 SYN */send_syn(sk);/* 3. 状态变更为 SYN_SENT */sk->sk_state = TCP_SYN_SENT;/* 4. 等待 SYN+ACK *//* 此时内核定时器启动,若超时未收到,重传 SYN */sk_reset_timer(sk, &sk->sk_timer, msecs_to_jiffies(1000));
}/* 收到 SYN+ACK 的处理 */
void tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) {if (sk->sk_state == TCP_SYN_SENT) {if (tcp_flag_test(skb->tcp_flags, TCP_ACK)) {/* 验证 ACK 是否正确 */if (tcp_in_window(sk, skb->seq)) {/* 5. 发送 ACK,状态变更为 ESTABLISHED */send_ack(sk);sk->sk_state = TCP_ESTABLISHED;/* 6. 通知应用层连接成功 */sk_data_ready(sk);}}}
}

逐行解析

  1. tp->snd_una = tp->snd_nxt = 4;:ISN 通常从 4 开始(简化示例,实际为随机值)。snd_una 是未确认序列号,snd_nxt 是下一个发送序列号。
  2. send_syn(sk):构造 SYN 包,设置 SYN 标志位,seq 为 ISN。
  3. sk->sk_state = TCP_SYN_SENT:进入 SYN_SENT 状态。此时客户端可以接收 SYN+ACK,但不能接收 RST(重置包)。
  4. sk_reset_timer:启动重传定时器。如果 1 秒内没收到响应,重传 SYN。默认重传 6 次,每次间隔指数退避(1s, 2s, 4s, 8s, 16s, 32s),总超时约 63 秒。
  5. tcp_in_window(sk, skb->seq):检查收到的 SYN+ACK 是否在窗口内。防止旧包干扰。
  6. sk_data_ready(sk):唤醒应用层线程,告知 connect() 调用成功。

常见误区:很多人认为 connect() 是阻塞调用,实际上它是非阻塞的(在多线程模型下)。内核发送 SYN 后立即返回,状态为 SYN_SENT。应用层调用 connect() 时,如果连接未完成,会阻塞在 sk_wait_data 上,直到状态变为 ESTABLISHED 或超时。

流程描述:从字节到状态

我们用文字+代码块表示完整的握手流程,重点关注**序列号(seq)确认号(ack)**的变化。

客户端 (Client)                          服务器 (Server)|                                       || 1. SYN seq=1001                       ||-------------------------------------->||                                       | 状态: LISTEN -> SYN_RCVD|                                       || 2. SYN+ACK seq=2002 ack=1002          ||<--------------------------------------|| 状态: SYN_SENT -> ESTABLISHED         ||                                       || 3. ACK seq=1002 ack=2003              ||-------------------------------------->||                                       | 状态: SYN_RCVD -> ESTABLISHED|                                       || 4. 数据传输 seq=1003                  ||-------------------------------------->|

关键状态变迁

  • LISTEN:服务器调用 listen() 后进入。等待 SYN。
  • SYN_RCVD:服务器收到 SYN,发送 SYN+ACK 后进入。等待 ACK。
  • SYN_SENT:客户端发送 SYN 后进入。等待 SYN+ACK。
  • ESTABLISHED:双方都收到对方的 ACK 后进入。

异常场景:第二次握手丢失 如果 SYN+ACK 丢失,客户端会超时重传 SYN。服务器收到重传的 SYN,会再次发送 SYN+ACK。只要 SYN+ACK 最终到达,连接就能建立。但如果 SYN+ACK 被 RST 包覆盖(例如服务器端口已满),连接失败。

异常场景:第三次握手丢失 如果 ACK 丢失,服务器会认为连接未建立,关闭连接。但客户端已进入 ESTABLISHED 状态,开始发送数据。服务器收到数据,发现不在 ESTABLISHED 状态,会发送 RST 包。客户端收到 RST,连接重置。

实战验证:用 tcpdump 抓包看真相

理论讲得再多,不如抓包看一遍。以下是在 Linux 上用 tcpdump 抓取的 TCP 握手过程。

# 抓取 localhost 上的 80 端口流量
sudo tcpdump -i lo port 80 -nn -vv

输出示例

10:00:01.123456 IP 127.0.0.1.51234 > 127.0.0.1.80: Flags [S], seq 12345, win 65535, length 0
10:00:01.123457 IP 127.0.0.1.80 > 127.0.0.1.51234: Flags [S.], seq 67890, ack 12346, win 65535, length 0
10:00:01.123458 IP 127.0.0.1.51234 > 127.0.0.1.80: Flags [.], ack 67891, win 65535, length 0

解析

  1. Flags [S]:SYN 包,seq 12345
  2. Flags [S.]:SYN+ACK 包,seq 67890, ack 12346。注意 ackseq+1,表示确认收到 12345。
  3. Flags [.]:ACK 包,ack 67891。确认收到 67890。

验证 ISN 随机性: 多次执行 curl localhost:80,观察 seq 值。你会发现每次 seq 都不同,且呈随机分布。这就是 RFC 793 要求的 ISN 随机化。

验证重传: 使用 tc 命令模拟网络延迟:

# 添加 200ms 延迟
sudo tc qdisc add dev lo root netem delay 200ms# 再次抓包
sudo tcpdump -i lo port 80 -nn -vv

你会发现 SYN 包与 SYN+ACK 包之间的时间差从微秒级变为毫秒级。如果设置 drop 50%,还能观察到 SYN 重传。

进阶技巧与避坑:生产环境常见陷阱

1. SYN Flood 攻击 攻击者发送大量伪造源 IP 的 SYN 包,服务器回复 SYN+ACK 后,攻击者不回复 ACK。服务器维持半打开连接,资源耗尽。 对策

  • 启用 SYN Cookie(内核参数 net.ipv4.tcp_syncookies=1)。
  • 缩短 SYN_RECV 状态超时时间(net.ipv4.tcp_synack_retries)。
  • 使用防火墙丢弃异常 SYN 包。

2. 连接复用与 Keep-Alive HTTP/1.1 默认启用 Keep-Alive,连接复用。但 TCP 层仍需维护状态。如果应用层长时间无数据,TCP 可能因超时断开。 对策

  • 设置 SO_KEEPALIVE 选项,定期发送探测包。
  • 应用层实现心跳机制。

3. 半打开连接检测 服务器重启后,客户端可能仍认为连接有效。发送数据后,服务器回复 RST。 对策

  • 客户端处理 RST 包,重新建立连接。
  • 使用 SO_RCVTIMEOSO_SNDTIMEO 设置超时。

4. 跨平台差异 不同操作系统对 TCP 状态机的实现略有差异。例如,Linux 的 SYN_SENT 状态与 Windows 的 SYN_SENT 状态在重传策略上不同。 对策

  • 测试时覆盖多平台。
  • 避免依赖特定操作系统的行为。

结尾互动

TCP 握手看似简单,但细节决定成败。面试中被问“为什么三次”,如果你能画出状态机,解释 ISN 随机化,分析 SYN Flood 防御,那就赢了。

这个知识点你面试被问过吗?留言说说:你遇到过最诡异的 TCP 连接问题是什么?是 SYN 重传导致的超时,还是 RST 包导致的连接重置?分享你的踩坑经验,帮助更多转岗开发者避坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 19:09:07

二四六八十打一成语:从版本升级API全变到入门到精通的避坑指南

二四六八十打一成语:从版本升级API全变到入门到精通的避坑指南 版本升级后 API 全变了,代码跑通一半报错,这时候你才发现,所谓的【二四六八十打一成语】,其实是个典型的“偶数序列”逻辑陷阱。很多开发者在面试或实际项目中,一提到数字规律就懵圈,导致从【入门到精通】的路上频频翻车。别慌,这种问题看似是…

作者头像 李华
网站建设 2026/9/22 19:09:04

2026最新鬾怎么读?房建人必看:代码跑不通的避坑指南

2026最新鬾怎么读?房建人必看:代码跑不通的避坑指南 刚把GitHub上那个“房建进度自动计算”的脚本复制下来,一运行就报错?别慌,这种“复制即崩”的痛,我当年在工地用平板查规范时天天见。很多人以为这是代码写错了,其实90%的情况,是你对基础概念的理解卡在了“鬾怎么读”这个看似无关的坎上——别笑,…

作者头像 李华
网站建设 2026/9/22 19:09:03

3分钟看懂导航地图路线语音提示图解原理

3分钟看懂导航地图路线语音提示图解原理 官方文档通常动辄几百页,里面充斥着晦涩的API定义和时序图,新手看完往往一脸懵圈。其实核心逻辑并不复杂,关键在于剥离冗余,直接看数据流如何变成声音。今天我们就通过图解原理,把导航地图路线语音提示的核心机制拆解得明明白白。…

作者头像 李华
网站建设 2026/9/22 19:09:00

3个实战项目教你吃透汽车限购城市名单数据流

3个实战项目教你吃透汽车限购城市名单数据流 官方文档太长抓不住重点?别慌,我直接带你拆源码。 很多转行做数据开发的兄弟,一看到【汽车限购城市名单】这种业务就头大,觉得逻辑复杂,政策变动频繁。 其实只要跑通一个【实战项目】,你就明白背后的数据流转逻辑了,根本没想象中那么玄乎。…

作者头像 李华
网站建设 2026/9/22 19:08:44

3个步骤搞定执行标准gb,实战项目避坑指南

3个步骤搞定执行标准gb,实战项目避坑指南 配置环境就卡半天?很多中小施工企业负责人在对接政府招投标或验收时,面对一堆“执行标准GB”的文档头大。别急,今天咱们不整虚的,直接上 实战项目 ,用代码把这套标准数字化,让你从“看天书”变成“查字典”。 项目目标:把纸质标准变成可查询的数据…

作者头像 李华
网站建设 2026/9/22 19:08:39

物业费收费标准避坑指南:3个致命错误让项目直接报废

物业费收费标准避坑指南:3个致命错误让项目直接报废 看了一堆教程还是不会写项目?别怪教程,是你没踩够坑。物业费收费标准看着简单,实则全是坑,稍有不慎数据全错。这份避坑指南,专治各种“看起来会了”的假象。 坑的现象:数据算错,业主投诉…

作者头像 李华