ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TCP核心机制全解析:从三次握手到拥塞控制与调优实战

TCP核心机制全解析:从三次握手到拥塞控制与调优实战 相信搞过后端、写过网络程序的朋友刚接触 TCP 的时候都绕不开那几张经典的时序图三次握手、四次挥手看着像绕口令实则环环相扣。TCP 这玩意儿是整个互联网传输层的地基之一HTTP、FTP、SSH、数据库连接十有八九跑在它上面。你刷网页、传文件、登服务器背后都是一条条千锤百炼的 TCP 连接在工作。这篇东西想做的事是把 TCP 那些“人人都听说过、但不少细节没真正捋顺”的核心机制摊开讲一遍。三次握手为什么要握三次挥手为什么要挥四次重传和拥塞控制到底在防什么粘包怎么处理调试网络栈时该从哪里下手——这些话题不分开讲合在一起看才是一个完整的“可靠传输”故事。适合刚入门网络编程的开发者、被线上连接异常折磨的运维也适合面试前想系统过一遍 TCP 知识点的人。TCP 的水很深但核心骨架并不难。咱们从“它到底解决什么问题”开始一路讲到抓包实操和常见坑尽量让每个原理都有来路、有归宿。1. 为什么需要 TCP从不可靠网络聊起1.1 IP 只负责“尽力送达”可靠性得另想办法网络通信的基础是 IP 协议它的职责简单粗暴把数据包从源地址送到目的地址中途经过哪些路由器、走哪条路径IP 层自己决定。这个过程相当粗放路由器可能因为缓存不足直接丢包链路可能出现延迟抖动甚至两个包先后出发后发的反而先到。换句话说IP 提供的是“尽力而为best-effort”的服务不保证不丢、不保证有序、不保证不重复。那问题来了应用层要的是一个可靠的字节流比如你上传文件不能传一半丢一半你调用远程接口不能因为底层丢了个包就整个请求失败。靠应用层自己处理丢包和乱序当然可以但每个应用都重造一遍轮子代价太高。于是 TCP 站在 IP 上面专门把这些脏活累活揽了下来。TCP 要做的事本质上就四件把应用数据切成合适大小的报文段发给 IP 层等对端确认ACK没确认就重传把乱序到达的数据按顺序拼好再交给应用。这四件事做好底下的不可靠就被完全屏蔽了应用拿到的是一条干净的、有序的字节管道。1.2 TCP 在协议栈里的位置与职责边界为了把责任划分清楚人们把网络功能分成几层最常见的是 TCP/IP 四层模型层级代表协议核心职责应用层HTTP、FTP、DNS生成业务数据传输层TCP、UDP提供端到端通信TCP 额外保证可靠网络层IP、ICMP寻址与路由尽力转发网络接口层以太网、Wi-Fi物理链路上传输比特TCP 就工作在传输层直接面对的是应用层和网络层两边的诉求。对上它把应用当成一个简单的“管道使用者”你 write 一段字节它保证对端 read 到的内容和顺序一致对下它假设网络随时可能丢包、乱序、重复所有可靠机制都在自己的协议内部消化。这里有个很多新手容易搞混的点可靠是 TCP 的传输语义而不是 IP 的。你在网络层抓包看到一个个 IP 报文它们是独立路由的到了 TCP 层它们才被组织成“连接”这个逻辑概念。所谓连接本质上就是两端各自维护的一组状态变量序列号、确认号、窗口大小、重传计时器。理解了这一点后面所有拥塞控制、滑动窗口才有落脚的根基。2. 连接建立的“三次握手”到底在握什么2.1 序列号同步是核心目的打开任何一篇 TCP 文章都会看到三次握手的示意图客户端发 SYN服务端回 SYNACK客户端再发 ACK。图很好画关键是为什么要走这三步少一步行不行。TCP 连接的每条数据字节都有一个编号叫序列号Sequence Number接收端靠它排序、去重、确认。连接建立之前两端必须知道对方初始序列号ISN是什么否则收到数据后无从判断顺序和完整性。第一次握手客户端告诉服务端“我的初始序列号是 x”第二次握手服务端告诉客户端“我收到了你的序列号同时我的初始序列号是 y并且我确认了你的序号”第三次握手客户端告诉服务端“我确认收到了你的序列号咱们可以开始传数据了”。看到没有三次握手本质是双向交换初始序列号并且各自确认对方的序列号。第一次和第二次完成了服务端对客户端序号的学习与确认第二次和第三次完成了客户端对服务端序号的学习与确认。如果只有两次握手服务端就没办法确认客户端是否收到了自己的 SYNACK极端情况下会出现“半开连接”和旧连接干扰新连接。这就是“握三次”而不是“握两次”的根本原因。2.2 握手状态的完整变化过程站在两端看状态机TCP 的状态变化非常直观。用 netstat 或者 ss 命令看连接时经常能看到 LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED 这些字样它们对应的就是握手的每一拍客户端状态从 CLOSED 进入 SYN_SENT发出SYN1, seqx报文服务端在 LISTEN 状态收到 SYN状态变成 SYN_RCVD回SYN1, ACK1, seqy, ackx1客户端收到 SYNACK状态变成 ESTABLISHED回ACK1, seqx1, acky1服务端收到这个 ACK 后也进入 ESTABLISHED。这里有个小细节值得注意SYN 报文本身也要占据一个序列号所以 ack 确认的时候是 x1 而不是 x。很多人画图时把这个忽略了抓包时看到的 ack 数字对不上就会一头雾水。实际排查连接建立失败时重点看两端状态卡在哪个位置。如果客户端一直停在 SYN_SENT说明发出的 SYN 没得到回应可能是网络不通也可能是服务端根本没监听如果服务端一堆 SYN_RCVD 但迟迟进不了 ESTABLISHED往往是被 SYN 洪水攻击打满了半连接队列或是服务端回包被防火墙丢了。后文会专门展开这些坑。2.3 三次握手的几个实际变体与场景握手不一定每次都是“干干净净的三步”。比如 TCP Fast OpenTFO允许客户端在 SYN 里带上应用数据省掉一个 RTT 的等待HTTP 三次握手加速就用到了类似思路。还有“同时打开”两端同时给对方发 SYN理论上也能建立连接但现实中极其罕见了解即可。另外要说说SYN 重传。客户端发出 SYN 后如果迟迟收不到响应并不会无限等待而是触发指数退避的重传先等 1 秒再等 2 秒、4 秒、8 秒……重传几次之后如果还没回应客户端才放弃并报错。你在服务器上看到大量 SYN_SENT 堆积多半就是对方网络黑洞或是被安全策略 drop 了而不是 TCP 本身出了故障。3. 断开的“四次挥手”与 TIME_WAIT3.1 为什么挥手要四次断开连接比建立连接多一次原因是 TCP 的连接是“全双工”的两端可以同时收发数据。结束连接时每一端都必须单独确认对方的数据已经收完。四次挥手示意图主动方发FIN1, sequ表示“我的数据发完了”被动方回ACK1, acku1表示“知道了”但被动方可能还有数据要发所以此时连接处于半关闭状态被动方数据发完后发FIN1, seqv表示“我的数据也发完了”主动方回ACK1, ackv1两边才真正断开。第 2 步和第 3 步之间可能隔着很长的数据传输时间所以无法像三次握手那样合并成一条报文。这就是四次挥手的由来。实际抓包时你会看到常见的挥手序列FIN、ACK、FIN、ACK。如果被动方恰好没有数据要回第 2 步和第 3 步的 ACK 与 FIN 可能合并成一条于是看起来只有三次报文这也是正常的别觉得奇怪。3.2 主动方最后要等一个 TIME_WAIT挥手过程的精髓在于最后的 TIME_WAIT 状态。主动方发出第四次挥手的 ACK 之后并不会立刻关闭而是进入 TIME_WAIT等待 2 个 MSL报文最大生存时间后才彻底释放。为什么要等这么久两个考虑。其一万一最后的 ACK 在网络上丢了被动方会重发 FIN主动方需要留出时间重发 ACK否则被动方会一直收不到确认无法关闭。其二防止“旧连接的迟到数据包”混入“同端口的新连接”。如果没有 TIME_WAIT一个断开后立刻重连的 TCP 连接有可能接收到上一次残留的延迟数据包导致数据错乱。TIME_WAIT 有一个臭名昭著的影响高并发短连接场景下服务端或主动发起方会积累大量 TIME_WAIT 状态的连接占满本地端口新连接建不出来。常见解法是调小 TIME_WAIT 等待时间、开启端口复用net.ipv4.tcp_tw_reuse、或者在应用层尽量考虑连接复用。后文排查篇会再细说。3.3 RST不想好好分手时的暴力断开并不是所有连接都会走完四次挥手。RST 报文是 TCP 里的异常终止信号收到 RST 的一方会直接丢弃连接不做确认、不重传。触发场景很多访问不存在的端口对端直接回 RST程序崩溃后 socket 未优雅关闭内核发 RST服务端 backlog 队列满了新的 SYN 被直接 RST 掉。排查问题时抓包里看到 RST 往往是个线索。要看它出现在哪里握手中的 RST 多为被动拒绝服务端不想理你传输中的 RST 多为程序异常挥手后的 RST 可能是在 TIME_WAIT 期间收到新数据导致冲突。RST 不一定是错误但出现得反常就值得警惕。4. TCP 是怎么做到“不讲道理地可靠”的4.1 确认与重传机制丢了就再发TCP 可靠的根基是“发送-确认-重传”这一闭环。发送方给每个字节编号接收方收到数据后回 ACK带上下一个期望收到的字节编号。发送方如果在一定时间内没收到 ACK就触发重传。重传分为几种超时重传RTO发送后启动计时器超时未收到 ACK 就重发。这个超时时间不是写死的而是根据往返时延动态估算的Linux 内核每收到一个 ACK 都会更新 RTT 样本算出平滑后的期望值和偏差值。所以网络状况一变超时时间也会跟着变这个自适应机制非常关键。快速重传发送方收到三个重复 ACK说明对端连续收到了几个序号都比较靠后的数据中间有包丢了不必等超时直接重发缺失部分。选择性确认SACK基础的 ACK 只能告诉对方“我期望的下一个序号是 N”但如果中间不只丢了一个包传统机制会很痛苦会做大量不必要的重传。SACK 允许接收方把已经收到的非连续数据块明确列给发送方发送方只补真正缺的那些。现代系统基本都默认开启 SACK排查问题时可以确认一下双方是否都启用了它。一个常见的误解是“TCP 收到数据必须逐段确认”。实际 ACK 是累积的我不一定回每一个包只要连续收到了可能只回最后一个缺口。所以抓包时看到 ACK 数量比数据包少是正常的。4.2 滑动窗口与流量控制别打爆对端“可靠”不只体现在丢包补传还包括“不要让对方处理不过来”。TCP 用滑动窗口来实现流量控制接收方在自己的 ACK 里带上窗口大小advertised window告诉发送方“我现在最多还能收这么多字节你发的时候别超过这个量”。发送方严格依据这个窗口发送一旦对端窗口变成了 0发送方基本就得停下来等对端发来窗口更新后再继续。这个机制和拥塞控制要区分开流量控制是照顾“接收方”的处理能力拥塞控制是照顾“中间网络”的承载能力。接收方窗口反映的是端到端的空闲缓冲区拥塞窗口反映的是链路的安全水位。两个窗口一起限制发送方的实际发送速率实际可取发送量是两者中较小的那个。4.3 拥塞控制从慢启动到拥塞避免网络拥塞是个很有意思的问题你发现丢包然后重传但重传又加重网络负担导致更多丢包。如果没有一套机制全网会陷入拥塞崩溃。TCP 的拥塞控制就是为了防止这种情况。经典算法大致分四个阶段慢启动连接刚建立时拥塞窗口cwnd从一个很小值开始每收到一个 ACK窗口翻倍。这是指数增长但慢启动的名头指的不是增长慢而是“初始值保守”。所以短连接吃不上吞吐量因为还没涨起来连接就断开了。拥塞避免当窗口涨到慢启动阈值ssthresh后增速放缓为线性增长每经过一个 RTT 窗口加 1。快速重传与快速恢复检测到三次重复 ACK判断是轻微丢包阈值减半窗口降到新阈值附近然后进入线性增长恢复。超时则重进慢启动如果直接超时说明网络状况比较严重窗口直接降到初始值重新开始慢启动。后来出现的 BBR 等新算法试图绕过“以丢包为拥塞信号”的传统思路改为基于带宽和时延的估计来控制发送在高带宽长链路场景下效果非常显著。但要注意TCP 拥塞控制算法是选择性地协商的两端都支持的算法才能生效排查性能问题时有必要看一眼实际生效的是哪个版本。4.4 粘包和拆包字节流带来的工程问题TCP 是面向字节流的没有“消息边界”概念。你 send 两次 100 字节对端接收时可能一口气读到 200 字节也可能分三次读到 50、70、80——具体怎么拼完全由底层网络决定。这就是著名的“粘包/拆包”问题。解决思路不复杂本质上是自己给数据“画边界”。常用方案有固定长度消息每条消息定长读够长度就算一个完整包简单但浪费带宽分隔符消息末尾加特殊标记如换行符读到标记就算一条适合文本协议但二进制内容里出现同样标记就麻烦了头部长度字段包头用固定几个字节表示后续负载长度读够头部再按长度读负载这是最通用、最推荐的做法。这里要特别提一句长度字段别用 4 字节不封顶地设计。很多老系统用 2 字节长度字段最大只能表达 65535等哪天消息体超过这个上限线上直接炸。我见过不止一次这种坑。另外读包时要考虑半包情况一次 recv 可能只读到了半个消息头代码要能“攒着”继续等待剩余字节这属于基本功。5. 抓包与调试实战怎么把它看明白5.1 用 Wireshark 看三次握手与重传工具讲情怀没用抓包是理解 TCP 最直接的方式。打开 Wireshark监听 loopback 或真实网卡随便发起一个 HTTP 请求过滤条件写tcp很容易就能筛出完整的连接过程。抓包时建议重点看几件事握手过程中的 seq/ack 变化对照前面讲的三次握手手工推导一下每一帧的 ack 值确认自己是否真的看懂了状态流转TCP 握手耗时从第一个 SYN 到最后的 ACK 之间隔了多久如果超过一个 RTT 很多多半是中间有设备介入或者对端性能问题重传标记Wireshark 会把“TCP Retransmission”用高亮标出来一条连接里如果频繁出现重传链路质量堪忧窗口大小变化看窗口字段如果对端老是发窗口为 0 的包说明接收端应用读取不及时问题在上层跟网络无关。这里的经验是别盯着单个包看要按“流”看。Wireshark 的“Follow TCP Stream”可以还原整条连接的应用数据方便判断是 TCP 层丢包还是应用层数据本身有误。5.2 命令行三件套netstat、ss、tcpdump服务器上没法总开着 Wireshark命令行工具才是日常主力。netstat -anp | grep tcp看端口监听状态、连接状态、进程 PID。有些系统默认不显示 PID需要加-p并拥有 root 权限。ss -s统计汇总 TCP 状态数量一眼看出系统里 TIME_WAIT 或 SYN_SENT 是否异常堆积。tcpdump -i eth0 tcp port 8080 -w cap.pcap抓包保存成文件拿回本地用 Wireshark 打开分析。线上排查我习惯只抓特征端口加-n不做域名解析减少干扰输出。一个很实用的排查套路客户端和服务端同时抓包两边对比。如果客户端已经发出 SYN但服务端一无所获说明包丢在网络中间如果服务端回了 SYNACK 但客户端没收到多半是安全组或防火墙拦截。通过两端时间线对齐很多定位困难的“玄学断连”都能找到答案。5.3 应用层对 TCP 的常见误解端口占用与监听失败开发中高频出现的一类报错是“Address already in use”或者“Failed to listen on port”。很多人第一反应是“端口被谁占了”但底层逻辑往往是 TCP 状态机导致的服务端主动断开连接后服务端端口可能进入 TIME_WAIT此时如果用 SO_REUSEADDR 没设置对立刻重启监听就会报地址占用如果端口被处于 ESTABLISHED 状态的进程占着那确实是另一个进程还在用如果监听 backlog 队列满了内核会直接丢弃新连接客户端表现为 SYN 发了没回应服务端却看不到异常。排查这类问题ss -ltnp、lsof -i:端口都能快速定位。另外监听地址配置也要留意绑定了127.0.0.1就只能本机访问绑定0.0.0.0才能对外服务。很多人排查了半天端口没错结果输在绑定地址上。6. 实战避坑速查表那些最常见的 TCP 问题6.1 连接建立与断开的问题现象直接原因优先排查方向SYN 一直发不出去本地路由不通或对端 IP 不可达ping、traceroute、查防火墙 drop连接卡在 SYN_SENT对端未监听或中间设备丢包telnet ip port测试两端同时抓包大量 SYN_RCVD半连接队列溢出SYN 洪水攻击检查net.ipv4.tcp_max_syn_backlog开启 syn cookies连接卡在 FIN_WAIT_1对方的 ACK 丢了或对方不响应中间防火墙丢包进程可能卡死TIME_WAIT 过多短连接高频创建服务端开启tcp_tw_reuse应用层连接复用经常 RST 复位端口未监听、协议栈异常抓包看 RST 出现时机定位哪一端发起6.2 性能与吞吐量问题吞吐量上不去未必是带宽不够TCP 参数往往才是瓶颈。典型几个TCP 缓冲区过小net.ipv4.tcp_rmem和tcp_wmem默认值偏保守大带宽长时延场景下需要调大接收/发送缓冲区。计算需要的缓冲大小有个简单公式带宽 × 往返时延。比如 100Mbps 带宽、30ms RTT理论上要有约 375KB 的在途数据缓冲设小了吞吐就卡住了。MSS 设置不合理TCP 报文段最大长度MSS影响单包能带多少数据。如果路径 MTU 被防火墙挡了大包直接丢触发大量重传吞吐量崩盘。需要确认 PMTUD 是否开启、MTU 是否匹配。Nagle 算法与延迟 ACK 的互踩Nagle 会把小包合并发送延迟 ACK 会把确认攒起来再回两者相遇可能造成“确认延迟 40ms”的怪现象。对延迟敏感的应用如游戏可以关闭 Nagle开启TCP_NODELAY。6.3 嵌入式与物联网场景的 TCP 断连问题热词里有不少 lwIP、ESP32、Modbus TCP 相关的内容这些场景有共同特点设备内存小、网络环境差、链路切换频繁。嵌入式 TCP 断连的常见原因keepalive 参数太短或太长lwIP 默认的 keepalive 探测间隔可能不适合弱网环境长时间静默连接被中间 NAT 设备当成空闲会话清掉ARP 缓存过期局域网设备长时间没发包ARP 表项被清除下一个包发出去前要先 ARP如果此时网内干扰严重就超时了缓冲区溢出设备接收缓冲太小对端一次发太多数据内核直接丢包重传又叠加最终连接断开。解决方法是把对端发送节奏降下来或者调大 lwIP 的 PBUF 池电源与射频干扰很多嵌入式断连最后查到物理层Wi-Fi 模块瞬间掉线TCP 层跟着崩。抓应用层排查之前先确认射频链路是否稳定别在 TCP 参数上白费功夫。遇到这类问题我建议先在设备上实现简单的断线重连逻辑把“连接恢复”做成常态而不是异常。弱网环境下费尽心思保持一条 TCP 长连接不如允许它断开、重连得快、重连后能续传。7. 调试 TCP 的几个额外心得前面把原理和场景都过了一遍最后聊点实际操作中的个人体会。排查连接问题先定层再定位。别一上来就抓包。先看是不是应用层根本没绑定对地址再看是不是防火墙丢包然后才轮到 TCP 状态机、重传这些。按“物理链路 → IP 连通性 → 端口监听 → TCP 状态 → 应用层收发”这个顺序走效率高得多。另外线上抓包要克制。不用大而全地抓所有流量按端口过滤抓几秒就能看出问题。抓包数据保存下来后用 Wireshark 的“Analysis → TCP Stream Graph”里面的时序图和吞吐量图能把连接脉络还原得很清楚。命令行方面ss -tin能看实时拥塞窗口和 RTT调优时比抓包更直观。再有就是参数调优别照抄网上的“最优配置”。不同业务模型的 TCP 参数差异很大高并发短连接最怕 TIME_WAIT 堆积所以要开端口复用长连接大文件传输最怕缓冲区太小所以要调大 rmem/wmem实时交互类应用最怕延迟叠加所以要关 Nagle。没有万能参数只有匹配场景的参数。最后TCP 这个协议本身已经定义得相当成熟绝大多数“连接异常”的根子都不在协议而在应用层行为、中间设备策略、硬件链路质量。把这些层面都过一遍问题基本就跑不掉了。
返回列表