news 2026/10/19 16:02:02

TCP三次握手、队列、滑动窗口与拥塞控制:从抓包到调优的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TCP三次握手、队列、滑动窗口与拥塞控制:从抓包到调优的实战指南

1. 从一个抓包现场说起:为什么这些概念总是搅在一起

很多人第一次接触网络协议,都是从“三次握手”这四个字开始的。背下来也容易:SYN、SYN+ACK、ACK,完事。但真正到了排查线上问题的时候,你会发现光知道这三个包根本不够用。连接建不上,可能是全连接队列满了;连接建上了但特别慢,可能是滑动窗口太小;丢包之后速度断崖式下跌,那是拥塞窗口在起作用;而数据包从你的机器到目标机器到底走了哪条路,又牵扯到路由表的查找逻辑。

这几个概念之所以总被放在一起讲,是因为它们本来就是一条链路上的不同环节。三次握手负责“建立连接”,全连接队列和半连接队列负责“管理连接”,滑动窗口负责“流量控制”,拥塞窗口负责“网络拥塞控制”,路由过程负责“找到路”。你把它们拆开看,每个都不难;但合在一起,才是一次完整的网络通信。

这篇文章我打算按实际排查问题的顺序来组织,而不是按教科书的章节顺序。因为我自己学这些东西的时候,最大的困惑不是“它是什么”,而是“它在哪里起作用、出问题的时候我该看哪个”。所以下面会从握手开始,一路讲到队列、窗口和路由,中间穿插我实际踩过的坑和常用的排查命令。

适合谁看?如果你写过网络编程、做过后端服务、或者被“连接超时”“吞吐上不去”这类问题折磨过,那这篇内容应该能帮你把零散的知识点串成一条线。如果你是完全的新手,也没关系,我会尽量用生活化的类比把原理讲清楚,再配上具体的命令和参数,让你能直接上手验证。

2. 三次握手到底在交换什么信息

2.1 为什么是三次而不是两次或四次

先把这个经典问题说透。三次握手的本质,是通信双方各自确认“我的发送能力”和“你的接收能力”都正常,并且同步初始序列号。

两次为什么不够?假设只有 SYN 和 SYN+ACK。客户端发了 SYN,服务端回了 SYN+ACK,从服务端角度看,它确认了客户端能发、自己能收,也确认了自己能发。但客户端还没告诉服务端“我收到你的 SYN+ACK 了”。这意味着服务端无法确认客户端能收。如果客户端因为某些原因没收到 SYN+ACK,服务端却已经认为连接建立了,就会白白维护一个半开连接。三次握手的最后一个 ACK,就是让服务端确认“客户端确实能收”。

四次为什么多余?因为 SYN+ACK 把“确认收到你的 SYN”和“我也要同步序列号”合并成了一个包。ACK 和 SYN 可以搭同一趟车,没必要分两趟。

用生活类比:你打电话给客服,你说“喂,能听到吗”(SYN),客服说“能听到,你能听到我吗”(SYN+ACK),你说“能听到”(ACK)。到这一步双方才确认通话链路双向通畅。如果只有前两步,客服不知道你那边是不是聋子。

2.2 序列号与确认号的实际含义

握手过程中交换的序列号不是随便生成的。初始序列号(ISN)的选取有一套规则,现代系统通常基于时钟和哈希生成,目的是防止旧连接的延迟包干扰新连接。

具体来说,客户端发送的 SYN 包里,seq 是客户端的初始序列号,比如 seq=x。服务端回复的 SYN+ACK 包里,seq 是服务端的初始序列号 seq=y,ack 是 x+1,表示“我已经收到你的 seq=x,下次请从 x+1 开始发”。客户端最后发的 ACK 包里,seq 是 x+1,ack 是 y+1。

这里有个容易忽略的点:SYN 和 FIN 这两个标志位会各消耗一个序列号。所以确认号要加一。普通数据包不消耗额外序列号,确认号就是 seq 加上数据长度。

2.3 抓包验证:用 tcpdump 看一次完整握手

理论说再多不如自己抓一次。在 Linux 上可以用 tcpdump 直接观察:

tcpdump -i eth0 -nn -S 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and port 80'

参数说明:-nn不解析主机名和端口名,-S显示绝对序列号而不是相对序列号,过滤条件只抓带 SYN 或 ACK 标志的包。

你会看到类似这样的输出:

IP 10.0.0.1.54321 > 10.0.0.2.80: Flags [S], seq 1000, win 64240, options [mss 1460,sackOK,TS val 123 ecr 0,nop,wscale 7], length 0 IP 10.0.0.2.80 > 10.0.0.1.54321: Flags [S.], seq 2000, ack 1001, win 65535, options [mss 1460,sackOK,TS val 456 ecr 123,nop,wscale 7], length 0 IP 10.0.0.1.54321 > 10.0.0.2.80: Flags [.], ack 2001, win 64240, length 0

注意第一行里的 options,这些是握手阶段协商的关键参数。MSS 决定单个包最大能带多少数据,SACK 开启选择性确认,wscale 是窗口缩放因子。这些参数直接影响后面的传输效率,后面讲滑动窗口时会再提到。

提示:抓包时如果只看到 SYN 没有 SYN+ACK,说明服务端没响应,可能是端口没监听、防火墙拦截、或者半连接队列满了。如果看到 SYN+ACK 但客户端没回 ACK,通常是客户端侧的问题,比如客户端进程已经退出。

3. 半连接队列与全连接队列:连接建立背后的两本账

3.1 两个队列分别在什么阶段起作用

这是很多人容易混淆的地方。服务端收到 SYN 之后,连接并没有立刻进入“已建立”状态,而是先被放进半连接队列。此时连接处于 SYN_RECV 状态。服务端回了 SYN+ACK,等待客户端的 ACK。

当服务端收到客户端的 ACK 后,连接从半连接队列移出,进入全连接队列,状态变为 ESTABLISHED。此时连接已经建立完成,等待应用程序调用 accept 把它取走。

所以两个队列的分工很明确:半连接队列管的是“握手还没完成”的连接,全连接队列管的是“握手完成了但应用还没接管”的连接。

3.2 队列满了会发生什么

半连接队列满了,新的 SYN 包会被丢弃。客户端会超时重传 SYN,表现为连接建立变慢甚至超时。全连接队列满了,服务端通常不会丢弃 ACK,而是根据内核参数决定行为。在 Linux 上,如果tcp_abort_on_overflow为 0(默认值),服务端会忽略这个 ACK,让客户端以为包丢了,触发重传。如果设为 1,服务端会直接回 RST,客户端立刻收到连接重置。

这两种行为各有适用场景。默认的忽略策略更温和,给应用一点时间把队列里的连接取走。但如果应用长时间不 accept,客户端会一直重传,最终超时。设为 1 则快速失败,适合对延迟敏感的场景。

3.3 关键参数与查看方法

半连接队列的大小由net.ipv4.tcp_max_syn_backlog控制。全连接队列的大小由listen()系统调用的 backlog 参数和net.core.somaxconn共同决定,取两者中的较小值。

查看当前队列情况:

ss -lnt

输出中的 Send-Q 就是全连接队列的最大长度,Recv-Q 是当前已经完成握手但还没被 accept 的连接数。如果 Recv-Q 经常接近 Send-Q,说明应用 accept 的速度跟不上。

查看半连接队列:

netstat -nat | grep SYN_RECV | wc -l

或者用ss -n state syn-recv。

注意:tcp_max_syn_backlog在较新的内核里并不是硬限制,实际还会受到somaxconn和路由缓存等因素影响。调参时不要只盯着一个值。

3.4 实操心得:队列溢出怎么排查

我遇到过一次线上问题,服务端偶尔出现连接超时,但 CPU 和内存都很正常。后来用ss -lnt发现 Recv-Q 周期性飙高,接近 Send-Q。原因是应用层在处理某个耗时操作时阻塞了 accept 循环。解决办法是把 accept 和业务处理拆到不同线程,或者调大 backlog。

另一个坑是somaxconn的默认值。很多系统默认是 128 或 4096,在高并发场景下明显不够。我一般会把它调到 65535,同时把应用的 backlog 参数也设成一样的值。注意改完somaxconn需要重启应用才能生效,因为它是在 listen 时读取的。

4. 滑动窗口:流量控制的核心机制

4.1 窗口的本质是接收方的缓冲区余量

滑动窗口解决的是“发送方发太快,接收方处理不过来”的问题。接收方在自己的 TCP 头部里有一个窗口字段,告诉发送方“我还能接收多少字节”。发送方根据这个值调整发送速度,确保不会把接收方的缓冲区撑爆。

这个机制叫流量控制,是端到端的。注意它和拥塞控制不是一回事:流量控制关心的是接收方的处理能力,拥塞控制关心的是网络链路的承载能力。

窗口大小不是固定的。接收方每收到一批数据,应用层读走一部分,缓冲区腾出空间,窗口就会变大。如果应用层读得慢,缓冲区越来越满,窗口就越来越小,甚至变成 0。窗口为 0 时,发送方会停止发送,定期发窗口探测包,询问接收方什么时候能腾出空间。

4.2 窗口缩放因子为什么必要

TCP 头部里的窗口字段只有 16 位,最大表示 65535 字节。在早期网络里这够用,但在高带宽高延迟的场景下,65535 字节的窗口远远不够。举个例子,带宽 100Mbps、往返延迟 100ms 的链路,带宽时延积是 100Mbps × 0.1s = 10Mbit = 1.25MB。也就是说,要跑满这条链路,窗口至少得有 1.25MB。65535 字节连零头都不够。

窗口缩放因子在三次握手阶段协商,通过 TCP 选项里的 wscale 字段传递。双方各自通告自己的缩放因子,实际窗口大小等于头部窗口字段左移缩放因子位。缩放因子最大是 14,所以最大窗口可以到 65535 × 2^14,约 1GB。

4.3 滑动窗口的实际滑动过程

假设接收方通告窗口为 4000 字节,发送方已经发了 2000 字节,收到确认 1000 字节。此时发送方还能发多少?窗口右边界是 4000,已经发了 2000,已确认 1000,所以还能发 4000 - 2000 = 2000 字节。当收到新的确认,窗口右边界右移,发送方就能发更多数据。

这个过程里,发送方维护三个指针:已发送已确认、已发送未确认、未发送但在窗口内。接收方也维护类似的指针,区分已接收已确认、已接收未确认、还能接收多少。

4.4 零窗口与窗口探测

当接收方缓冲区满时,会通告窗口为 0。发送方收到零窗口后停止发送,但会启动一个持续计时器,定期发送窗口探测包。探测包通常只带一个字节的数据,接收方收到后如果缓冲区有空间了,就回复当前窗口大小;如果还是满的,就回复零窗口。

这里有个经典问题叫“零窗口死锁”。如果接收方腾出空间后发的窗口更新包丢了,发送方还在等,接收方以为发送方收到了,双方就僵住了。窗口探测机制就是用来打破这个僵局的。

提示:抓包时如果看到大量[TCP ZeroWindow]和[TCP Window Update],说明接收方应用读取速度跟不上。这时候要么优化应用,要么调大接收缓冲区。

4.5 接收缓冲区和发送缓冲区的调优

Linux 上接收缓冲区由net.ipv4.tcp_rmem控制,发送缓冲区由net.ipv4.tcp_wmem控制。每个参数有三个值:最小值、默认值、最大值。

sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem

典型输出是4096 131072 6291456,表示最小 4KB,默认 128KB,最大 6MB。如果应用需要高吞吐,可以把最大值调大,比如 16MB。但要注意,缓冲区不是越大越好,太大会增加内存占用,也会让延迟变大。

自动调优参数net.ipv4.tcp_moderate_rcvbuf默认开启,内核会根据连接情况自动调整缓冲区大小。大多数场景下保持开启就行,只有在明确知道需要固定大缓冲区时才关闭。

5. 拥塞窗口:网络拥堵时的自我克制

5.1 拥塞窗口和滑动窗口的区别

滑动窗口是接收方告诉发送方“我能收多少”,拥塞窗口是发送方自己估计“网络能承受多少”。实际发送窗口取两者中的较小值。

拥塞窗口没有在 TCP 头部里传递,它是发送方自己维护的一个状态变量。发送方通过观察丢包和延迟来判断网络是否拥塞,然后调整这个值。

5.2 慢启动、拥塞避免、快速重传、快速恢复

这是拥塞控制的四个核心阶段。

慢启动:连接刚建立时,拥塞窗口从一个较小的值开始(通常是 10 个 MSS 左右),每收到一个 ACK 就翻倍。所以是指数增长,很快就涨到阈值。

拥塞避免:当拥塞窗口超过慢启动阈值后,改为线性增长,每个往返时间加一个 MSS。这样增长速度慢下来,避免一下子把网络撑爆。

快速重传:发送方收到三个重复 ACK,就认为某个包丢了,立刻重传,不用等超时。这比超时重传快得多。

快速恢复:快速重传之后,把慢启动阈值设为当前拥塞窗口的一半,拥塞窗口也设为这个值,然后进入拥塞避免阶段。这样避免了重新从慢启动开始,速度不会掉得太狠。

5.3 不同拥塞控制算法的选择

Linux 支持多种拥塞控制算法,常见的有 cubic、reno、bbr。

cubic 是 Linux 默认算法,适合大多数场景,在高带宽长距离链路上表现不错。reno 是老算法,现在很少用。bbr 是后来出现的算法,它不依赖丢包来判断拥塞,而是测量带宽和往返延迟,在有一定丢包的链路上表现更好。

查看当前算法:

sysctl net.ipv4.tcp_congestion_control

切换算法:

sysctl -w net.ipv4.tcp_congestion_control=bbr

注意:切换算法需要内核支持。可以用sysctl net.ipv4.tcp_available_congestion_control查看可用算法列表。

5.4 实操案例:丢包导致的吞吐下降

我做过一次跨机房的传输测试,带宽 1Gbps,延迟 30ms,但实际吞吐只有 100Mbps 左右。抓包发现有不少重传。用ss -ti查看连接详情,看到拥塞窗口经常掉到很低的值。

原因是链路上有轻微丢包,cubic 算法把丢包当作拥塞信号,大幅降低拥塞窗口。换成 bbr 之后,吞吐提升到 600Mbps 以上。因为 bbr 不把丢包等同于拥塞,而是根据带宽和延迟来调整发送速率。

这个案例说明,拥塞控制算法的选择对实际性能影响很大。如果你的业务跑在有一定丢包的链路上,值得试试 bbr。

6. 网络包路由过程:数据到底走了哪条路

6.1 路由表查找的基本逻辑

当一台机器要发送数据包时,内核会查找路由表,决定从哪个网卡发出去、下一跳是谁。查找规则是“最长前缀匹配”:在多个匹配的路由条目中,选择子网掩码最长的那条。

查看路由表:

ip route show

典型输出:

default via 10.0.0.1 dev eth0 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.2

第一行是默认路由,所有不匹配其他条目的包都走这里。第二行是本地子网路由,目标地址在 10.0.0.0/24 范围内的包直接通过 eth0 发送,不需要经过网关。

6.2 数据包的完整旅程

一个数据包从发送到到达目标,大致经过这些步骤:

发送方查路由表,确定下一跳和出口网卡。如果下一跳的 MAC 地址未知,发 ARP 请求获取。然后包被送到网卡,加上以太网头部,发到链路上。

中间经过多个路由器,每个路由器都查自己的路由表,决定下一跳。路由器的路由表可能比主机复杂得多,包含各种动态路由协议学习到的条目。

到达目标网络后,最后一跳路由器通过 ARP 找到目标主机的 MAC 地址,把包送达。

目标主机收到包后,检查目标 IP 是不是自己。如果是,交给上层协议处理;如果不是,根据ip_forward参数决定是否转发。

6.3 Traceroute 的原理和局限

traceroute 利用 IP 头部的 TTL 字段来探测路径。发送方发一个 TTL=1 的包,第一跳路由器收到后 TTL 减为 0,丢弃并回复 ICMP 超时消息。发送方就知道了第一跳的地址。然后发 TTL=2 的包,以此类推。

traceroute -n 10.0.0.100

-n表示不解析主机名,速度更快。

traceroute 的局限在于,很多路由器不回复 ICMP 超时消息,或者回复的优先级很低。所以看到* * *不代表链路断了,可能只是路由器不响应。另外,去程和回程的路径可能不一样,traceroute 只能看到去程。

6.4 策略路由和多路由表

Linux 支持策略路由,可以根据源地址、目标地址、端口等条件选择不同的路由表。这在多网卡、多线路的场景下很有用。

查看策略路由规则:

ip rule show

默认规则是查 local 表、main 表、default 表。可以添加自定义规则,比如“来自 10.0.0.0/24 的包查表 100”。

ip rule add from 10.0.0.0/24 table 100 ip route add default via 10.0.1.1 dev eth1 table 100

这样来自 10.0.0.0/24 的包就会走 eth1 出去,而不是默认的 eth0。

提示:策略路由配置错误很容易导致网络不通。改之前先用ip route get确认当前路径,改完再验证一次。

7. 常见问题速查与排查思路

7.1 连接建立相关

现象可能原因排查方法
连接超时,抓包只有 SYN服务端没监听、防火墙拦截、半连接队列满检查服务端监听状态、防火墙规则、`netstat -s
连接超时,有 SYN+ACK 但无 ACK客户端问题、SYN+ACK 被中间设备丢弃客户端抓包、检查中间设备
连接被重置全连接队列满且tcp_abort_on_overflow=1检查ss -lnt的 Recv-Q 和 Send-Q
连接建立慢SYN 重传、半连接队列频繁溢出检查tcp_max_syn_backlog和 SYN 重传统计

7.2 传输性能相关

现象可能原因排查方法
吞吐上不去窗口太小、拥塞窗口受限、缓冲区不足ss -ti查看 cwnd 和 rwnd,检查缓冲区参数
大量重传链路丢包、拥塞控制过于激进`netstat -s
延迟忽高忽低缓冲区膨胀、队列抖动检查tc qdisc配置,考虑减小缓冲区
零窗口频繁出现接收方应用读取慢优化应用读取逻辑,调大接收缓冲区

7.3 路由相关

现象可能原因排查方法
目标不可达路由表缺少条目、网关配置错误ip route get <目标IP>
去程和回程路径不一致策略路由、多线路两端分别 traceroute
部分网段不通子网掩码配置错误检查ip addr和ip route

7.4 几个容易踩的坑

第一个坑是只看一个参数就下结论。比如看到全连接队列满了就调大 backlog,但实际原因可能是应用 accept 太慢。调大 backlog 只是缓解症状,不解决根因。

第二个坑是忽略内核版本差异。不同内核版本对队列、窗口、拥塞控制的实现有差异。比如新内核的tcp_max_syn_backlog行为和老版本就不一样。排查时先确认内核版本。

第三个坑是抓包位置不对。在客户端抓包和在服务端抓包,看到的现象可能完全不同。定位问题时最好两端同时抓,对比分析。

第四个坑是过度调优。缓冲区调得太大,延迟会变高;拥塞窗口调得太激进,丢包会更严重。调优要基于实际测量,不要凭感觉。

8. 我个人的一些实操体会

这些东西刚学的时候觉得零散,用多了就会发现它们是一条链上的。我的习惯是,遇到网络问题先抓包,看握手是否完成、窗口是否正常、有没有重传。抓包能解决大部分疑惑,比看各种统计计数器直观得多。

另外,ss -ti这个命令强烈推荐。它能把连接的拥塞窗口、往返延迟、重传次数都列出来,排查性能问题时非常有用。我基本每次遇到吞吐问题都会先跑一遍。

调参方面,我的原则是能不调就不调,先找应用层的原因。应用层的缓冲区读取、accept 速度、并发模型,往往比内核参数影响更大。内核参数调优是最后一步,而且每次只调一个,调完观察一段时间再决定下一步。

最后说一个细节:MSS 的协商。如果两端 MSS 不一致,或者中间设备改了 MSS,可能导致分片,影响性能。抓包时留意 SYN 包里的 MSS 选项,确保两端协商一致。这个点很小,但实际排查中遇到过好几次。

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

PsychoPy 稳定部署指南:Python 3.9 + 预编译包 + 图形后端配置

简介&#xff1a;本资源是PsychoPy开源心理实验开发工具的完整安装包&#xff0c;面向心理学、神经科学及认知科学研究者与高校实验课程师生&#xff0c;用于在Python环境中高效构建视觉/听觉刺激、行为反应采集及时间精确控制的心理学实验范式。压缩包为ZIP格式&#xff0c;大…

作者头像 李华
网站建设 2026/10/19 15:56:25

排序算法性能分析:四维观测体系与工程归因方法论

1. 项目概述&#xff1a;为什么排序算法的性能分析不是“背代码”&#xff0c;而是工程师的底层思维训练“算法设计与分析 实验一 排序算法性能分析”——这个标题看起来像高校实验课手册里最普通不过的一行字&#xff0c;但在我带过十几届某高校计算机专业实验课、参与过多个工…

作者头像 李华
网站建设 2026/10/19 15:55:34

pot-desktop 安装与上手指南:划词翻译 + 截图 OCR 一次跑通

pot-desktop 安装与上手指南&#xff1a;划词翻译 截图 OCR 一次跑通 【免费下载链接】pot-desktop &#x1f308;一个跨平台的划词翻译和OCR软件 | A cross-platform software for text translation and recognition. 项目地址: https://gitcode.com/GitHub_Trending/po/po…

作者头像 李华
网站建设 2026/10/19 15:52:51

用两个栈实现队列:C语言完整实现与内存模型解析

在数据结构这一关里&#xff0c;“用两个栈实现队列”是被问得最多的一道题。核心思路其实很简单&#xff1a;栈是后进先出&#xff08;LIFO&#xff09;&#xff0c;队列是先进先出&#xff08;FIFO&#xff09;&#xff0c;两个LIFO一颠倒&#xff0c;顺序刚好就正回来了。听…

作者头像 李华
网站建设 2026/10/19 15:49:13

区县逐月房价数据解析:从Excel清洗到GIS专题图实战

做房产数据分析的人&#xff0c;应该都经历过这种尴尬&#xff1a;想研究某个板块的房价走势&#xff0c;翻遍公开数据&#xff0c;要么只有整个城市的平均值&#xff0c;要么只有季度甚至年度的颗粒度&#xff0c;根本没法看清“哪个区在涨、哪个区在横盘”。我这些年折腾各类…

作者头像 李华
网站建设 2026/10/19 15:46:55

Code Review最佳实践:为什么你的代码评审在走过场及改进指南

1. 为什么大多数Code Review都在走过场——价值与痛点的错位代码评审这件事&#xff0c;我见过两种极端的团队&#xff1a;一种干脆不做&#xff0c;代码合并靠吼&#xff0c;谁手快谁说了算&#xff0c;线上出了事故集体考古&#xff1b;另一种倒是天天开评审会&#xff0c;但…

作者头像 李华