news 2026/10/27 5:15:12

TCP三次握手与四次挥手:抓包排障与内核调优实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TCP三次握手与四次挥手:抓包排障与内核调优实战

做后端这些年,被问得最多的一类问题里,TCP三次握手和四次挥手绝对排得上前三。有意思的是,很多人背得滚瓜烂熟,一到真实场景就露馅:线上突然冒出一堆 TIME_WAIT 不知道怎么处理,看到 CLOSE_WAIT 上千个以为是内核参数没调好,抓包发现握手第三个包一直重传却不知道从哪儿查起。这几个状态字看着简单,背后牵着的是连接队列、序列号管理、端口复用、内核参数这一整套东西。

这篇文章想做的事情很明确:把 TCP 连接的建立与断开从"背答案"拉到"看得见"。前半部分讲清楚三次握手为什么必须三次、四次挥手为什么省不掉一次、TIME_WAIT 那段时间到底在等什么;后半部分是实操,用tcpdump和ss把一次完整的握手挥手抓下来逐帧对照状态机,再整理一份高频面试题和答题思路,最后给一张线上问题的速查表。

适合谁看:刚学完计算机网络、做题会但心里发虚的同学;工作中要写长连接服务、要处理连接数告警的后端和运维;以及准备面试、希望答出"为什么"而不是只答"是什么"的人。文中所有抓包和参数示例都来自我自己的实验环境(两台内网机器,端口随便挑的,IP 用 10.x.x.x 代替),你可以照着复现一遍。

1. 先把地基打牢:TCP 到底在解决什么问题

在拆握手之前,得先想明白 TCP 存在的理由。很多人一上来就记SYN、ACK,结果记完就忘,是因为脑子里没有"它要对付什么麻烦"这个锚点。TCP 面对的是一条不可靠的、会丢包、会乱序、会重复的传输通道,而它要向上面提供一条"看起来像一根独占的管子"的服务。这中间所有机制,都是为弥补这个落差。

1.1 从寄挂号信说起:面向连接到底意味着什么

我一直觉得用寄挂号信来类比 TCP 是最贴切的。你把一封重要文件寄出去,邮局会给你一张回执,对方签收后回执再寄回来;同时每封信都有编号,对方收到第 3 封没收到第 2 封,可以明确告诉你"重发第 2 封"。这个过程里,双方先要确认彼此的存在、确认收发地址没问题,然后才开始一封封地寄。

"面向连接"其实就是这么回事:通信双方在传数据之前,先在各自内核里为这条连接分配资源、协商参数、同步序列号,形成一个双方都认可的状态。这个状态不是虚无缥缈的东西,它在 Linux 里是一块实实在在的内存——struct sock,里面有接收缓冲区、发送缓冲区、重传队列、定时器。连接建立不起来,这块内存就不会分配;连接断了,这块内存要被回收。理解了这一点,你就能想明白为什么"两次握手"会出问题:服务端可能为一个根本不会到来的连接白分配了一块内存,还得一直守着。

顺带说清楚一个容易混淆的点:TCP 的"连接"是逻辑上的,不是物理上的专线。中间的路由器、交换机完全不知道你在"连接"什么,它们只看见一个个独立的 IP 包。所谓连接,是两端主机的内核共同维护的一份状态记录。这是后面所有讨论的前提。

1.2 序列号、确认号、窗口:三个必须刻进脑子的字段

TCP 头部 20 字节(含选项会更长),字段不少,但真正撑起握手挥手逻辑的只有三个。

序列号(Sequence Number):标识本报文段数据第一个字节在整个字节流中的位置。关键细节是——SYN 和 FIN 这两个标志位虽然不带数据,但各消耗一个序列号。这就是为什么第三次握手的 ACK 号是x+1,而 FIN 之后对方的 ACK 号是u+1。这一点面试里被问过太多次,答不上来说明没真看懂报文。

确认号(Acknowledgment Number):期望收到对方下一个字节的序号,等价于"这个号之前的我都收到了"。它是累积确认,配合 SACK 选项才能表达"中间缺了一块"。

窗口(Window):告诉对方"我这边还能收多少字节",是流量控制的核心。握手阶段双方会在 TCP 选项里协商窗口缩放因子(wscale),把 16 位窗口最大放大到 2^30 左右,否则在高带宽高延迟链路上吞吐会被死死卡住。

除了这三个,握手阶段还会协商 MSS(最大报文段长度)、SACK(选择性确认)、时间戳(Timestamps)这几个选项。MSS 尤其重要,它决定了单个 TCP 段能装多少数据,直接影响到后面分片和性能。这些协商都在 SYN 报文里完成,一旦握手结束就不能改了。

提示:序列号的初始值(ISN)不是从 0 或 1 开始的,而是一个随机值。这不是为了好看,一是防止旧连接的报文被新连接误收,二是避免序列号被外部推测出来造成数据注入。早期实现用时钟驱动,现在主流内核用更复杂的哈希加随机数方案。

1.3 为什么是 TCP 而不是 UDP:一个选型判断表

写代码的时候经常要在 TCP 和 UDP 之间做选择,与其背特性,不如按场景对号入座。

维度TCPUDP
连接建立开销三次握手,至少 1 个 RTT无,直接发
可靠性确认重传,保证有序不丢不保证,交给应用层
消息边界字节流,需自行切分保留报文边界
头部开销20 字节起步8 字节
拥塞控制有,会主动降速无,不自带
典型场景接口调用、数据库、文件传输音视频、游戏状态同步、DNS

选型的时候我一般问自己三个问题:丢一个包业务能不能忍、需不需要严格保序、能不能接受多一个 RTT 的建连成本。三个答案里只要有一个是"必须保证",基本就往 TCP 走;如果业务对延迟极度敏感、丢包能靠应用层自己补偿,UDP 反而更合适。近些年不少实时通信场景在 UDP 之上自己搭可靠层,本质就是用工程复杂度换延迟。

2. 三次握手:为什么偏偏是三次

这是全文的核心。我先给流水账,再拆"为什么",最后落到内核实现和参数上。顺序不能反,先看现象再讲道理,印象才深。

2.1 一次完整握手的三帧报文长什么样

假设客户端是 A(10.0.1.5:51234),服务端是 B(10.0.2.7:8080),A 主动发起连接。

第一帧,A → B:SYN=1, seq=x。A 说"我想连你,我的起始序列号是 x"。此时 A 进入SYN_SENT状态。注意这一帧不带数据,但它消耗一个序列号。

第二帧,B → A:SYN=1, ACK=1, seq=y, ack=x+1。B 说"收到你的请求了(ack=x+1 表示 x 及之前我都收到了),我这边起始序列号是 y"。这一帧同时完成了"确认对方"和"发起自己方向"两件事,所以 SYN 和 ACK 合并在一个包里发。此时 B 进入SYN_RCVD状态,并且这条连接被放进半连接队列。

第三帧,A → B:ACK=1, seq=x+1, ack=y+1。A 说"我也收到你的了"。此时 A 进入ESTABLISHED,B 收到后从半连接队列取出,放到全连接队列(accept 队列),等待应用层accept()拿走。这一帧是可以携带数据的——这也是"TCP Fast Open"想优化的点,只不过普通情况下应用数据要等accept之后才处理。

看起来三帧很简单,但每一帧背后都有讲究。第二帧发出去之后,B 并不知道 A 是否收到,所以 B 会启动一个重传定时器;如果迟迟等不到第三帧,B 会重发 SYN+ACK,重传次数由net.ipv4.tcp_synack_retries控制,默认 5 次,间隔按 1s、2s、4s、8s、16s 指数退避,总共大约 63 秒后放弃。这个 63 秒经常被误认为"三次握手超时是 3 秒",其实完全不是。

2.2 两次为什么不够:历史连接与资源浪费

教科书上最常见的解释是"两次握手无法确认客户端的接收能力",这话对,但不够狠。我认为更有说服力的场景是历史重复连接。

设想这么一条链路:客户端发了一个 SYN,因为网络拥塞这个包在路上滞留了很久;客户端等不及超时重发,第二个 SYN 顺利到达,连接建立、数据传输、连接关闭,一切结束。这时候那个滞留在网络里的老 SYN 突然到了服务端。如果只有两次握手,服务端收到这个 SYN,直接回一个 SYN+ACK 并立即认为自己已建立连接、分配资源、进入 ESTABLISHED 等着收数据。可客户端根本没想连,收到这个莫名其妙的 SYN+ACK 只会回一个 RST,服务端白白浪费了一次资源分配。

三次握手就避开了这个坑:服务端在第二次握手后只是进入SYN_RCVD,并不认为连接已建立,要等第三次的 ACK 才转正。老 SYN 触发的 SYN+ACK 到了客户端会被 RST 掉,服务端资源随之释放。换句话说,第三次握手的价值在于让服务端获得"客户端确实收到了我的 SYN+ACK 并且愿意继续"这个确认,而不是它自己单方面宣布连接成立。

还有一个更本质的角度:握手要做的是双向确认收发能力,一共四件事——A 的发、B 的收、B 的发、A 的收。两次握手只能确认前三件,第四件(A 的接收能力)必须靠第三次。这四种能力缺一不可,否则任何一方都可能对着一个"聋子"拼命发数据。

2.3 四次为什么浪费:SYN 和 ACK 能合并的原因

既然两次不够,那为什么不用四次、五次?答案是没必要,因为TCP 报文头里的 SYN 和 ACK 是两个独立的标志位,可以同时置位。

如果硬要拆成四次,无非是把"B 确认收到 A 的 SYN"和"B 发起自己的 SYN"分成两个包发。但这两个信息没有任何冲突,完全可以塞进同一个报文里,省一个 RTT、省一次中断处理。TCP 的设计哲学一贯如此:能在头部标志位里表达的信息,绝不多开一个包。

这里有个容易被追问的点:为什么第四次挥手不能也合并?因为挥手的时候,被动关闭方收到 FIN 只是表示"对方没数据要发了",但它自己可能还有数据没发完。它必须先回一个 ACK 告诉对方"我知道了",然后等自己的数据发完,再单独发一个 FIN。这两件事在时间上是分离的,没法合并。这就是挥手比握手多一次的根本原因——握手的双方是对等的,都想马上开始;挥手的时候双方的数据发送进度不一样,急不来。

2.4 半连接队列、全连接队列与 SYN 洪水的关系

这两个队列是线上排障的高频考点,也是最容易背混的地方。

半连接队列(SYN Queue):存放收到 SYN、回了 SYN+ACK、但还没收到第三次 ACK 的连接,状态是SYN_RCVD。长度受net.ipv4.tcp_max_syn_backlog和listen()的 backlog 参数共同影响。

全连接队列(Accept Queue):存放已完成三次握手、等待应用层accept()取走的连接,状态已经是ESTABLISHED。长度由listen(fd, backlog)的 backlog 和net.core.somaxconn中较小的那个决定。实际生效值可以用ss -lnt看Send-Q那一列。

这两个队列溢出的表现完全不同,这也是排查的关键分水岭:

现象队列典型原因排查命令
客户端偶发连接超时,服务端ss -s里 SYN_RECV 很多半连接队列队列太小,或遭遇大量伪造源地址的 SYNss -ant state syn-recv | wc -l
客户端连接成功但请求很久没响应,ListenOverflows增长全连接队列应用 accept 太慢,业务处理阻塞nstat -az | grep -i listen
大量SYN_RECV且源 IP 极度分散半连接队列典型的 SYN 洪水特征netstat -an | grep SYN_RECV | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn

关于 SYN 洪水,我不展开攻击细节,只说防护思路,这也是运维日常要配的。核心手段是开启 SYN Cookie:服务端收到 SYN 后不立即分配资源,而是用源/目的 IP、端口和时间等信息算一个哈希当作序列号回给对方,只有收到正确的第三次 ACK 才真正建连接。开关是net.ipv4.tcp_syncookies = 1。配合调大半连接队列、适当降低tcp_synack_retries,基本能扛住常见的异常流量。

注意:tcp_syncookies在队列未溢出时不会启用,属于兜底机制,平时开着没有副作用。但如果队列溢出了它才生效,这时 SYN 报文里的 MSS、窗口缩放等选项会丢失,极端情况下性能会打折。所以正确做法是队列和 Cookie 一起配,而不是只依赖 Cookie。

2.5 握手阶段的参数调优与实测数据

我做过一组简单实验:把tcp_syncookies关掉、tcp_max_syn_backlog调到 128、backlog 调到 5,然后用压力工具狂发连接请求,观察SYN_RECV堆积和ListenOverflows的增长。结论是队列小于 128 的时候溢出非常快,几百并发就打满了。

生产环境我一般这么配,仅供参考,具体数值要按机器规格和业务量调:

# 全连接队列上限,和 listen 的 backlog 取小值,通常同步调大 net.core.somaxconn = 32768 # 半连接队列上限 net.ipv4.tcp_max_syn_backlog = 16384 # 开启 SYN Cookie 兜底 net.ipv4.tcp_syncookies = 1 # 队列溢出时不直接 RST,便于观察(默认 0 就是丢弃) net.ipv4.tcp_abort_on_overflow = 0

改完sysctl -p生效,然后务必用ss -lnt确认Send-Q真的变了。我见过不止一次改完参数没重启应用的案例——listen()的 backlog 是进程启动时传进去的,改内核参数不重启进程,队列长度不会变。

3. 四次挥手:为什么分手比牵手多一步

握手是双方都想开始,所以能合并;挥手是双方数据发送进度不同步,只能各走各的。这个不对称性是理解全部挥手细节的钥匙。

3.1 挥手四帧报文的逐帧拆解

还是 A 主动关闭,B 被动关闭。

第一帧,A → B:FIN=1, seq=u。A 说"我没数据要发了"。此时 A 进入FIN_WAIT_1。注意这只是半关闭,A 的接收通道还开着,还能收 B 的数据。

第二帧,B → A:ACK=1, ack=u+1。B 说"知道了"。此时 B 进入CLOSE_WAIT,A 收到后进入FIN_WAIT_2。这就是半关闭状态:A 不再发,但能收;B 还能继续发数据给 A。

第三帧,B → A:FIN=1, ACK=1, seq=w, ack=u+1。B 把自己剩下的话说完,也发 FIN。此时 B 进入LAST_ACK。

第四帧,A → B:ACK=1, ack=w+1。A 确认。此时 A 进入TIME_WAIT,等 2MSL 之后才真正CLOSED;B 收到后直接CLOSED。

一共四个状态字要记牢:FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK。线上排障看到最多的就是后两个。

3.2 为什么不能三次:半关闭状态是不可省略的

很多人问,能不能把第二帧和第三帧合并,像握手那样省一次?答案在"半关闭"这三个字里。

TCP 是全双工的,两个方向的数据流相互独立。A 发 FIN 只代表A→B 这个方向关闭了,不代表 B→A 也关闭。B 收到 FIN 的时候,它可能正有一大坨数据在发送队列里没发完,可能还在等数据库返回结果。这时候 B 不可能立刻回一个 FIN——那等于告诉 A "我也没话了",可事实上它还有话要说。所以 B 只能先回 ACK 表示"你的关闭请求收到了",然后等自己的活干完,再单独发 FIN。

这就是三次握手的合并逻辑在挥手场景失效的原因:合并的前提是两个动作可以同时发生,而挥手时这两个动作有严格的先后依赖。

顺带提一句,如果应用层明确知道自己不会再发数据了,可以用shutdown(fd, SHUT_WR)主动发 FIN 进入半关闭,同时继续recv()收数据。这个技巧在某些"一问一答"的协议里很有用,比如先发完请求,然后半关闭,等对方把响应发完自然关闭。

3.3 TIME_WAIT 的 2MSL 到底在等什么

这是被问得最多、也最容易答得含糊的点。TIME_WAIT 存在的理由有两条,缺一不可。

第一条:保证最后一个 ACK 能到达对端。第四帧 ACK 如果丢了,B 会以为自己的 FIN 没被确认,于是重传第三帧 FIN。此时 A 还处在 TIME_WAIT,能重发 ACK 并重置计时器。如果 A 直接关掉,收到重传的 FIN 会回一个 RST,B 那边就会认为连接异常终止。所以 TIME_WAIT 的时长必须覆盖"B 重传 FIN 的最长时间",也就是 2MSL(去一趟 + 回来一趟)。

第二条:让旧连接的残留报文在网络中彻底消散。如果 A 立刻用相同的四元组(源 IP、源端口、目的 IP、目的端口)建立新连接,网络上那些老连接的迟到报文可能被新连接误收,造成数据错乱。等 2MSL,理论上老报文已经全部超时消失。

Linux 里这个值并不是严格由 RFC 的"MSL=30 秒"推出来的,内核直接定义了TCP_TIMEWAIT_LEN = 60 秒,也就是 TIME_WAIT 固定持续 60 秒。这是实测出来的值,ss -tan state time-wait观察一个连接从进入到消失,基本就是 60 秒。

还有一个容易混的状态叫CLOSING,出现在双方同时关闭的情况下:A 发了 FIN 还没收到 ACK,同时也收到了 B 的 FIN。这时 A 不能进 FIN_WAIT_2,而是进 CLOSING,等自己的 FIN 被确认后再进 TIME_WAIT。实际生产里很少见,但面试偶尔会问。

3.4 大量 TIME_WAIT 与 CLOSE_WAIT 的排查路径

这两种堆积性质完全不同,处理方式也截然相反,搞混了会南辕北辙。

TIME_WAIT 大量堆积:说明本机是主动关闭方,而且是短连接高并发。常见于 HTTP 客户端、反向代理、频繁建连的服务。这个状态本身不占太多资源(一个 timewait 控制块很小,主要占端口),真正的问题是端口耗尽导致Cannot assign requested address。

处理思路按性价比排序:

  1. 最先考虑的永远是改成长连接 / 连接池,从源头减少建连次数。这条比改任何内核参数都有效。
  2. 让客户端主动关闭,把 TIME_WAIT 分散到调用方去。
  3. 扩大本地端口范围:net.ipv4.ip_local_port_range = 1024 65535,端口多了,周转空间自然大。
  4. 谨慎开net.ipv4.tcp_tw_reuse = 1。它只对主动发起连接的一方生效,且要求tcp_timestamps=1,只复用存在超过 1 秒的 TIME_WAIT 连接。它在纯客户端角色上相对安全,但在服务端或 NAT 场景下可能带来问题。
  5. 应用层设SO_REUSEADDR,主要是解决重启时端口被占用,跟 TIME_WAIT 数量本身关系不大。

CLOSE_WAIT 大量堆积:这个性质完全不一样,它说明本机是被动关闭方,收到了对方的 FIN 并回了 ACK,但应用层一直没有调用close()。这是代码问题,不是内核参数问题。

我用ss -tan state close-wait排查过好几次这类问题,每次根因都逃不出这几种:连接用完没有在finally块里释放;异常分支提前 return 漏了关闭;连接池的归还逻辑有 bug;或者业务线程被下游阻塞住,主循环根本没机会处理关闭事件。

注意:看到 CLOSE_WAIT 堆积,第一反应应该是翻代码找泄漏的 fd,而不是去改内核参数。改参数只会掩盖问题,fd 泄漏最终一定导致Too many open files。我见过一个团队在这个问题上折腾了两周,最后发现是一个抛异常后没关连接的 try-catch 写法。

4. 抓包实操:把握手挥手完整看一遍

原理讲再多,不如自己抓一次。这一节是可以直接照着做的,我用两台内网机器复现,你完全可以在本机用 Docker 起两个容器来做。

4.1 环境准备与抓包命令

实验拓扑很简单:A(10.0.1.5)作客户端,B(10.0.2.7)作服务端,B 上起一个监听 8080 的服务,A 用nc或curl发起连接。

在 B 上开抓包,先把过滤条件写准,不然一堆无关流量会淹没你:

# -nn 不做域名和端口解析,-S 显示绝对序列号,-l 行缓冲便于实时看 sudo tcpdump -i any -nn -S 'tcp port 8080 and host 10.0.1.5' -l

这里-S是关键。不加它,tcpdump 显示的是相对序列号(从 0 开始),你就看不到 ISN 的随机性,也没法验证ack = seq + 1这个关系。

同时另开一个窗口,准备在 A 上发起连接并立即关闭:

# 发起连接后立刻关闭,制造一次完整的握手 + 挥手 printf 'hello\n' | nc -N 10.0.2.7 8080

-N表示在 stdin 结束后发送 FIN,正好能触发主动关闭,一次抓全。

4.2 握手三帧的实际抓包记录解读

抓下来大概是这样(内网地址做了替换,序列号是真实随机值):

10:21:33.101522 IP 10.0.1.5.51234 > 10.0.2.7.8080: Flags [S], seq 3216549870, win 64240, options [mss 1460,sackOK,TS val 112233 ecr 0,nop,wscale 7], length 0 10:21:33.101887 IP 10.0.2.7.8080 > 10.0.1.5.51234: Flags [S.], seq 1897733421, ack 3216549871, win 65160, options [mss 1460,sackOK,TS val 445566 ecr 112233,nop,wscale 7], length 0 10:21:33.101945 IP 10.0.1.5.51234 > 10.0.2.7.8080: Flags [.], ack 1897733422, win 502, length 0

逐帧读一遍,几个细节值得说:

第一帧Flags [S]就是 SYN,seq 3216549870是一个随机 ISN,length 0说明不带数据。options里mss 1460表示"我最多能收 1460 字节的 TCP 载荷",wscale 7表示窗口要放大 2^7=128 倍。

第二帧Flags [S.],点号代表 ACK,所以这是 SYN+ACK。ack 3216549871正好是对方 seq 加 1,验证了"SYN 消耗一个序列号"。这一帧同时也通告了自己的 MSS 和 wscale,双方各取较小的那个生效。

第三帧Flags [.]只有 ACK,ack 1897733422是对方 seq 加 1。注意win 502比第一帧的 64240 小得多,这是应用还没accept、接收缓冲区还没准备好的表现,正常现象,不是问题。

三次握手的三个包时间戳分别是 33.101522、33.101887、33.101945,总共 0.4 毫秒左右。这就是内网 RTT 的真实水平,也是为什么内网服务多一次 RTT 感觉不明显,而跨境调用多一次握手就要多几十毫秒。

4.3 挥手过程与状态变迁对照

紧接着就是挥手,抓包记录:

10:21:33.102210 IP 10.0.1.5.51234 > 10.0.2.7.8080: Flags [F.], seq 3216549871, ack 1897733422, length 0 10:21:33.102255 IP 10.0.2.7.8080 > 10.0.1.5.51234: Flags [.], ack 3216549872, win 65160, length 0 10:21:33.102401 IP 10.0.2.7.8080 > 10.0.1.5.51234: Flags [F.], seq 1897733422, ack 3216549872, length 0 10:21:33.102430 IP 10.0.1.5.51234 > 10.0.2.7.8080: Flags [.], ack 1897733423, win 502, length 0

Flags [F.]是 FIN+ACK。第一帧 FIN 的seq 3216549871是握手时客户端 seq 加 1,因为 SYN 消耗了一个。第二帧 ACK 号变成3216549872,验证了"FIN 也消耗一个序列号"。第三帧服务端 FIN 的seq 1897733422也是同理。最后一帧 ACK 之后,客户端进入 TIME_WAIT。

这里有个很实用的观察点:如果你的服务端每次都主动发第一帧 FIN,那么 TIME_WAIT 会全部堆在服务端。用ss -tan state time-wait | wc -l一看数量就知道谁在主动关闭。这个判断方法比翻代码快得多。

抓完之后建议把 pcap 存下来反复看:

sudo tcpdump -i any -nn -w /tmp/tcp-handshake.pcap 'tcp port 8080'

配合ss实时看状态,边抓边对照,印象最深:

# 看所有 TCP 连接及状态 ss -tan # 只看各状态的数量汇总 ss -s # 看监听队列,Send-Q 是队列上限,Recv-Q 是当前排队数 ss -lnt

ss -lnt的Recv-Q那一列如果持续不为 0 且接近Send-Q,说明全连接队列快满了,应用 accept 太慢,这时候该去看应用为什么卡住了,而不是调内核参数。

5. 面试题实录:从背答案到讲原理

面试官问这些题,真正想看的是你能不能把机制和现象联系起来。下面按难度分三档,每题给出"答题骨架"而不是标准答案——标准答案背了也会忘,骨架能让你自己组织语言。

5.1 基础题:三个必答点

问:为什么握手是三次而不是两次?

答题骨架分三层:第一层说双向确认收发能力需要四次信息交换,两次只能完成三次,缺客户端接收能力这一环;第二层说历史重复连接的风险,两次握手会让服务端为无效连接分配资源;第三层补一句"服务端在第二次握手后只进 SYN_RCVD,不认为连接建立",这是实现层面的证据。三层说完,基本满分。

问:为什么挥手是四次?

核心就一句:TCP 是全双工的,FIN 只关闭一个方向。被动方收到 FIN 时可能还有数据没发完,只能先 ACK,等数据发完再发自己的 FIN。补充一句"如果是双方同时关闭,会出现 CLOSING 状态"能加分。

问:TIME_WAIT 为什么要等 2MSL?

两个理由都要说:保证最后那个 ACK 能送达(丢了还能重发),以及让旧连接的残留报文消散,避免污染使用相同四元组的新连接。再补一句"Linux 里这个值是固定的 60 秒",显得你真看过。

5.2 进阶题:往内核实现上靠

问:服务端出现大量 CLOSE_WAIT 说明什么?

这题答"内核参数没调好"直接出局。正确答案是:应用层收到 FIN 后没有调用 close(),属于 fd 泄漏。要接着说出排查方法——lsof -p <pid>看 fd 数量,ss -tan state close-wait看堆积,然后去代码里找没释放连接的路径(异常分支、连接池归还、阻塞导致主循环卡死)。

问:tcp_tw_reuse 和 tcp_tw_recycle 有什么区别?

tcp_tw_reuse用于主动发起连接的一方复用处于 TIME_WAIT 且超过 1 秒的连接,需要时间戳开启,相对安全。tcp_tw_recycle是老版本内核的激进选项,靠时间戳判断,在 NAT 环境下会大量误丢包,新版内核已经移除,不要再在任何环境开它。这题能答出"已经移除"这个事实,说明你不是照抄十年前的博客。

问:第三次握手可以携带数据吗?

可以。第三次握手的 ACK 报文本身不消耗序列号,客户端完全可以在这一帧里捎带数据。但服务端通常要等accept()之后才处理,所以业界才有 TCP Fast Open 这个优化方向,让数据在 SYN 里就带上,省一个 RTT。

问:SYN 队列满了会怎样?

两种可能:如果开了tcp_syncookies,服务端不分配资源,直接用哈希当序列号回 SYN+ACK,靠第三次 ACK 里的信息重建连接;如果没开,新来的 SYN 直接被丢弃,客户端只能超时重传,表现出来就是偶发连接超时。答题时把这两个分支说清楚,再提一句"全连接队列溢出是另一回事,表现为 ListenOverflows 增长",能明显拉开差距。

5.3 场景题:把知识用到线上

场景一:某服务上线后监控显示 TIME_WAIT 常年几万个,怎么处理?

先说结论:TIME_WAIT 本身不是病,端口耗尽才是病。然后给排查顺序——先确认是不是短连接高并发(ss -s看总数和状态分布),再看是不是本机主动关闭(抓包看谁先发 FIN),最后按性价比给方案:改连接池 / 长连接优先,其次让调用方主动关闭,再其次是扩端口范围,tcp_tw_reuse放最后且要说明适用条件。

场景二:客户端连接偶发超时,服务端看起来一切正常,怎么定位?

先区分是握手阶段超时还是数据传输超时。握手超时看服务端SYN_RECV数量和ListenOverflows,前者多半是半连接队列或异常流量,后者是 accept 太慢。数据传输超时就看重传率和窗口。给具体命令:nstat -az | grep -i -E 'listen|syn'、ss -s、tcpdump抓 SYN 看有没有回包。这个答题结构体现的是"分层定位"的思路,比背结论值钱。

场景三:一台机器上万个连接,怎么看它们卡在哪个状态?

这题考的是命令熟练度。ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn一条命令出状态分布,然后针对异常状态往下查。能顺手说出ss比netstat快的原因(前者走 netlink,后者遍历/proc),是加分项。

6. 常见问题速查与踩坑记录

理论和实操都过了一遍,最后把散落的经验收一收。这张表是我自己排障时常用的,遇到连接问题先照着对一遍,能省不少时间。

6.1 状态与现象速查表

观察到的现象大概率原因第一步动作
TIME_WAIT 数量上万短连接高并发,本机主动关闭ss -s确认占比,评估改连接池
CLOSE_WAIT 持续增长应用未 close,fd 泄漏lsof -p看 fd 数,查代码异常分支
SYN_RECV 大量堆积半连接队列小,或异常 SYN 流量看源 IP 分布,评估开 syncookies
ListenOverflows 增长全连接队列满,accept 慢查应用是否阻塞,调 somaxconn 和 backlog
连接超时但服务端无日志握手未完成,包没到或被丢两端同时抓包,看 SYN 有没有回包
报 Cannot assign requested address本地端口耗尽扩ip_local_port_range,查 TIME_WAIT
大量 RST端口没监听,或连接被中间设备重置确认服务状态,看是否有超时踢连接
长连接一段时间后必断中间设备空闲超时,或 TCP keepalive 太短对时间点,调 keepalive 或加应用层心跳

几个命令行细节也记一下:

# 各状态连接数排名 ss -tan | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn # 全连接队列溢出和历史累计 nstat -az | grep -i -E 'ListenOverflows|ListenDrops' # 某进程持有的 fd 数量 ls /proc/<pid>/fd | wc -l # TCP 重传统计,看链路质量 nstat -az | grep -i retrans

6.2 我踩过的几个坑

第一个坑:以为握手超时是 3 秒。早期排查一个连接慢的问题,我一直按"三次握手 3 秒超时"去算时间,怎么也对不上日志。后来才搞清楚,客户端 SYN 重传次数由tcp_syn_retries控制(默认 6 次,总耗时约 127 秒),服务端 SYN+ACK 重传由tcp_synack_retries控制(默认 5 次,约 63 秒)。根本不存在"3 秒超时"这回事,那是某些应用层连接池自己设的。

第二个坑:改完内核参数没重启应用。somaxconn改了,ss -lnt看队列还是老值,折腾半天才发现listen()的 backlog 是进程启动时传的,内核参数只是上限,不重启进程不生效。这个坑我踩过不止一次,现在改完参数第一件事就是ss -lnt确认。

第三个坑:把 CLOSE_WAIT 当 TIME_WAIT 处理。早年间看到连接堆积,条件反射去搜"TIME_WAIT 优化",把tcp_tw_reuse开了,结果一点用没有——那批连接是 CLOSE_WAIT。这两个状态一个是内核在等时间,一个是应用在偷懒,处理方向完全相反。现在我第一步永远是ss -tan | awk看状态分布,不看分布不下结论。

第四个坑:抓包没加-S看不懂序列号。刚开始抓包的时候,看到 seq 从 0 开始,误以为握手时序列号就是 0,写总结的时候写错了。加-S才是真实的随机 ISN,这点小细节影响的是对整个机制的理解——ISN 随机化本身就是安全设计的一部分。

第五个坑:在 NAT 后面开时间戳相关的激进选项。这个教训来自一次跨网络环境的联调,开了激进的时间戳复用策略之后,部分请求莫名其妙失败,抓包发现是包被静默丢弃。后来才明白,中间设备做地址转换后,多台机器的时间戳会被混在一起判断,导致误判。现在我的原则是:涉及时间戳的复用选项,非必要不开,开了必须做充分验证。

6.3 一个容易被忽略的延伸方向

握手挥手这套东西学完之后,很自然的下一步是长连接的保活设计。TCP 自带的 keepalive 参数(tcp_keepalive_time默认 7200 秒、tcp_keepalive_intvl75 秒、tcp_keepalive_probes9 次)粒度太粗,两个多小时才探测一次,对付中间设备的空闲超时完全不够用。所以真正在生产里跑的长连接服务,基本都是应用层自己发心跳,比如每 30 秒发一个轻量 ping,两个周期没响应就主动断开重连。

这里有个很实际的取舍:心跳太频繁浪费带宽和电量(移动端尤其明显),太稀疏又会被中间设备踢掉。我的经验值是取中间设备空闲超时的一半左右,同时配合"断开重连带随机抖动"避免所有客户端同时重连把服务端打崩。这个抖动设计在客户端规模上万的场景里是必须的,我见过因为没做抖动、一次网络抖动后所有客户端同时重连导致服务端雪崩的案例。

另外,如果你在做的是需要极致降低建连延迟的场景,可以顺着 TCP Fast Open 往下看——它允许在 SYN 包里携带数据,把建连和数据传输合并成一个 RTT。不过它需要客户端和服务端都支持,且中间设备可能不认,落地时要充分测试。

我自己这些年反复看这套机制的最大体会是:TCP 的每一个"多余"的步骤,回头看都是在为某种异常情况兜底。第三次握手的"多余"是为了防止历史连接,TIME_WAIT 的"等待"是为了防止 ACK 丢失和报文污染,四次挥手的"啰嗦"是为了尊重双工的两个方向。真正把它们理解透之后,你看线上问题的眼光会变——不再问"这个参数该调多少",而是先问"这个状态是谁造成的、它在等什么"。这个思路上的转变,比记住任何一组参数值都管用。

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

基于YOLOv12的蜜蜂识别检测系统技术解析

1. 项目概述&#xff1a;蜜蜂识别检测系统的技术架构与应用价值在农业监测和生态保护领域&#xff0c;蜜蜂作为重要的传粉昆虫&#xff0c;其种群监测一直是个技术难题。传统人工计数方法效率低下且准确性难以保证。这套基于YOLOv12的识别系统&#xff0c;通过计算机视觉技术实…

作者头像 李华
网站建设 2026/10/27 5:14:09

E2E测试稳定性实战:从环境隔离到CI集成的最佳实践

1. E2E测试为什么总是容易被低估&#xff0c;却又绕不开先聊点实际的。做后端接口测试时&#xff0c;单测跑得飞快&#xff0c;覆盖率数字也漂亮&#xff0c;但一到联调阶段&#xff0c;前后端口径不一致、登录态串了、某个页面白屏&#xff0c;问题一个接一个往外冒。这时候你…

作者头像 李华
网站建设 2026/10/27 5:13:33

不完全去重技术:平衡存储与数据完整性的实践指南

1. 不完全去重技术解析在数据处理领域&#xff0c;不完全去重&#xff08;Partial Deduplication&#xff09;是一种平衡存储效率与数据完整性的创新方法。与传统的完全去重不同&#xff0c;这种方法允许系统保留部分重复数据&#xff0c;在实际业务场景中往往能带来意想不到的…

作者头像 李华
网站建设 2026/10/27 5:13:31

SMC PF3W7数字流量开关手册:卡门涡街原理与F1-F99参数设定指南

简介&#xff1a;这份资源是SMC公司数字式流量开关PF3W7##系列&#xff08;含PF3W704型号&#xff09;的官方使用手册&#xff0c;面向从事气动系统设计、设备安装调试与维护的工程师及技术人员&#xff0c;用于解决流量开关从选型、配管配线到功能参数设定与故障排查的实际问题…

作者头像 李华
网站建设 2026/10/27 5:13:07

模力方舟:AI开发者全栈平台的技术解析与实践

1. 模力方舟&#xff1a;AI开发者生态的"新基建"实践样本国内AI开发者圈最近有个高频词——"模力方舟"。这个听起来像科幻作品名字的平台&#xff0c;正在成为技术社区热议的"新基建"标杆。作为深度参与过多个AI基础设施建设的从业者&#xff0c…

作者头像 李华
网站建设 2026/10/27 5:12:36

Facebook Prophet时间序列预测实战指南

1. Facebook Prophet&#xff1a;时间序列预测的瑞士军刀第一次接触Facebook Prophet是在2019年一个零售销量预测项目中。当时团队尝试了ARIMA、LSTM等多种模型&#xff0c;要么效果不佳&#xff0c;要么调参复杂。直到发现Prophet这个工具&#xff0c;仅用十几行代码就实现了比…

作者头像 李华