1. 面试官问握手挥手,到底在考察什么
面过前端的人大概都有这种体验:简历上写着熟悉网络基础,面试官张口就是“说说三次握手”。很多人背得滚瓜烂熟,SYN、ACK、seq、ack 一顿输出,结果面试官追问一句“为什么不能是两次”,当场卡壳。问题不在于记性不好,而在于从一开始就没搞清楚这道题到底在考什么。
TCP 的三次握手和四次挥手,表面上是背概念,实际上是面试官在验证三件事:你知不知道连接是怎么建立的、你理不理解可靠传输的代价、你能不能用这套逻辑去解释真实遇到的网络问题。前端岗位问这个,不是要你去写协议栈,而是因为页面加载慢、接口偶发失败、WebSocket 断连这些日常问题,根子经常就在连接的建立和释放上。
我见过太多候选人把三次握手背成顺口溜,但问他“前端什么时候会触发握手”,答不上来。这就暴露了一个问题:知识是孤立的,没有和自己的工作场景挂钩。所以这篇内容我不打算只给你一份背诵材料,而是把这条链路拆开,讲清楚每一步为什么存在、前端在哪些环节会碰到它、面试被追问时怎么答才显得是真懂而不是真背。
适合谁看:准备前端面试的同学、想补网络基础的开发者、以及那些背了但总感觉没吃透的人。下面从连接建立讲起,一路讲到挥手、状态机、常见追问,最后说说怎么梳理成自己能讲出来的东西。
2. 三次握手:把“可靠连接”这件事拆开看
2.1 握手之前,双方其实都是“裸奔”状态
要理解握手,先得理解握手之前发生了什么。客户端和服务端在建立连接之前,彼此没有任何共享信息:客户端不知道服务端能不能收、服务端不知道客户端能不能发、双方都不知道对方的初始序列号是多少。而 TCP 是一个面向连接的、可靠的、基于字节流的协议,它要保证数据不丢、不重、不乱序,那就必须先解决一个前提——双方得先确认彼此都在线,并且协商好从哪个字节开始编号。
这里有个容易被忽略的点:TCP 的可靠性不是靠“连接”这个抽象概念实现的,而是靠序列号、确认号、重传机制这些具体手段。握手的过程,本质上就是双方交换初始序列号(ISN)并确认对方收到了自己的序列号。你可以把它想象成两个人打电话前先对暗号:我说一句“我要开始说了,我从第 100 句开始”,你回一句“收到,我从第 200 句开始”,我再回一句“好的,收到你的 200”。这三句话说完,双方才敢正式开聊。
为什么初始序列号不能固定从 0 或 1 开始?这是安全层面的考虑。如果每次连接的起始序列号都可预测,攻击者就能伪造报文注入数据。所以现代 TCP 实现里,ISN 是根据时钟和一个随机因子算出来的,每次连接都不一样。这一点面试时如果被问到“ISN 为什么是随机的”,能答上来会加分。
2.2 三次握手的每一步到底在交换什么
把过程拆成三步来看,每一步都有明确的目的,不是凑数。
第一步,客户端发送 SYN 报文,携带自己的初始序列号 seq=x,同时进入 SYN_SENT 状态。这一步的含义是:“我想跟你建立连接,我的起始编号是 x。”注意此时客户端还不能发数据,因为连接还没建立。
第二步,服务端收到 SYN 后,回复一个 SYN+ACK 报文,携带自己的初始序列号 seq=y,以及确认号 ack=x+1,进入 SYN_RCVD 状态。这一步同时做了两件事:确认收到了客户端的 SYN(ack=x+1),并告诉客户端自己的起始编号(seq=y)。
第三步,客户端收到后,再发送一个 ACK 报文,ack=y+1,seq=x+1,双方进入 ESTABLISHED 状态。这一步的含义是:“我收到你的起始编号了,连接可以正式用了。”
用表格把这三步的关键字段列清楚,面试时脑子里有这个表就不会乱:
| 步骤 | 方向 | 标志位 | seq | ack | 状态变化 |
|---|---|---|---|---|---|
| 第一步 | 客户端→服务端 | SYN | x | 无 | CLOSED→SYN_SENT |
| 第二步 | 服务端→客户端 | SYN+ACK | y | x+1 | LISTEN→SYN_RCVD |
| 第三步 | 客户端→服务端 | ACK | x+1 | y+1 | SYN_SENT→ESTABLISHED,服务端 SYN_RCVD→ESTABLISHED |
这里有个细节值得注意:第三步的 seq 是 x+1,因为 SYN 报文虽然不携带数据,但它会消耗一个序列号。同理,第二步的 SYN 也消耗一个序列号,所以第三步的 ack 是 y+1。这个“SYN 占一个序号”的规则,是很多人背的时候会忽略的,但面试官如果追问“为什么 ack 要加 1”,答不上来就露馅了。
2.3 为什么两次不够、四次多余
这是握手部分最高频的追问,必须答得有逻辑而不是靠感觉。
先说为什么两次不行。假设只有前两步:客户端发 SYN,服务端回 SYN+ACK,然后双方就认为连接建立了。问题出在服务端这边——它发完 SYN+ACK 之后,并不知道客户端有没有收到。如果这个报文在网络中丢了,客户端会以为连接没建立,而服务端却已经进入 ESTABLISHED 状态,傻等着客户端发数据。这就造成了双方状态不一致,服务端白白占用资源。
更严重的是历史连接的问题。假设客户端之前发过一个 SYN,因为网络延迟很久才到达服务端,服务端回复 SYN+ACK 后客户端早已放弃。如果只有两次握手,服务端收到这个迟到的 SYN 就会直接建立连接,然后一直等一个永远不会来的客户端。三次握手的第三步让客户端有机会说“这个连接我不要”,从而避免服务端建立无效连接。
再说为什么不需要四次。有人会想:既然要双向确认,那客户端确认服务端、服务端再确认客户端,不是要四次吗?关键在于第二步把 SYN 和 ACK 合并了。服务端收到 SYN 后,确认客户端和发送自己的 SYN 这两件事可以放在同一个报文里完成,没必要分两次。所以三次是理论上的最小值:要双向交换 ISN 并各自确认,最少需要三个报文。
面试时如果被问“能不能用四次”,可以答:逻辑上可以拆成四次,但第二步的 ACK 和 SYN 没有必须分开的理由,合并后效率更高,所以标准实现是三次。
2.4 前端在哪些场景会真实触发握手
很多人觉得握手离前端很远,其实不然。每一次新的 TCP 连接建立,背后都有一次握手。前端常见的触发场景包括:
- 首次访问页面时,浏览器和服务器建立 HTTP 连接,底层就是 TCP 握手。如果是 HTTPS,握手之后还有 TLS 握手,所以 HTTPS 比 HTTP 慢的那部分,一部分就花在这里。
- 使用 WebSocket 建立长连接时,先走一次 TCP 握手,再走 HTTP 升级协议。
- 页面里请求了不同域名的资源,每个新域名都可能触发新的 TCP 连接(除非复用了连接池)。
- 使用 fetch 或 XMLHttpRequest 发起跨域请求时,如果触发了预检请求,也可能涉及新连接的建立。
理解这一点,你就能解释为什么“减少域名数量”“使用连接复用”“开启 HTTP/2”这些优化手段有效——它们本质上都在减少握手的次数。面试时如果能主动把握手和性能优化联系起来,比单纯背流程高一个层次。
3. 四次挥手:为什么断开比建立更麻烦
3.1 挥手为什么是四次而不是三次
连接建立时,SYN 和 ACK 可以合并;但断开时,ACK 和 FIN 通常不能合并,这是四次挥手的根本原因。
TCP 是全双工的,意味着双方可以同时发送数据。当一方想关闭连接时,它只能保证自己不再发数据了,但对方可能还有数据没发完。所以关闭必须分方向进行:一方发 FIN 表示“我没数据要发了”,对方回 ACK 表示“知道了”,但对方可能还要继续发数据,等它也没数据了,再发一个 FIN,这边再回一个 ACK。这样就是四次。
具体流程:
第一步,主动关闭方(假设是客户端)发送 FIN 报文,seq=u,进入 FIN_WAIT_1 状态。含义是“我的数据发完了”。
第二步,服务端收到 FIN,回复 ACK,ack=u+1,进入 CLOSE_WAIT 状态。此时客户端进入 FIN_WAIT_2。注意,服务端此时还可以继续发送数据,客户端也还能接收。
第三步,服务端数据发完后,发送 FIN 报文,seq=w,ack=u+1,进入 LAST_ACK 状态。含义是“我也发完了”。
第四步,客户端收到 FIN,回复 ACK,ack=w+1,进入 TIME_WAIT 状态。服务端收到后进入 CLOSED。
| 步骤 | 方向 | 标志位 | seq | ack | 状态变化 |
|---|---|---|---|---|---|
| 第一步 | 客户端→服务端 | FIN | u | 无 | ESTABLISHED→FIN_WAIT_1 |
| 第二步 | 服务端→客户端 | ACK | v | u+1 | ESTABLISHED→CLOSE_WAIT,客户端→FIN_WAIT_2 |
| 第三步 | 服务端→客户端 | FIN | w | u+1 | CLOSE_WAIT→LAST_ACK |
| 第四步 | 客户端→服务端 | ACK | u+1 | w+1 | FIN_WAIT_2→TIME_WAIT,服务端→CLOSED |
同样注意 FIN 也消耗一个序列号,所以 ack 要加 1。
3.2 TIME_WAIT 状态:那个让人又爱又恨的 2MSL
四次挥手里最常被追问的就是 TIME_WAIT。客户端发完最后一个 ACK 后,不会立刻关闭,而是进入 TIME_WAIT 状态,等待 2MSL(Maximum Segment Lifetime,报文最大生存时间)后才真正关闭。Linux 上这个值通常是 60 秒。
为什么要等?两个原因。
第一,保证最后一个 ACK 能到达服务端。如果这个 ACK 丢了,服务端会超时重传 FIN,此时客户端如果已经关闭,就无法回应,服务端会一直重试。有了 TIME_WAIT,客户端还能收到重传的 FIN 并再次发送 ACK。
第二,让本次连接的所有报文在网络中消散。如果立刻用相同的四元组建立新连接,旧连接中延迟的报文可能被新连接误收,造成数据错乱。等待 2MSL 后,旧报文基本都消失了。
TIME_WAIT 过多是服务端常见的性能问题,但要注意:TIME_WAIT 通常出现在主动关闭方。如果是服务端主动关闭连接,大量 TIME_WAIT 会占用端口资源。前端开发虽然不直接管服务端,但面试时如果被问到“大量 TIME_WAIT 怎么办”,能说出“让客户端主动关闭”“调整内核参数”“使用连接复用”这些方向,就说明你理解了这个状态的本质。
3.3 CLOSE_WAIT 堆积:前端也可能踩的坑
相比 TIME_WAIT,CLOSE_WAIT 更值得前端警惕。CLOSE_WAIT 出现在被动关闭方,含义是“我收到了对方的 FIN,但我还没发自己的 FIN”。如果这个状态大量堆积,说明程序收到了关闭请求却没有正确关闭连接。
前端在 Node.js 服务端渲染、BFF 层、或者用 Node 写中间层时,如果代码里打开了 socket 或数据库连接却没有在合适时机关闭,就可能出现 CLOSE_WAIT 堆积。表现是连接数越来越多,最终耗尽文件描述符,服务不可用。
排查思路:用netstat或ss命令查看连接状态分布,如果 CLOSE_WAIT 数量持续增长,基本可以定位到代码里某处没有调用 close。常见原因是异常分支没有释放连接、或者回调里忘记关闭。这个经验在面试时讲出来,比背状态机图有说服力得多。
3.4 半关闭与连接复用的取舍
TCP 支持半关闭,即一方关闭发送方向但保留接收方向。四次挥手的前两步之后,服务端进入 CLOSE_WAIT,此时客户端处于 FIN_WAIT_2,连接处于半关闭状态。这个机制允许服务端在客户端不再发送数据后,继续把剩余数据发完。
但实际工程中,半关闭用得并不多。HTTP/1.1 默认使用持久连接,连接不会频繁关闭;HTTP/2 更是多路复用,一条连接上跑很多请求。所以挥手发生的频率比握手低。不过理解半关闭有助于解释一些边界问题,比如为什么有些服务在收到 FIN 后还能返回数据。
面试时如果被问“HTTP 连接什么时候关闭”,可以答:HTTP/1.0 默认每次请求后关闭,HTTP/1.1 默认持久连接,由 Connection 头控制,服务端或客户端都可以主动关闭。关闭时走的就是四次挥手。
4. 状态机与异常:面试官真正想听的深度
4.1 把状态迁移串成一条线
零散地背状态很容易忘,串成一条线就清晰了。客户端视角:CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED。服务端视角:CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED。
注意服务端有一个 LISTEN 状态,这是被动打开时特有的。客户端是主动打开,所以没有 LISTEN。面试时如果被问“服务端在握手前处于什么状态”,答 LISTEN 就对了。
还有一个容易混淆的点:同时打开和同时关闭。同时打开时,双方都发 SYN,状态迁移会变成 SYN_SENT → SYN_RCVD → ESTABLISHED,需要四次报文。同时关闭时,双方都发 FIN,会进入 CLOSING 状态。这些属于偏门但能体现深度的问题,了解即可,不必死记。
4.2 SYN 洪泛与握手的安全维度
三次握手有一个天然弱点:服务端收到 SYN 后就要分配资源并进入 SYN_RCVD,但此时连接还没完全建立。攻击者可以伪造大量源 IP 发送 SYN,服务端回复 SYN+ACK 后收不到第三次 ACK,就会一直重试直到超时。这就是 SYN 洪泛攻击。
防御手段常见的有 SYN Cookie:服务端不立即分配资源,而是把状态编码进序列号里,收到第三次 ACK 时再重建。还有缩短 SYN_RCVD 的超时时间、限制半连接队列长度等。
前端面试一般不会深挖到这个层面,但如果聊到网络安全,能提一句“三次握手的资源分配时机是 SYN 洪泛的根源”,会显得知识面更广。注意这里只谈技术原理,不涉及任何具体攻击工具或操作。
4.3 抓包验证:把理论变成看得见的东西
光背不练,面试时容易心虚。建议自己用抓包工具跑一遍,亲眼看看三次握手和四次挥手的报文。Wireshark 是最常用的工具,抓取本机回环接口或者访问一个 HTTP 站点,过滤tcp.flags.syn == 1就能看到握手过程。
抓包时重点观察几个字段:Flags 里的 SYN、ACK、FIN 标志,Seq 和 Ack 的数值变化,以及每次报文的时间戳。你会发现握手通常在一瞬间完成,而挥手可能因为服务端还有数据要发而间隔较长。这种直观感受是背书给不了的。
如果面试时能说“我自己抓包看过,第三次握手的 ACK 里 seq 是 x+1”,面试官会认为你是真的动手验证过。这比任何背诵都有说服力。
5. 高频追问与容易翻车的地方
5.1 那些看似简单却容易答错的追问
整理几个我见过的高频追问,以及容易踩的坑:
| 追问 | 常见错误回答 | 正确思路 |
|---|---|---|
| 第三次握手可以携带数据吗 | 不可以 | 可以。第三次握手时客户端已进入 ESTABLISHED,可以携带数据,但通常不这么做 |
| 第二次握手可以携带数据吗 | 可以 | 不可以。服务端此时还在 SYN_RCVD,未完全建立连接 |
| 握手过程中可以传输数据吗 | 不能 | 前两次不能,第三次可以 |
| 为什么 ISN 要随机 | 不知道 | 防止序列号被预测导致数据注入 |
| TIME_WAIT 为什么是 2MSL 不是 1MSL | 背的 | 1MSL 只能保证一个方向报文消散,2MSL 保证往返两个方向 |
这些追问的共同点是:它们考察的不是记忆,而是你对机制的理解。比如“第三次握手能不能带数据”,如果你理解状态迁移,就知道客户端发第三次 ACK 时已经进入 ESTABLISHED,理论上可以带数据。但实际实现中,为了简单和安全,通常不带。
5.2 把握手挥手和前端性能优化挂钩
面试时如果能把基础知识和实际优化联系起来,层次立刻不一样。举几个例子:
- 减少 TCP 连接数可以降低握手开销,所以 HTTP/2 的多路复用比 HTTP/1.1 的多个连接更高效。
- 使用 CDN 时,客户端和边缘节点的握手延迟比回源小,所以静态资源加载更快。
- WebSocket 建立后保持长连接,避免了反复握手,适合实时通信场景。
- 预连接(preconnect)和 DNS 预解析(dns-prefetch)能提前完成握手和 DNS 查询,缩短关键路径。
这些点不需要全部答出来,挑一两个结合项目经历讲,效果最好。比如“我之前做过一个实时聊天功能,用 WebSocket 替代轮询,就是因为轮询每次都要新建连接,握手开销太大”。
5.3 面试表达:怎么讲才不像背的
最后说表达技巧。背出来的答案有一个特征:语速均匀、没有停顿、像念课文。而真正理解的答案会有轻重缓急,会主动解释“为什么”。
建议的讲法是:先一句话概括流程,再挑一个关键点深入,最后联系实际。比如:“三次握手就是双方交换初始序列号并确认的过程。我觉得最关键的是第三步,它让客户端有机会拒绝服务端建立的无效连接,这也是为什么两次不够。实际中我们做性能优化时,减少连接数就是在减少这个过程。”
这样的回答有结构、有重点、有落地,面试官一听就知道你不是临时抱佛脚。
6. 自己梳理一遍,比背十遍都管用
回到标题里那句话:结尾总结建议自己梳理全文背诵。我的理解是,梳理在前,背诵在后。你得先用自己的话把整条链路讲通,再谈记忆。
具体怎么做?我自己的方法是画一张图,把客户端和服务端的状态迁移画成两条平行线,中间用箭头标出报文流向。画完之后合上书,对着空白纸重新画一遍,画不出来的地方就是没掌握的。然后再用抓包工具验证一遍,看看真实报文和图上是否一致。
还有一个技巧:把三次握手和四次挥手对比着记。建立时 SYN 和 ACK 能合并,所以三次;断开时 ACK 和 FIN 不能合并,所以四次。抓住这个差异,就不用死记硬背数字了。
面试前如果时间紧,至少把这几件事过一遍:状态迁移图、每一步的 seq 和 ack 变化、TIME_WAIT 的原因、两次不够三次够的理由。这几个点覆盖了八成以上的追问。剩下的深度问题,靠平时积累和临场发挥。
网络基础这东西,背是一时的,理解是一辈子的。真正吃透之后,你会发现它不只是面试题,而是你排查线上问题、做性能优化时的底层直觉。