面试问到TCP三次握手和四次挥手,基本是网络方向必考题。我帮不少候选人做过模拟面试,发现一个非常普遍的现象:大家都能把“第一次SYN、第二次SYN+ACK、第三次ACK”这些流程背得滚瓜烂熟,但一旦被追问“第三次握手丢了会怎样”“为什么挥手必须是四次”“TIME_WAIT是哪一方出现的”这类问题,十有八九会卡壳。这篇文章我打算把这个话题彻底讲透,不只是给一份背诵版答案,而是把三次握手和四次挥手背后牵扯到的状态机、队列机制、参数调优、抓包验证、高频追问全串起来。不管你是正在准备面试的求职者,还是想补网络基础的开发者,这篇文章都适合你,看完之后再去面试,至少网络这个模块不会被问倒。
既然标题写了“全网最细”,那我不光要把流程讲清楚,还会把很多面试官自己都未必讲得明白的细节拿出来拆解,包括SYN Flood攻击的原理、半连接队列与全连接队列的差异、TIME_WAIT为什么那么“倔强”、RST和FIN在什么场景下出现、怎么用抓包来验证理论,这些内容我会一点一点展开。
1. 先把“连接”这层窗户纸捅破
很多人学TCP三次握手,上来就背“SYN、SYN+ACK、ACK”,但从来没有认真想过一个问题:TCP的“连接”到底是个什么东西?
TCP连接并不是一条真的物理线路,它本质上是通信双方各自在内核里为这次通信分配的一组资源。这些资源包括:发送缓冲区、接收缓冲区、连接状态、初始序列号。可以理解为两个办公室之间并没有拉一根电话线,而是约定了一套邮寄规则和编号方案,每个包裹上写明“我是第几号包裹、我收到了你的第几号包裹”,双方根据这些编号来排序、去重、确认。TCP连接是否“建立”,取决于这套约定是否完成了双向确认。
这里有一个关键点:TCP是全双工的。也就是说,A可以同时给B发包,B也可以同时给A发包,两个方向互不干扰。因此,连接建立时,双方都需要确认“我能发、你能收”“你能发、我能收”。如果用一句话总结三次握手的本质,那就是:双方交换初始序列号,并确认双方的收发能力都是正常的。
1.1 每次握手携带了什么核心信息
三次握手的每条报文里,最核心的字段是seq(序列号)和ack(确认号)。很多人把这两个字段当成死记硬背的编号,其实它们是有逻辑的。
比如客户端初始化一个随机序列号x,发送SYN包时,seq=x。服务端收到后,知道“对方将从x开始编号”,于是自己的确认号写成ack=x+1,意思是“我已经收到你的x,下一次我需要你从x+1开始发”。同时服务端也会把自己的初始序列号y放进SYN+ACK包里。客户端收到后,回复ACK包,seq=x+1,ack=y+1。
可以看到,整个序列号体系是环环相扣的。确认号永远表示“你下一个要发的字节编号”,而seq表示“我这个包第一个字节的编号”。面试官如果让你画一个三次握手的报文过程,这些细节一个都不能错。
1.2 为什么初始序列号必须是随机数
早年的TCP实现里,ISN(初始序列号)是固定从0开始的,后来发现这有严重的安全隐患。如果ISN可预测,攻击者就能伪造一个带正确序列号的RST包,直接把别人的TCP连接切断,这就是经典的TCP序列号预测攻击。
现在的Linux内核里,ISN是基于一个和连接四元组、时间相关的哈希计算出来的随机值。随机ISN还有一个附带好处:如果一个旧连接的数据包迟迟才到达,新连接可以更容易识别出这不是本次连接的合法报文。这个细节在面试里问出来,能直接拉开和其他候选人的差距。
1.3 状态机与两张队列:服务端承受压力的两个地方
三次握手和四次挥手过程中,TCP是有状态的,状态转移是面试必考内容。这里列一张表,把主要状态和含义整理清楚:
| 状态 | 含义 | 出现阶段 |
|---|---|---|
| LISTEN | 服务端监听端口,等待建连 | 握手前 |
| SYN_SENT | 客户端已发送SYN,等待确认 | 握手第一次后 |
| SYN_RCVD | 服务端已收到SYN并回复,等待最终ACK | 握手第二次后 |
| ESTABLISHED | 连接建立成功,可以传输数据 | 握手完成后 |
| FIN_WAIT_1 | 主动关闭方已发送FIN | 挥手第一次后 |
| FIN_WAIT_2 | 主动关闭方已收到对方ACK | 挥手第二次后 |
| CLOSE_WAIT | 被动关闭方收到FIN,等待应用层close | 挥手第二次后 |
| LAST_ACK | 被动关闭方已发FIN,等待最终ACK | 挥手第三次后 |
| TIME_WAIT | 主动关闭方等待2MSL后彻底关闭 | 挥手第四次后 |
这张表建议截图保存,面试前反复看。
除了状态,面试中经常出现的还有两个内核队列,很多人对它们认识非常模糊:半连接队列和全连接队列。
半连接队列,也叫SYN队列,专门存放那些“已经收到SYN、但还没完成三次握手”的连接。服务端收到SYN后,会把这个连接放入SYN队列,然后回复SYN+ACK,等待客户端最终的ACK。注意,此时连接还没有建立,只是“半开”状态。
全连接队列,也叫accept队列,存放的是“已经完成三次握手、但还没被应用层调accept()取走”的连接。当客户端第三个ACK到达,服务端会把这个连接从半连接队列移动到全连接队列,等待进程处理。
这两个队列的大小和作用完全不同,也是理解SYN Flood攻击的关键。面试如果聊到高并发,八成会从这里切入。
2. 三次握手:为什么少一次都不行
三次握手的标准流程是:
- 客户端发送SYN包,seq=x,进入SYN_SENT状态。
- 服务端收到后回复SYN+ACK包,seq=y,ack=x+1,进入SYN_RCVD状态。
- 客户端收到后发送ACK包,seq=x+1,ack=y+1,双方进入ESTABLISHED状态。
很多新手会有个疑问:为什么不能只握两次手?为什么客户端发一次SYN、服务端回一次ACK就结束?这个问题恰恰是面试官最常追问的。下面把两次握手方案的漏洞摆出来。
2.1 只握两次手,无法阻止历史连接
假设客户端发起了一个连接请求,但这个SYN包因为网络拥塞,在网络上滞留了很久,超过了正常建连的等待时间。客户端等不到回应,就放弃了这次连接,重新发起一个新的SYN。结果旧的那个SYN包这时候才到达服务端。
如果只有两次握手,服务端收到这个迟到的SYN,会认为是一个新连接请求,立刻回复ACK,然后进入ESTABLISHED状态,分配资源等待客户端发数据。但客户端知道这个连接是自己已经放弃的旧请求,根本不会理睬,服务端就这样白白耗费了资源和端口。
有了第三次握手,情况就完全不同。客户端收到服务端对旧SYN的ACK后,判断这个连接的seq/ack对不上,于是发送一个RST包来终止这个连接,服务端收到RST后就会释放资源,不会造成浪费。
2.2 三次握手能确认双方的收发能力都正常
只握两次手,有一半能力是确认不了的。
一次握手中,客户端发SYN,服务端能确认“客户端的发送能力正常,并且我能收到包”。但服务端回ACK,客户端收到后,只能确认“服务端的发送能力正常,并且我的发送能力已经被对方收到”。可“服务端的接收能力怎么样?”“客户端能不能收到服务端发的包?”这两个问题,第二次握手根本确认不了。
用生活场景类比:A给B发了条消息,B回了“收到”。A知道自己的消息B能看到,也知道B能回消息,但B是否真的收到了A的全部信息?B是否理解A的意思?A并不知道。必须等A再回一条“你的回复我也收到了”,双方才算建立了共识。这就是三次握手的意义。
三次握手完成后,双方都能确认:我能发、你能收、你能发、我能收。这正是全双工通信的最低确认条件。
2.3 只握两次手,也为SYN Flood留下了更大的口子
假设只有两次握手,服务端发完ACK就直接建立连接并分配资源。攻击者可以伪造大量源IP,不断发送SYN包,服务端每收到一个都回复ACK并分配资源,攻击成本极低,服务端资源却迅速耗尽,正常用户完全无法连接。
而三次握手设计下,服务端第一次握手后进入SYN_RCVD状态,资源其实也已经分配了,所以依然存在被攻击的可能,这就是经典的SYN Flood攻击。不过相比两次握手,三次握手至少让攻击者需要处理应答包、或者利用伪造IP的方式来完成攻击,后续还有SYN Cookie等机制来缓解,这在后面展开。
2.4 第三次握手丢了会发生什么
这是一个非常有区分度的面试追问。第三次ACK如果丢了,服务端处于什么状态?它不会傻傻等着,而是会重传SYN+ACK包。这个重传次数由net.ipv4.tcp_synack_retries参数决定,Linux默认一般是5次。如果重传多次仍然收不到客户端的ACK,服务端会放弃这次半连接,释放SYN_RCVD状态下的资源。
但这里有个有意思的细节:如果此时客户端已经进入ESTABLISHED状态,并且开始发送数据,那客户端发的数据包本身会带一个ACK号,这个ACK号正好能起到第三次握手的作用,服务端就会顺势完成握手,进入ESTABLISHED状态。所以第三次握手的丢失不一定是致命的,只要客户端后续有数据通信,连接还是能建起来;如果没有数据交互但应用层一直在等待,那就会一直等到服务端重传超时。
2.5 SYN Flood攻击与本地防护手段
SYN Flood攻击的核心,就是让服务端的半连接队列被填满。一旦半连接队列满了,新的SYN包就算合法,也会被内核丢弃,服务表现为“端口活着,但永远无法建立新连接”。
Linux针对这个问题有一套组合拳:
- 开启tcp_syncookies。当半连接队列满了之后,内核不再保存这个半连接的状态,而是把连接信息加密后编码在SYN+ACK包的seq字段里。客户端回ACK时带上这个值,服务端解码后直接恢复出连接信息,从而达到不占用队列也能完成握手的效果。这个参数在现代Linux里默认就是开启的。
- 适当调大tcp_max_syn_backlog,可以理解为半连接队列的上限。但要注意,调大只是拖延被攻陷的时间,并没有根治问题。
- 降低tcp_synack_retries,让服务端对半连接的重传次数减少,加快释放无效半连接。
网上很多人会说“直接用tcp_syncookies=1就完了”,实际操作里它也不是万能的,因为SYN Cookie绕过了半连接队列,但一旦握手完成,连接还是要进入全连接队列。如果全连接队列也满了,服务端同样会开始丢包。所以真正抗住攻击,往往需要网络层、系统层和应用层一起做防护,单靠某个参数并不能解决所有问题。
3. 四次挥手:状态流转与TIME_WAIT的坑
三次握手解决了“如何建立连接”的问题,四次挥手解决的是“如何安全断开连接”的问题。挥手比握手多一次,核心原因是TCP的全双工特性:两个方向各自独立关闭,谁都不依赖谁。
举个例子。A对B说“我要关闭了,我不发数据了”,这是第一个FIN。B收到后回复“我知道了”,这是第一个ACK。但这个时候B可能还有数据要发给A,B的发送通道还没有关闭,所以B不能立刻也说“我也关闭”,必须等自己的数据全部发完,再单独发一个FIN。A再回复ACK,四个包齐了。所以整个挥手过程天然就是四段:关闭请求、关闭确认、对端关闭请求、最终确认。
3.1 挥手过程中谁在哪个状态
假设A是主动关闭方,B是被动关闭方:
- A发送FIN包,进入FIN_WAIT_1状态。
- B收到FIN,回复ACK,B进入CLOSE_WAIT状态。A收到这个ACK,进入FIN_WAIT_2状态。
- B的应用程序完成收尾工作后调用close(),B发送FIN,进入LAST_ACK状态。
- A收到FIN,回复ACK,A进入TIME_WAIT状态。B收到ACK后关闭连接。
需要特别强调一点:TIME_WAIT只出现在主动关闭方,也就是那个先发FIN的进程所在的机器。被动关闭方收到ACK就直接关闭了,不需要等待。很多面试者把TIME_WAIT当成“双方都要经历的状态”,这是常见的错误。
另外还有一个细节:B在收到A的FIN并回复ACK之后,进入的是CLOSE_WAIT状态,但它并不是收到FIN后就会立刻发FIN。B什么时候能发FIN,完全取决于上层应用。如果应用层不调用close(),B会一直停在CLOSE_WAIT,A则一直停在FIN_WAIT_2,这就是线上CLOSE_WAIT堆积的直接原因。
3.2 TIME_WAIT为什么要等2MSL
TIME_WAIT是整个TCP中“看起来最浪费”的状态,所以面试官特别爱问。答案要从两个角度来说。
一是为了确保最终ACK能够被对方收到。A发出第四次握手的ACK后,这个包有可能在网络中丢失。B没收到ACK,会认为是自己的FIN丢了,于是会重传FIN。A必须停留在TIME_WAIT状态,才能收到B重传的FIN并重新回复ACK。如果A发完ACK就直接关闭,那么一旦ACK丢失,B会不断重传FIN,直到超时放弃,连接关闭流程将会异常。
二是为了保证旧连接的迟到报文不会污染新连接。2MSL,也就是两倍最大报文生存时间,足以让一个数据包在网络中存活足够久并自然消失。A只有在TIME_WAIT状态等待2MSL,才能确保旧连接的所有报文都在网络中消亡,不会干扰后续新的连接。
这里给一个直观的数字:Linux里MSL通常被认为在30秒左右,所以2MSL大约60秒,Linux中TIME_WAIT状态的保持时间接近net.ipv4.tcp_fin_timeout参数,默认就是60秒。不过不同操作系统的具体实现并不一致,这个数值也有差异,面试时先说清楚理论计算,再补一句“实际以系统实现为准”,会显得严谨很多。
3.3 被动关闭方没有TIME_WAIT,那CLOSE_WAIT堆积怎么排查
TIME_WAIT并不可怕,它是TCP正常关闭的一部分。真正让运维头痛的,是CLOSE_WAIT堆积。很多业务应用在处理请求时,只关注了读数据,忘了在错误分支或者超时分支里关闭连接,导致对端FIN已经发过来了,本端却迟迟不close,CLOSE_WAIT越积越多。
我曾经排查过一个线上接口,进程的句柄数持续上涨,连接不可用,netstat一看,几千个CLOSE_WAIT。最后定位到是代码里有一段读取响应没有设置超时,导致线程卡住,走不到close()。这种问题从系统层面无法根治,只能从应用层去修。
排查CLOSE_WAIT的标准姿势是先确认数量:
ss -ant | awk '{print $1}' | sort | uniq -c然后查看这些连接的对端是谁、是哪个进程持有:
ss -antp | grep CLOSE_WAIT lsof -n | grep CLOSE_WAIT如果CLOSE_WAIT的IP都集中在少数几个对端,大概率是对方发来FIN后,某个下游服务没有正确关闭连接。如果IP很分散,反而是本身服务代码没有正确管理连接生命周期。
3.4 大量TIME_WAIT的优化方案
高并发短连接的场景下,主动关闭方会积压大量TIME_WAIT,端口和内存都可能紧张。这里的优化思路有几种:
第一种是开启tcp_tw_reuse。这个参数允许内核在满足一定条件时,把处于TIME_WAIT状态的连接重新分配给新的连接使用,前提是开启TCP时间戳选项。这个方案比较常见,但要注意它只适用于出站连接,对于真正需要监听的服务端场景,效果有限。
第二种是调大tcp_max_tw_buckets,可以理解为系统允许同时存在的TIME_WAIT连接数上限。超过上限后,系统会主动回收一些TIME_WAIT连接,但这样做有风险,可能影响TCP正常关闭流程。
第三种是最推荐的方式,从应用层改造,使用连接池或者长连接,从根本上减少主动关闭连接的频率。数据库连接池、HTTP Keep-Alive,本质上都是在复用TCP连接来规避TIME_WAIT。
还需要提醒一句,网上很多文章会提到tcp_tw_recycle这个参数,这个参数曾经用来快速回收TIME_WAIT连接,但它在网络环境复杂时会导致严重问题,而且在Linux 4.12之后已经被移除,现在的系统配置这个参数已经没有意义。面试时如果听到有人还在推荐tcp_tw_recycle,要留个心眼。
4. 实操:抓包看一遍,胜过背十遍
这一节我们来点实战。我自己带网络方向的人时,最推荐的做法是动手抓包,用肉眼看到三次握手和四次挥手的过程。纸上谈兵永远比不过一份真实的抓包文件。
4.1 用Python起一个最小TCP服务
先在本地起一个极简的TCP服务端,监听9000端口,收到一次连接请求后读一点数据,然后关闭连接。Python很适合干这个事。
服务端代码:
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9000)) server.listen(5) conn, addr = server.accept() print('receive connection from', addr) data = conn.recv(1024) print('received data:', data) conn.close() server.close()客户端代码:
import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 9000)) client.send(b'hello tcp') client.close()先启动服务端,再运行客户端,然后观察输出。这里的listen(5)是什么意思?它就是全连接队列的长度。很多人面试被问到backlog参数,会直接懵掉,其实listen的第二个参数主要就是限制完成握手但还没被accept取走的连接数。
4.2 tcpdump抓包看三次握手
在另一个终端里执行抓包命令:
sudo tcpdump -i lo port 9000 -nn -S这里有几个参数很关键:-i lo表示抓回环接口,因为我们访问的是127.0.0.1;-nn表示不做域名解析和端口转换成服务名,直接显示原始IP和端口;-S是精髓,它让tcpdump显示绝对序列号,而不是相对的seq,这样能直观看到seq和ack之间的关系。
跑完客户端之后,输出大概长这样:
IP 127.0.0.1.50001 > 127.0.0.1.9000: Flags [S], seq 123456789, win 64240 IP 127.0.0.1.9000 > 127.0.0.1.50001: Flags [S.], seq 987654321, ack 123456790, win 64240 IP 127.0.0.1.50001 > 127.0.0.1.9000: Flags [.], ack 987654322, win 64240第一行[ S ]就是SYN,第二行[ S. ]就是SYN+ACK,第三行[ . ]就是ACK。注意第二行的ack=第一行的seq+1,第三行的ack=第二行的seq+1。这就是前面讲的序列号和确认号逻辑,不用背,看一眼报文明白了。
4.3 接着抓挥手过程
在服务端代码里,conn.close()会触发主动关闭。抓包结果会有四条报文:
IP 127.0.0.1.9000 > 127.0.0.1.50001: Flags [F.], seq 987654322, ack 123456790 IP 127.0.0.1.50001 > 127.0.0.1.9000: Flags [.], ack 987654323 IP 127.0.0.1.50001 > 127.0.0.1.9000: Flags [F.], seq 123456790, ack 987654323 IP 127.0.0.1.9000 > 127.0.0.1.50001: Flags [.], ack 123456791第一条是FIN+ACK,服务端告诉客户端“我没数据发了,准备关闭了”;第二条是ACK,客户端表示收到;第三条是客户端的FIN,客户端也准备关闭;第四条是ACK,服务端确认。注意第一条报文带[F.]表示FIN和ACK同时置位,这是由于数据通道上顺带确认了之前的报文。这几条报文的seq/ack逻辑,建议自己对着上面的数字推一遍。
有人会问,为什么服务端close()之后发出的第一个包是FIN+ACK而不是单独的FIN?因为TCP报文的ACK标志是一直带上的,只要收到过对方的数据,回包就自然会携带ACK确认号。这个细节在面试中说清楚,会显得你真的上手抓过包,而不是只背了教科书。
4.4 用Wireshark看连接状态更直观
如果你不喜欢命令行,可以用Wireshark打开同样的抓包文件,然后在上方过滤栏输入:
tcp.flags.syn == 1 && tcp.flags.ack == 0这样只会显示第一次握手的SYN包。想同时看握手和挥手,可以输入:
tcp.flags.syn == 1 || tcp.flags.fin == 1在Wireshark里,每次握手的seq、ack以及对应的状态变化会显示得很清楚,还能看到TCP流的完整时序图。建议初学者同时用tcpdump和Wireshark各抓一次,对照着看。
抓包过程中还有个小技巧:启动服务端后,先用netstat确认监听状态:
netstat -ant | grep 9000客户端connect的过程中,再快速执行:
ss -antp | grep 9000就可以观察到服务端从LISTEN变成SYN_RCVD再变成ESTABLISHED的过程。这种“边抓包边看状态”的体验,比单纯看理论强得多。
5. 拓展面试题速答清单
这一节整理面试里出现频率最高的追问。每题给一个精炼的回答思路,顺着这个思路展开讲,基本都能过关。
5.1 高频问题精答
Q1:为什么建立连接是3次,断开连接却是4次?
因为建立连接时,服务端的SYN+ACK可以合并成一个包发送,两个方向的信息一次搞定。但挥手时,被动关闭方收到FIN后,可能还有数据没发完,不能立刻合并,所以ACK和FIN必须分开。如果被动方正好没有任何数据要发了,它的ACK和FIN理论上也可以合并,这时会出现三次挥手的情况,但标准实现下基本都是四次。
Q2:TIME_WAIT为什么是主动关闭方有,被动关闭方没有?
被动关闭方发出最后一个ACK后,连接就关闭了。它不需要重传,也没有遗留的报文需要清理。而主动关闭方需要保证自己最后的ACK能送达,还需要等待旧报文在网络中消失,所以必须停留在TIME_WAIT一段时间。
Q3:第三次握手丢了,连接最终还能建立吗?
分情况。如果客户端之后发送了数据包,数据包携带的ACK可以替代第三次握手,连接能建立。如果客户端一直不发数据,服务端会重传SYN+ACK,重传上限到了就丢弃半连接,连接失败。
Q4:第一次握手可以携带数据吗?
RFC 793规定SYN包不可以携带数据。如果SYN带数据,而服务端拒绝了这个连接,只能回复RST来终止,数据就会产生歧义。从安全角度看,SYN包携带数据也容易被用来放大攻击流量。第三次握手ACK是可以携带数据的,因为客户端此时已经确认服务端可用。
Q5:SYN Flood攻击怎么防?
依赖系统层面可以启用tcp_syncookies,调大tcp_max_syn_backlog,适当降低tcp_synack_retries;更完整的手段还涉及防火墙限速、DDoS高防、网络层清洗。真正的生产环境,单纯靠内核参数扛不住大流量攻击。
Q6:connect()返回成功后,服务端一定完成三次握手了吗?
不一定。客户端发出第三个ACK后,connect就返回了,但服务端收到这个ACK还有一个时间差。极端情况下,第三个ACK在网络中丢了,服务端可能还在SYN_RCVD。但如前所述,后续如果有数据包携带ACK,服务端会补齐建连。
5.2 进阶追问:RST、半关闭与连接故障排查
Q7:RST和FIN有什么区别?
FIN表示“我要正常关闭了,但我之前发出去的数据你慢慢处理”,是礼貌地协商关闭。RST表示“连接出错了,立刻终止”,双方所有缓冲数据全部丢弃,不重传不确认。浏览器里常见的“连接被重置”,本质上就是收到了RST包。触发RST的场景包括:客户端连接一个没有监听的端口、连接过程中某个进程崩溃、程序设置SO_LINGER为0后关闭连接。
Q8:什么是TCP半关闭?
半关闭是指只关闭一个方向的传输能力。调用shutdown(SHUT_WR)会发送FIN,但本端仍然可以接收数据。对于全双工的TCP来说,半关闭在“对端还需要继续发数据”的场景很有用。顺便一提,keepalive探测和半关闭不是一回事,keepalive是空闲时的探活机制,默认探测间隔要很久,不能依赖它快速感知断线。
Q9:服务端大量CLOSE_WAIT,根因是什么?
根因永远是应用层没有及时关闭socket。排查时确认CLOSE_WAIT数量和持有进程,再看代码里是否所有分支都覆盖了close或资源释放。遇到HTTP服务,还要检查连接复用超时配置是否合理。
Q10:如何查看TCP连接状态分布?
常用命令有netstat、ss和nstat。建议用ss因为它更快更准确:
ss -s # 查看全局TCP统计 ss -antp # 查看所有TCP连接及进程 ss -ant state time-wait # 只看TIME_WAITQ11:连接建立后,一方突然断电会怎样?
断电方没有机会发送FIN,对端会一直认为连接还活着。如果双方都不发送数据,这条连接可能保持几个小时甚至更久。真正触发回收的是TCP keepalive探测机制,当系统的tcp_keepalive_time到达后,内核会尝试探测,探测失败后连接才会被回收。默认空闲探测是2小时,所以线上应用通常需要自己做心跳机制,不能完全依赖操作系统的keepalive。
Q12:如果握手完成但应用层不调用accept,会发生什么?
连接会一直待在全连接队列中。队列满之后,新完成握手的连接无法入队,服务端在处理新SYN包时行为取决于具体实现,但客户端可能会表现成“connect成功,但服务端迟迟不回应数据”。排查时看ss -lnt里的Recv-Q,如果一直不下降,说明有连接堆积在accept队列里。
这12个问题在面试里已经覆盖了绝大部分考点。如果还能主动谈到序列号随机化、SYN Cookie原理、半关闭场景这些细节,基本就能让面试官认可你的网络功底了。
写在最后
我在带别人的时候,一直强调一个观点:TCP三次握手和四次挥手,不要把它当面试题来背,而是把它当协议设计来理解。抓包看到真实的报文字段之后,很多面试追问根本不需要死记硬背,因为seq、状态转移这些逻辑是会自然“浮”出来的。
最后再分享一个小技巧:空闲的时候可以自己在本地模拟一次断网或者直接kill掉进程,观察是FIN还是RST,再对比一下TIME_WAIT和CLOSE_WAIT的状态变化。这种实验多做几次,你对TCP的理解会和只刷八股文的人完全不一样。