十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

qqqqqqqq速查手册

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);}}} }逐行解析:tp-snd_una = tp-snd_nxt = 4;:ISN 通常从 4 开始(简化示例,实际为随机值)。snd_una 是未确认序列号,snd_nxt 是下一个发送序列号。 send_syn(sk):构造 SYN 包,设置 SYN 标志位,seq 为 ISN。 sk-sk_state = TCP_SYN_SENT:进入 SYN_SENT 状态。此时客户端可以接收 SYN+ACK,但不能接收 RST(重置包)。 sk_reset_timer:启动重传定时器。如果 1 秒内没收到响应,重传 SYN。默认重传 6 次,每次间隔指数退避(1s, 2s, 4s, 8s, 16s, 32s),总超时约 63 秒。 tcp_in_window(sk, skb-seq):检查收到的 SYN+ACK 是否在窗口内。防止旧包干扰。 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解析:Flags [S]:SYN 包,seq 12345。 Flags [S.]:SYN+ACK 包,seq 67890, ack 12346。注意 ack 是 seq+1,表示确认收到 12345。 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_RCVTIMEO 和 SO_SNDTIMEO 设置超时。4. 跨平台差异 不同操作系统对 TCP 状态机的实现略有差异。例如,Linux 的 SYN_SENT 状态与 Windows 的 SYN_SENT 状态在重传策略上不同。 对策:测试时覆盖多平台。 避免依赖特定操作系统的行为。结尾互动 TCP 握手看似简单,但细节决定成败。面试中被问“为什么三次”,如果你能画出状态机,解释 ISN 随机化,分析 SYN Flood 防御,那就赢了。 这个知识点你面试被问过吗?留言说说:你遇到过最诡异的 TCP 连接问题是什么?是 SYN 重传导致的超时,还是 RST 包导致的连接重置?分享你的踩坑经验,帮助更多转岗开发者避坑。
返回列表