每年面试季,我都会把“TCP 三次握手与四次挥手”这道题放在技术面第一轮。问了这么多年,我越来越确定一件事:这道题根本不是送分题。十个候选人里,八个能把流程背得一字不差——“客户端发SYN,服务端回SYN+ACK,客户端再回ACK”,甚至能顺手画出状态迁移图。但只要我追问一句“为什么是三次而不是两次”,一半以上的人会愣住。
更让我意外的是,很多有几年工作经验的人,面对“挥手为什么是四次”“TIME_WAIT为什么要等2MSL”这类变体追问时,依然答不到点子上。问题不在记忆力,而在他们把这套协议当成了八股文,没有真正理解它解决的是什么问题。这篇文章我想换个思路,不从状态机出发,而是从“为什么”出发,把三次握手和四次挥手的完整逻辑链条拆开,再结合我实际抓包和线上排障的经验,讲讲面试官在追问时真正想听到的东西。
1. 题目人人会背,但面试官到底在考察什么
1.1 三次握手与四次挥手背后的能力模型
我面试时喜欢先从这道题开场,不是因为我懒,而是因为它天然覆盖了一个网络工程师或者说后端开发者最核心的几项能力。
第一项是网络分层的基本功。问到TCP,你至少得清楚它属于传输层,承上启下地连接着应用层HTTP、DNS这类协议和网络层的IP。面试官想听你自然地提到“TCP是面向连接的、可靠的、基于字节流的传输层协议”,而不是机械地背书。
第二项是理解“可靠性从哪来”。TCP的可靠并不是拥有某条神奇通道,而是通过确认、重传、排序、流量控制这些机制堆出来的。三次握手和四次挥手,恰恰是这些机制最浓缩的体现。你如果能说清楚“每一次握手确认了谁的发送能力、谁的接收能力”,说明你真的理解了可靠连接的本质。
第三项是对异常场景的敏感度。很多候选人只记得流程,一被问到“如果第三次握手丢包会怎样”“为什么要TIME_WAIT”就答不上来。而线上故障排查看的最多的恰恰就是这些异常场景:连接建不起来、连接卡在某个状态、连接被对端重置。
所以这道题表面考协议,实际考的是你对一个复杂系统在正常路径和异常路径上的综合把握能力。这也是为什么,几乎所有公司的面试题库里都有它。
1.2 面试官追问的四个层级
同一个问题,不同水平的候选人会自动分层。我把这些年比较典型的追问路径整理了一下:
| 层级 | 追问方向 | 典型问题 | 考察意图 |
|---|---|---|---|
| 第一层 | 背流程 | 三次握手的过程是什么? | 有没有基础记忆 |
| 第二层 | 讲原理 | 为什么是三次而不是两次? | 是否理解确认机制 |
| 第三层 | 追状态 | SYN_SENT、CLOSE_WAIT、TIME_WAIT各自何时出现? | 是否熟悉状态迁移 |
| 第四层 | 问工程 | SYN Flood怎么防御?TIME_WAIT太多怎么处理? | 是否具备排障思维 |
说实话,能稳定通过前三层的候选人已经超过平均水平了。能到第四层的,基本就是我要的人。因为到了这个层级,面试官和候选人是在用同一套语言讨论问题,而不是在一问一答地检验记忆力。
2. 三次握手拆解:从SYN到ACK的完整逻辑链
2.1 第一次握手:SYN包为什么要携带一个随机序列号
第一次握手,客户端向服务端发送一个SYN包,首部的SYN标志位被置为1,同时携带一个序列号,通常记作seq=x。
很多人背到这里就停了,但这里其实是最值得展开的地方。序列号x不是随便定的,它叫ISN,Initial Sequence Number,初始序列号。在Linux系统中,它不是固定的0,而是通过一个随时间变化的算法加上随机扰动生成的。为什么不能固定?因为序列号的核心作用有两个:一是给字节流编号做排序和去重,二是让接收方能识别出哪些是旧连接的残留报文。
打个比方,如果你和我之间传纸条,每张纸条上都有一个编号,同时我们约定“编号越大越靠后”。如果每次重新开始对话都从1编号,万一上一轮对话里有一张迟到的纸条恰好也写着1,我就会把它当成新一轮的纸条,整个顺序就全乱了。ISN随机化正是为了降低这种新旧报文串扰的概率。
所以第一次握手的本质是:客户端告诉服务端——“我要和你建立连接,我的初始字节编号是x,请你准备好接收从我这里发出的数据。”
2.2 第二次握手:SYN+ACK组合包的真实含义
服务端收到SYN后,如果愿意建立连接,会同时设置SYN和ACK两个标志位,回复一个SYN+ACK包。这个包携带两个关键信息:自己的初始序列号seq=y,以及对客户端seq的确认号ack=x+1。
注意这里的细节,很多人会忽略ack=x+1的准确含义。它不是在说“我收到了你的第x个字节”,而是在说“我期待你的下一个字节编号是x+1”。TCP的确认号是累计确认,表示的是期望收到的下一个字节序号,这跟你网购时“已签收第5件”不是一回事,更像是在说“你从第6件开始发我就接着收”。
服务端的SYN+ACK同时表达了三层意思:第一,我收到了你的SYN,你的发送方向是通的;第二,我这边也准备好发送数据了,我的初始编号是y;第三,请你开始发送字节编号为x+1的数据。
这其实也在暗示:TCP是全双工的,两条方向独立的通道需要分别建立。第二次握手就是服务端在为“服务端到客户端”这个方向做宣告和确认。
2.3 第三次握手:ACK的作用是完成双向确认
客户端收到SYN+ACK后,回复一个包,ACK标志位置1,确认号ack=y+1,同时自己的序列号seq变成x+1。
第三次握手的重要之处在于:它完成的是服务端方向的确认。经过前两次握手,客户端已经确认了自己能发、服务端能收,服务端也确认了自己能发、客户端能收?不对,严格来说,服务端此时还不确定“自己的SYN+ACK是否成功送到了客户端”。如果客户端没收到SYN+ACK,它就不会发第三次ACK,服务端就只能在SYN_RECV里苦等。所以这最后一个ACK,本质上是给服务端吃的那颗定心丸。
三次握手结束后,双方才同时确认了四个事实:我能发,你能收;你能发,我能收。这时连接才真正进入ESTABLISHED状态。
这也是为什么第三次握手的ACK包一旦丢失,服务端会超时重发SYN+ACK,而不是双方就此进入ESTABLISHED——因为在服务端视角,自己的宣告还没得到确认,连接就不算真正建立。
2.4 追问停在这里:为什么不能是两次握手
这个问题几乎每次必问。我的建议是别背标准答案,直接把场景推演出来。
如果只有两次握手,客户端发SYN,服务端回SYN+ACK,服务端就认为连接建立了,会立刻分配资源、进入ESTABLISHED状态,开始等待数据。问题是,客户端收到SYN+ACK之后才知道连接建立,但服务端并不知道这个SYN+ACK到底有没有到达客户端。
考虑一个更危险的情况:网络中滞留了一个很久以前发出的SYN包,因为网络延迟现在才到达服务端。服务端收到后回了一个SYN+ACK,然后开始苦苦等待永远不会来的数据。客户端根本没有发起连接的意图,却因为一个过期报文,在服务端留下了一个半开的废连接,占用着内存和端口资源。这就是两次握手的致命缺陷——它让服务端在无法确认对端是否真的在线的情况下,就提前进入了可用状态。
三次握手则多了一道“反向确认”的关卡,客户端没确认,服务端就不建立完全体连接,最多停留在SYN_RECV的超时等待里,合法握手和垃圾报文的命运也因此被区分开。
2.5 再多问一步:为什么不用四次握手
这个问题面试官不常问,但如果你能在回答里主动提一句“其实握手不需要四次”,会显得思考更完整。
因为三次握手已经在信息上达到了闭环。第一次握手让服务端确认客户端的发送意愿,第二次握手让客户端确认服务端的发送意愿,第三次握手让服务端确认客户端的接收意愿。所有需要双向确认的信息都到位了,第四次握手并不会增加任何新的确定性。
有人会说“客户端再回一个确认服务端不就更放心了吗”,可第三次就是那个确认。再多一次,只是把同样的信息重复一遍,徒增一个RTT的时延。要知道,每多一次握手,建立连接的耗时就会增加一个网络往返周期,这对延迟敏感的业务是不可接受的。所以协议设计者选了“最少但完整”的三次,而不是冗余的四次。
3. 四次挥手拆解:半关闭、FIN与2MSL的真相
3.1 挥手全程的状态流转
如果说三次握手是慢慢建立一条双向通路,四次挥手就是把这条双向通路一边一折地拆除。默认场景下,客户端主动发起关闭,完整过程是这样的:
- 客户端发FIN包,序列号为u,表示“我的数据发完了,不会再发了”,同时进入FIN_WAIT_1状态。
- 服务端收到FIN后,回一个ACK,确认号ack=u+1,然后进入CLOSE_WAIT状态。这里体现的就是TCP的半关闭特性——客户端关了发送方向,但服务端可能还有数据要发,接收方向仍然保留。
- 服务端在CLOSE_WAIT里处理完剩余数据,发送自己的FIN包,序列号为v,然后进入LAST_ACK状态,意思是“我这边也发完了,准备关闭”。
- 客户端收到FIN后,回一个ACK,确认号ack=v+1,然后进入TIME_WAIT状态。服务端收到这个ACK后关闭,客户端则在TIME_WAIT里等上一段时间再彻底关闭。
看到没有,四个包,两个方向各一对FIN+ACK。这跟握手本质上是同一种模式,只不过拆的时候每个方向都要单独确认。
3.2 四次 vs 三次:FIN和ACK为什么不一定能合并
很多人在这一点上很困惑:既然挥手时服务端是先回ACK、再发FIN,为什么不合并成一次,学握手里的SYN+ACK?
关键在于半关闭。TCP允许一端在完成发送任务后关闭发送方向,同时继续接收数据。服务端收到客户端的FIN,只是宣告“你不再发数据了”,但服务端自己可能还有一批数据没发完,比如一个响应报文还在传输中。它必须等那些数据都发完,才能发自己的FIN。所以在绝大多数场景里,FIN和ACK天然是分开的两个包,各自的时机不同。
只有在一种特殊情况下它们能合并:服务端收到FIN时,恰好也没有数据要发,这时它可以立刻一起回FIN+ACK,整个挥手过程就会从四步缩成三步。我在抓包时经常见到这种情况,尤其是在测试环境里,客户端访问一个返回空响应的简单服务时,挥手常常只有三个包。所以严谨的说法应该是“TCP挥手通常为四次,双方恰好都没有待发数据时可以合并为三次”。
3.3 主动关闭方为什么还要等2MSL
四次挥手不是踢完了最后一脚球就散场,主动关闭方踢完最后那个ACK后,还要在场边独自等待2MSL才能离场。这个等待就是TIME_WAIT状态。
2MSL指的是两个报文最大生存时间,MSL是Maximum Segment Lifetime,报文在网络上存活的最长时间。TCP协议规定IP报文有生命周期,过了这个时间还没到达就会被丢弃。TIME_WAIT中的2MSL,指的是一去一回两个方向上可能残留报文的最大时间。
这样做有两个目的。
第一,确保最后一个ACK能被服务端收到。如果这个ACK在网络中丢失了,服务端因为一直没收到确认,会超时重发FIN。此时客户端还处于TIME_WAIT,听到重发的FIN后可以再补发一个ACK。如果客户端收到FIN后立刻关闭,那重发的FIN就无人应答了,服务端会一遍遍重传直到放弃,甚至误以为客户端掉线。
第二,让旧连接的报文在网络上彻底消失。TIME_WAIT持续2MSL,足够让前一个连接里所有挣扎在途的报文全部消亡。否则,一条新连接刚好复用了同一个四元组,接收方可能会把一个迟到的旧数据包误当成新连接的数据,造成不可预知的结果。
所以TIME_WAIT不是无意义的空等,它是用一段确定的时间,换取了确认可靠性和新旧连接隔离的确定性。
3.4 最后丢包怎么办:挥手失败时的核心兜底
顺着上面的逻辑,可以推演出挥手丢包的种种表现。
如果客户端的最后一个ACK丢了,服务端会重复发送FIN。客户端因为还在TIME_WAIT,会再次响应ACK,这个循环会持续到服务端重传次数耗尽或者成功收到ACK为止。
如果服务端的FIN丢了,客户端会一直停留在FIN_WAIT_2状态,等待一个永远不来的FIN。这个过程同样由内核的超时机制兜底,最终可能关闭连接并释放资源。
在实际业务里,CLOSE_WAIT堆积是最常见的问题。服务端收到客户端的FIN后进入了CLOSE_WAIT,正常情况下应该很快处理完业务数据然后发FIN,但如果应用程序忘记调用关闭接口,或者代码逻辑阻塞在某个地方没进行收尾处理,服务端就会一直停在CLOSE_WAIT,大量连接堆积,最终导致资源耗尽。每次有人拿着“CLOSE_WAIT暴涨”的截图来问我,我第一反应都是:先查应用层为什么没有关闭连接,而不是急着调内核参数。
4. 从握手到排障:面试官后续追杀的高频追问
4.1 SYN Flood:半连接队列被打满之后
三次握手给安全带来一个天然的瞄准点。攻击者可以伪造大量源地址,向目标服务端发送SYN,但永远不完成第三次握手。服务端的每个SYN都会占用一个半连接队列的位置,这些半开连接就像永远不付款的订单,一直占着仓库的资源。
半连接队列,也就是SYN队列,专门存放SYN_RECV状态的连接;与之相对应的是全连接队列,也叫Accept队列,存放已完成三次握手等待应用层accept的连接。SYN Flood攻击的核心效果就是把半连接队列塞满,让正常用户的SYN根本排不上队,表现为“服务不可用”。
防御手段里最典型的是SYN Cookie。服务端不老实为每个SYN分配队列资源,而是把连接信息加密编码成一个Cookie放在SYN+ACK里回给客户端,等客户端回ACK时再验证Cookie。这样攻击者的半连接不再吃资源,合法连接又能正常建起来。这个机制如今在内核里默认就是开着的。
这个追问在面试里的价值在于:如果你只背“三次握手是SYN、SYN+ACK、ACK”,你是无论如何也推导不出攻击原理和防御思路的;只有真正理解了吗为什么服务端需要为每个SYN停留在一个半连接状态,才能理解为什么队列会被打满,又为什么Cookie能化解这个矛盾。
4.2 握手超时重传:内核是怎么替应用层兜底的
握手过程中如果某个包丢了怎么办?TCP没有直接放弃,而是启动了重传机制。
比如第一个SYN发出去后,客户端迟迟没有收到SYN+ACK。一般情况下,1秒后客户端会重发SYN,之后等待时间翻倍,2秒、4秒、8秒……呈指数退避,直到重传次数达到上限。这就是为什么你看到一个“连接超时”的报错,往往不是瞬间出现,而是要诡异地等上好几秒甚至几十秒。
第三次ACK丢失时,服务端那边则会在SYN_RECV状态反复重发SYN+ACK。这里有个很容易混淆的知识点:向一个没监听端口的地址发SYN,会收到RST,表现为“Connection refused”,秒拒;向一个有监听但网络丢包的地址发SYN,则是沉默,表现为“Connection timed out”,干等。面试里如果你能顺口把这两个表现区分开,会非常加分——因为能区分“被拒绝”和“超时”,说明你真的懂RST和重传是两套完全不同的机制。
4.3 TIME_WAIT堆积:短连接业务的头号痛点
线上问题上,TIME_WAIT比CLOSE_WAIT更容易引起警觉。TIME_WAIT出现在主动关闭连接的一端,频率最高的场景是客户端,或者更常见的,是处在流量入口位置的代理和负载均衡。
如果一个服务大量使用短连接,每处理完一次请求就断开一条连接,那么服务端一侧会堆积大量TIME_WAIT,持续约60秒。单看状态数量并不致命,真正致命的是另一层风险:TIME_WAIT期间同一四元组不能被复用,一个服务对外连接数量有限,如果连接端口耗尽,新请求就只能排队或者失败。
解决思路分两个层次。第一层是治本,让客户端复用连接,用长连接替代短连接,减少断开频率,这是我最推荐的方式。第二层是调参数,比如开启tcp_tw_reuse,它允许客户端在特定条件下复用处于TIME_WAIT的端口,前提是对端也要支持TCP时间戳,并且只在主动发起连接的一侧有意义。坦白讲,这些参数不是万能药,如果在不理解的前提下乱开,反而可能引出时序错乱的新问题。面试时能把“为什么要谨慎”说出来,比我听到“把TIME_WAIT改成0”之类的说法要高出一个段位。
4.4 粘包拆包与Nagle:字节流模型引发的变体题
面试官经常会把粘包拆包跟三次握手放在同一个话题里聊。表面上是两个问题,内核其实是一个:TCP是字节流协议,没有消息边界。
三次握手之后,客户端连续发送两个业务报文“AAA”和“BBB”,接收方可能一次性读到“AAABBB”,也可能分几次读到各种片段。不是TCP故意要拼在一起,而是它只保证字节的顺序和完整性,不负责帮你划出每条应用消息的边界。就像一条河流,水是连贯的,河面上飘的每一件货物之间的距离,物流公司不管,发货方和收货方自己得约定。
解决方式也很固定:要么在每个消息前加长度字段,用4个字节标记后面内容的长度;要么用特殊分隔符切分;要么干脆固定长度。设计应用层协议时,这是基本功。
和粘包经常一同出现的还有一个名字叫Nagle算法。它的初衷是减少网络上小报文的个数,把多个小数据攒一起再发。这对交互性要求高的场景几乎是灾难,所以很多框架默认会开启TCP_NODELAY来禁用Nagle,让数据立即发送。被问到粘包时主动关联一下Nagle,能看出来你对“传输效率与实时性如何取舍”是有概念的。
4.5 TCP Keep-Alive与HTTP Keep-Alive:最容易偷换概念的一题
这道题出现得更隐蔽,很多面试官会故意把TCP Keep-Alive和HTTP Keep-Alive放在同一句话里,看你会不会踩坑。
TCP Keep-Alive是传输层的机制,作用是在连接空闲一段时间后,主动发送探测包确认对端是否还活着。默认情况下它属于可选能力,Linux的默认空闲时间是2小时,大多数业务场景其实感知不到它的存在。它解决的是“死链检测”问题。
HTTP Keep-Alive是应用层的机制,也叫HTTP长连接,作用是在一条TCP连接上连续传输多个HTTP请求和响应,避免频繁握手挥手带来的开销。它解决的是“连接复用”问题。
两个名字长得像,位置和作用完全不同。面试时你不需要说得特别复杂,只要先跟面试官确认“你说的Keep-Alive是指传输层的还是应用层的,这两个不一样”,然后分别展开,就足以证明你没有把概念混成一锅粥。
5. 抓包验证一次握手与挥手:实操演示的完整过程
5.1 实验环境与命令准备
背了这么多,不如亲手抓一次包。我在给新人培训时总是强调,只要能自己抓到一次三次握手和四次挥手,那些状态名就会从纸面上的字母变成看得见摸得着的时间线。准备方法很简单,一台Linux机器,装上tcpdump和netcat。Windows机器也可以用Wireshark抓,但命令行下用Linux最直观。
开三个终端。第一个终端起抓包:
sudo tcpdump -i lo tcp port 8080 -w handshake.pcap第二个终端起服务端:
nc -l 8080第三个终端发起连接:
curl http://127.0.0.1:8080/这样会得到一个最小的TCP会话。等curl执行完,在抓包终端按Ctrl+C结束抓包,然后读取结果:
tcpdump -r handshake.pcap -nn5.2 三次握手抓包逐行解读
正常抓包结果里,前三行会是这样:
IP 127.0.0.1.12345 > 127.0.0.1.8080: Flags [S], seq 123456789, ... IP 127.0.0.1.8080 > 127.0.0.1.12345: Flags [S.], seq 987654321, ack 123456790, ... IP 127.0.0.1.12345 > 127.0.0.1.8080: Flags [.], ack 987654322, ...第一行是SYN,seq是一个很大的随机数。第二行是SYN+ACK,注意它同时回了一个新seq和自己的ack——ack等于客户端的seq加1。第三行是纯ACK,带上了对服务端seq的确认。
这里有个很坑的细节。Wireshark默认开启“Relative Sequence Numbers”,把seq显示成0、1这种小数字,方便人阅读,但也很容易让人误以为初始序列号就是0。看绝对抓包数据时,seq其实是一长串随机数。如果你在面试里主动提这一句“抓包工具默认做了相对序列号转换,实际ISN是随机的”,面试官通常会眼前一亮。验证时也可以在Wireshark里关掉视图偏好里的相对序列号选项,立刻就能看到原始序列号。
5.3 四次挥手抓包逐行解读
连接建立并传输完数据后,curl进程会主动关闭连接。抓包末尾会出现类似四行内容:
IP 127.0.0.1.12345 > 127.0.0.1.8080: Flags [F.], seq 123, ack 456, ... IP 127.0.0.1.8080 > 127.0.0.1.12345: Flags [.], ack 124, ... IP 127.0.0.1.8080 > 127.0.0.1.12345: Flags [F.], seq 456, ack 124, ... IP 127.0.0.1.12345 > 127.0.0.1.8080: Flags [.], ack 457, ...注意,这里的第一行是FIN+ACK,第二行是ACK,第三行又是FIN+ACK,第四行是ACK。FIN包为什么常带ACK?因为数据发送过程本身会产生确认号,FIN包需要捎带传输过程中尚未确认的字节信息。这又印证了前面说的:挥手常被说成四次,实际抓包时FIN几乎总是和ACK一起出现。
如果你在抓包结束后立刻执行netstat -t,还能看到客户端的TIME_WAIT状态出现在那条连接的四元组上。这个状态从开始到消失大约要60秒,正好对应2MSL的时间窗。
5.4 抓包中常见的三类异常特征
抓包的主要价值在于排障,这里分享三类我线上看得最多的异常特征。
第一类,同一seq的SYN反复出现,且间隔越来越长。这说明第一个SYN发出去后石沉大海,客户端按指数退避在重传,大概率是网络丢包,或者是中间设备悄悄丢弃了SYN。
第二类,收到RST。RST表示“我不想继续了,直接重置”。出现RST时,没有重传、没有协商,连接被立刻打断。常见原因包括:访问的端口没人监听、防火墙主动拒绝、对端应用层异常强行关闭。线上如果大量出现RST,需要重点排查是不是有探测流量或者超时踢连接的安全策略。
第三类,FIN_WAIT_1或CLOSE_WAIT持续堆积。前者需要抓包看 FIN_WAIT_1 的对端是否回ACK,如果没回,多半是丢包;后者则大多数时候是应用层代码问题。抓包看到的是现象,但定位时要分清“网络层异常”和“应用层不关闭”两条线。
6. 面试拿高分的回答框架:我筛选候选人时真正看重的东西
6.1 背状态机的答案和讲原理的答案差在哪
做个对比,低分答案大概长这样:“客户端发送SYN,进入SYN_SENT;服务端收到后发SYN+ACK,进入SYN_RCVD;客户端收到后发ACK,进入ESTABLISHED。”信息没错,但没有说服力。
高分答案会长这样:“第一次握手,客户端宣告我要连,序列号是x;第二次握手,服务端既确认收到,又宣告自己的序列号y;第三次握手,客户端再确认收到y。整个过程只有一个目标,就是让双方都确认‘你能收、我能发’这个双向能力。所以必须有三次,两次不行,因为服务端无法确认自己的宣告是否被客户端收到;四次又太多,因为第三轮已经完成信息闭环。”
后一种回答,即使面试官当时不在状态,也会不由自主地点点头。它不是在复述过程,而是在解释设计意图。
6.2 我建议的四层回答框架
面对这个问题时,我会建议候选人用“一次交互、双向确认、资源释放、异常容错”四层来组织表达。
第一层,一次交互,说清楚三次握手是建立双向通道:TCP是全双工的,连接代表两个方向的数据通路,每条通路都要单独确认。
第二层,双向确认,推导为什么是三次:分别阐述第一次握手确认了客户端的发送、服务端的接收;第二次握手确认了服务端的发送、客户端的接收,同时服务端收到客户端ISN;第三次握手让服务端确认客户端收到了自己的SYN+ACK,至此双向闭环。
第三层,资源释放,讲清四次挥手:发送方向关一个、确认一个,接收方向同理。服务端收到FIN后可能还有数据继续传,所以FIN和ACK不一定能合并,这是挥手比握手多的根本原因。
第四层,异常容错,主动提到TIME_WAIT:最后一个ACK要保证送达,还要保证旧报文不会串入新连接,所以主动方必须等2MSL。
把这一套讲下来,大约两分钟。面试官在这两分钟里听到的不只是答案,而是你思考网络协议的完整方式。
6.3 把ISN、状态机、异常场景串起来讲
如果临场被追问细节,再往里面加料。比如,面试官问“seq为什么是大随机数”,你可以立刻接上:它既要为字节流排序提供基准,又要避免新旧连接报文混淆,所以内核用随机化算法生成ISN。
问你“第三次握手丢包了什么表现”,你可以答:服务端一直处于SYN_RCVD,会重发SYN+ACK,客户端此时已经ESTABLISHED,如果后续它先发数据,数据到了服务端,反而会让服务端顺势确认连接。这个背靠背的场景推演,比单纯说“会重传”要立体得多。
讲完这些,你已经不是“背题”,而是“在讨论一个真实运行的系统”。面试氛围会从考试变成探讨,这恰恰是我最希望看到的状态。
6.4 一些细节:别在这些小问题上栽跟头
最后提醒几个容易被忽略的小细节。
第一,主动关闭方不一定是客户端。任何一端,只要需要断开连接,都可以主动发FIN。服务端主动断开的情况也很常见,比如HTTP服务超时踢掉闲置连接。面试时如果脱口而出“客户端先发FIN”,会被看作理解不完整。
第二,FIN包通常带ACK。有人说挥手的第一步就是“FIN”,但抓包工具显示的大概率是FIN+ACK,别为这个细节和面试官硬杠。
第三,挥手不能叫“四次握手”。我听过不少候选人说“关闭连接也是四次握手”,每次听到都忍不住皱眉。握手和挥手是两个方向完全不同的机制,一个在建立双向确认,一个在拆除双向通道,用词要准确。
第四,CLOSED状态应该放在整个状态机的最外围,而不是某个节点上。很多人在白板上画状态迁移图时,CLOSED位置画错,印象分会掉一大截。
这些年面试下来,我最深的感受是:真正的能力不在于记住多少名词,而在于能不能把每个设计选择背后的理由讲清楚。TCP三次握手与四次挥手这道经典题,正是检验这种能力最好的试金石。它看起来是最基础的知识,但能把基础讲出层次的人,处理线上复杂问题的时候通常也不会差。把这篇里的逻辑理顺,再自己抓一次包验证一遍,这顿饭就吃透了。