1. 这不是教科书里的TCP,而是我亲手抓包、调参、踩坑后写下的“协议人话手册”
你点开这个标题,大概率不是为了背诵“三次握手四次挥手”的标准答案——那玩意儿在面试前突击半小时就能默写,但真让你调试一个卡在SYN_SENT状态的连接,或者解释为什么Wireshark里看到的ACK号比预期大1,多数人还是会愣住。《TCP协议基础》这六个字背后,藏着网络世界最沉默也最倔强的守门人:它不声张,却决定着你刷短视频是否卡顿、远程协作文档能否实时同步、甚至物联网设备上报数据会不会丢包。我带过几十个网络方向的实操项目,从某高校实验室的嵌入式传感器组网,到某公司跨地域视频会议系统的延迟优化,所有问题最终都绕不开TCP——不是作为抽象概念,而是作为一串可观察、可修改、可验证的字节流。它没有魔法,只有规则;不讲情怀,只认序列号。这篇文章不教你画OSI七层图,也不堆砌RFC文档编号,而是带你回到协议诞生的现场:看一个数据包如何被分段、如何被确认、如何在丢包时自我修复,以及——最关键的是——当你在终端敲下ss -i或tcpdump -n port 80时,屏幕上跳动的每个字段到底在说什么。如果你刚学完“滑动窗口是动态的”,但还不知道rwnd和ssthresh在真实连接中谁先变化、为什么变化,那这篇就是为你写的。它适合两类人:一类是刚接触网络编程的开发者,想搞懂setsockopt(SO_RCVBUF)调大后为什么吞吐量反而下降;另一类是运维或测试工程师,面对“连接建立慢”这类模糊告警,需要一套可落地的排查路径。全文所有结论,都来自我在Linux 5.10内核、CentOS 7与Ubuntu 22.04环境下的真实抓包记录、内核参数调整和应用层日志比对。现在,我们从第一个SYN包开始。
2. 协议设计逻辑:为什么TCP必须是“面向连接”且“可靠”的?
2.1 不是“为了可靠而可靠”,而是物理层根本不靠谱
很多人初学TCP,第一反应是:“它为什么这么复杂?UDP多轻快。” 这个疑问本身就有陷阱——它把TCP当成了一个可选项,而非底层约束下的必然解。真相是:TCP的每一条规则,都是对物理世界缺陷的精准补偿。我们拆开看:
物理链路天生丢包:光纤衰减、无线干扰、交换机缓存溢出……这些都不是异常,而是常态。某次在某实验室部署LoRaWAN网关时,我们用同一型号模块在相同距离下测试,丢包率从0.3%到12%波动,原因仅仅是当天空气湿度变化导致2.4GHz频段信噪比劣化。UDP对此无能为力,它只管发,发完就忘。而TCP的重传机制(RTO计算、快速重传)本质是给不可靠链路装上“快递追踪+自动补发”系统:发出去的包没回执,就再发一次;连续收到三个重复ACK,立刻重发丢失包,不等超时。
网络路径动态变化:数据包从A到B,可能走A→C→D→B,也可能因C节点故障切到A→E→F→B。路径切换时,各段链路的带宽、延迟、MTU(最大传输单元)完全不同。比如某公司视频会议系统,员工在家通过千兆宽带接入,但中间经过运营商某老旧BRAS设备,其MTU被硬设为1400字节。若TCP未做路径MTU发现(PMTUD),大包会被分片,而分片包只要丢一个,整个IP报文就作废——这比单个TCP段丢失代价高得多。TCP的MSS(最大报文段长度)协商,就是在连接建立时主动“量体裁衣”:客户端SYN包里声明自己能接收的最大段长,服务端SYN-ACK里回应自己的MSS值,双方取小值作为后续通信上限,彻底规避分片。
接收方处理能力不可预知:你的手机App可能正后台渲染动画,CPU占用90%,此时即使网络畅通,它也没空处理新到的TCP段。如果发送方不管不顾狂发,接收方缓冲区(receive buffer)必然溢出,新包只能被丢弃。TCP的流量控制(Flow Control)就是用“接收窗口”(rwnd)这个字段来动态反馈:“我现在还能收X字节,别发多了”。这个窗口值随接收方应用读取数据的速度实时变化,像一个会呼吸的阀门。
提示:别把“拥塞控制”和“流量控制”混为一谈。前者是发送方感知网络整体拥堵(如路由器队列积压),通过cwnd(拥塞窗口)限制发包速率;后者是接收方告诉发送方“我这台机器吃不下更多”,通过rwnd限制。两者共同作用,才构成TCP的“自适应”特性。
2.2 “面向连接”不是虚的概念,而是三次握手构建的双向状态机
教科书说“TCP是面向连接的”,但很少解释:这个“连接”在内核里到底是什么?它既不是一根物理线,也不是一个内存地址,而是一对套接字(socket)在双方内核中维护的状态结构体。以Linux为例,当你调用connect()发起连接,内核会在本地创建一个struct sock实例,其中关键字段包括:
sk_state:当前状态(TCP_SYN_SENT, TCP_ESTABLISHED等)sk_write_queue:待发送的数据队列sk_receive_queue:已接收但应用尚未读取的数据队列tp->snd_nxt/tp->rcv_nxt:发送/接收的下一个序列号tp->snd_wnd/tp->rcv_wnd:当前拥塞窗口/接收窗口大小
三次握手的本质,就是让双方内核的这个结构体完成初始化并达成一致:
- SYN包:客户端发送
SYN=1, seq=x,告诉服务端:“我想建连,我的初始序列号是x,我能收MSS=1460字节的段”。 - SYN-ACK包:服务端回复
SYN=1, ACK=1, seq=y, ack=x+1,意思是:“同意建连,我的初始序列号是y,我确认收到了你的SYN(所以ack=x+1),我也能收MSS=1440字节”。 - ACK包:客户端再发
ACK=1, seq=x+1, ack=y+1,确认收到服务端的SYN,至此双方sk_state都变为TCP_ESTABLISHED,snd_nxt和rcv_nxt也按约定初始化完毕。
这个过程耗时至少1个RTT(往返时间),但它换来了什么?是双方对彼此序列号起点、窗口大小、MSS的精确共识。没有这个共识,后续每一个ACK、每一个数据段的校验都会失败。我曾调试过一个嵌入式设备,其TCP栈实现有bug:在SYN-ACK包中错误地将ack字段设为x(而非x+1),导致客户端认为握手失败,反复重发SYN。用tcpdump抓包一看,SYN包满天飞,但永远卡在SYN_SENT状态——这就是“面向连接”缺失的直接后果:没有状态同步,就没有可靠传输。
2.3 可靠性的四大支柱:校验、确认、重传、排序,缺一不可
TCP的“可靠”不是一句口号,而是由四个相互咬合的机制共同支撑的精密齿轮组:
校验和(Checksum):每个TCP段头部都有16位校验和,覆盖TCP头、数据及一个伪IP头(含源/目的IP、协议号、TCP长度)。它能检测出传输中发生的比特翻转。注意:校验和只在接收端计算,发送端不校验自己发的包。某次在某公司数据中心,我们发现一批服务器间通信丢包率异常高,抓包发现大量TCP校验和错误(Checksum incorrect)。排查后发现是网卡驱动bug,开启了TSO(TCP Segmentation Offload)但硬件校验卸载失效,导致分段后校验和计算错误。关闭TSO后问题消失——这说明校验和不是摆设,它是最后一道数据完整性防线。
确认应答(ACK):ACK号表示“我已成功收到序号小于ACK号的所有数据”。例如,收到
seq=100, len=100的数据段,ACK号应为200(即100+100)。这里有个关键细节:ACK是累积的,且不消耗序列号空间。也就是说,纯ACK包(无数据)的序列号沿用上一个数据包的snd_nxt,而ACK号指向下一个期望接收的字节。这保证了确认机制的轻量性。超时重传(RTO):发送方为每个未确认段启动定时器,超时则重发。RTO不是固定值,而是基于RTT(往返时间)动态计算的。Linux内核使用Karn算法和Jacobson算法:先测得平滑RTT(SRTT)和RTT方差(RTTVAR),再算RTO = SRTT + 4×RTTVAR。这样,当网络延迟突增(如高峰时段),RTO会自动拉长,避免不必要的重传;当延迟降低,RTO又会缩短,提升响应速度。我实测过,在4G网络下,RTO初始值约200ms,但遭遇信号波动时,它能在3次测量后升至1200ms以上。
序列号与重组(Sequencing & Reassembly):每个字节都被赋予唯一序列号,接收方按序号将乱序到达的段重新组装。这解决了IP层“尽力而为”导致的乱序问题。但要注意:TCP不保证段的到达顺序,只保证字节流的顺序。一个应用层消息(如HTTP请求)可能被拆成多个TCP段,它们可能经不同路径到达,但接收方内核会按序列号拼回原样,再交给应用层。这也是为什么你用
curl发请求,从来不用关心“第一个包是不是header”。
3. 核心机制深度拆解:从三次握手到TIME_WAIT,每个状态都在说话
3.1 三次握手:不只是建连,更是参数协商的黄金窗口
三次握手的每个包,都携带了关键参数,这些参数决定了整条连接的生命体征。我们逐包解析Wireshark抓包中的典型字段(以Linux客户端连接Nginx服务端为例):
| 包类型 | 关键字段 | 典型值 | 含义与影响 |
|---|---|---|---|
| SYN | MSS | 1460 | 客户端声明最大段长。若服务端MSS更小(如1440),则取1440。过大导致IP分片,过小增加包头开销。 |
| Window Scale (WS) | 7 | 表示接收窗口左移7位(即乘128)。若不开启,最大窗口仅65535字节,无法满足高速网络需求。 | |
| SACK Permitted | Present | 声明支持选择性确认(SACK),允许接收方告知发送方“收到了[1000-2000]和[3000-4000],但缺[2000-3000]”,大幅提升丢包恢复效率。 | |
| SYN-ACK | MSS | 1440 | 服务端声明MSS,双方取min(1460,1440)=1440。 |
| Window Scale | 7 | 服务端同样启用窗口缩放,双方窗口可扩展至65535×128≈8MB。 | |
| SACK Permitted | Present | 确认支持SACK。 | |
| Timestamps | TSval=123456789, TSecr=0 | 启用时间戳选项,用于精确计算RTT(TSval是发送时间,TSecr是回显对方上次的TSval),并防止序列号回绕(PAWS机制)。 |
注意:Window Scale和Timestamps必须在SYN包中首次提出,后续包不能新增。若客户端SYN未带WS,服务端SYN-ACK即使带了,客户端也会忽略,窗口仍受限于64KB。这是新手常踩的坑:以为调大
net.ipv4.tcp_rmem就能提升吞吐,却忘了检查握手阶段是否协商了窗口缩放。
实操中,你可以用ss -i命令查看当前连接的协商参数:
# 连接建立后执行 $ ss -i 'dst 192.168.1.100:80' State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 0 192.168.1.50:54321 192.168.1.100:80 cubic wscale:7,7 rto:204 rtt:1.234/0.056 ato:40 mss:1440 cwnd:10 ssthresh:10 send 11.2Mbps rcv_space:262144这里wscale:7,7表示双方都启用了窗口缩放(左移7位),mss:1440是协商后的MSS,cwnd:10是当前拥塞窗口(10个MSS大小的段),rcv_space:262144是接收缓冲区大小(256KB)。这些数字不是静态配置,而是连接运行中动态变化的“生命体征”。
3.2 四次挥手:为什么需要TIME_WAIT状态?2MSL到底在等什么?
连接终止比建立更微妙。四次挥手流程如下:
- 主动关闭方(A)发
FIN=1, seq=u; - 被动方(B)回
ACK=1, ack=u+1(此时A进入FIN_WAIT_2,B进入CLOSE_WAIT); - B处理完数据后,发
FIN=1, seq=v, ack=u+1; - A回
ACK=1, ack=v+1,并进入TIME_WAIT状态,等待2MSL后才彻底关闭。
关键问题:为什么A不直接关闭,非要等2MSL?MSL(Maximum Segment Lifetime)是IP报文在网络中存活的最长时间,RFC规定为2分钟,Linux默认设为30秒(net.ipv4.tcp_fin_timeout)。2MSL即60秒(或Linux默认60秒)。
TIME_WAIT存在的两大刚性理由:
- 防止旧连接的迷途包干扰新连接:假设A和B刚断开连接,A立即用相同四元组(源IP:端口,目的IP:端口)新建连接。若上一个连接的延迟ACK包(如第4步的ACK)在网络中游荡,恰好在新连接建立后到达B,B会误以为这是新连接的ACK,导致状态混乱。等待2MSL,确保网络中所有属于旧连接的包都已消亡。
- 确保被动关闭方(B)收到最后的ACK:如果A发的最后一个ACK丢了,B会重发FIN。A在TIME_WAIT状态能捕获这个重传的FIN,并再次发送ACK。若A直接关闭,B将永远卡在LAST_ACK状态,资源无法释放。
实操心得:线上服务(如Web服务器)常面临大量TIME_WAIT连接堆积。有人想通过
net.ipv4.tcp_tw_reuse=1(允许TIME_WAIT套接字被重用)或net.ipv4.tcp_tw_recycle=0(已废弃,禁用)来缓解。但tcp_tw_reuse仅对客户端有效(如服务端作为HTTP客户端调用其他API),对服务端监听端口无效。真正有效的方案是:1)增加可用端口范围(net.ipv4.ip_local_port_range="1024 65535");2)合理设置net.ipv4.tcp_fin_timeout(如30秒);3)对短连接场景,考虑使用连接池复用TCP连接,从源头减少挥手次数。
3.3 滑动窗口:接收方的“信用额度”如何动态调节?
滑动窗口是TCP流量控制的核心,它不是一个固定大小的框,而是一个随接收方应用读取速度实时伸缩的“信用额度”。我们用一个具体场景说明:
假设服务端向客户端发送一个1MB文件,客户端接收缓冲区(rmem)设为256KB。连接建立后:
- 初始
rcv_wnd = 256KB,服务端可连续发送256KB数据(约178个MSS=1440字节的段)。 - 客户端内核收到数据后,存入
sk_receive_queue,但应用层(如read()调用)尚未读取,rcv_wnd保持不变。 - 当应用层调用
read(buf, 64KB),内核从sk_receive_queue拷贝64KB到用户空间,同时将rcv_wnd增大64KB(因为缓冲区空出了64KB空间)。 - 客户端在下一个ACK包中,将更新后的
rcv_wnd值(如320KB)通告给服务端。
这个过程的关键在于:rcv_wnd的更新是异步的,且依赖于ACK包的发送时机。TCP规范允许“延迟ACK”(Delayed ACK):接收方不必每个数据段都立即ACK,可以等200ms或攒够2个段再发一个ACK。这减少了ACK包数量,但也意味着rcv_wnd更新有延迟。若应用层读取很慢,rcv_wnd长期为0,服务端就会停止发送,进入“零窗口探测”(Zero Window Probe)状态:每隔一段时间(如30秒)发一个1字节的探测包,询问“你的窗口开了吗?”。
我曾优化过某IoT平台的设备心跳上报,设备端TCP栈未实现延迟ACK,每收到一个心跳响应包就立刻发ACK,导致上行ACK包数量激增,加剧了低功耗设备的电量消耗。改为标准延迟ACK后,上行流量减少40%,电池寿命延长2倍。
3.4 拥塞控制:从慢启动到快速恢复,TCP如何学会“看路行车”
拥塞控制是TCP的智慧所在,它让无数TCP流共享网络带宽时,既不过度抢占,也不过分谦让。Linux默认使用Cubic算法(替代了传统的Reno),其核心思想是:在丢包发生前,大胆增长窗口;丢包发生后,果断收缩,再谨慎试探。整个过程分为四个阶段:
慢启动(Slow Start):连接初始,
cwnd设为10个MSS(Linux 5.10+),每收到一个ACK,cwnd增加1个MSS。因此,cwnd呈指数增长(10→20→40→80…),直到达到ssthresh(慢启动阈值)或发生丢包。这就像新车上路,先试探性加速。拥塞避免(Congestion Avoidance):当
cwnd >= ssthresh,进入此阶段。此时cwnd不再指数增长,而是每收到一个ACK,只增加1/cwnd个MSS(即线性增长)。例如cwnd=100,每收到100个ACK,cwnd才增1。增长变缓,更“稳重”。快速重传与快速恢复(Fast Retransmit & Fast Recovery):当发送方收到3个重复ACK(如连续收到
ack=1000三次),它推断seq=1000的段丢失,立即重传该段(不等RTO超时),并将ssthresh设为cwnd/2,cwnd设为ssthresh + 3×MSS(3个重复ACK代表3个已发送但未确认的段)。然后进入快速恢复,每收到一个重复ACK,cwnd加1;收到新ACK(确认新数据)后,退出快速恢复,cwnd设为ssthresh,进入拥塞避免。这比超时重传快得多,是现代TCP低延迟的关键。超时重传(Timeout):若RTO超时仍未收到ACK,则判定严重拥塞,
ssthresh设为max(cwnd/2, 2×MSS),cwnd重置为10个MSS,重新慢启动。这是最严厉的惩罚。
实操技巧:
ssthresh的初始值通常为65535(64KB),但可通过net.ipv4.tcp_slow_start_after_idle=0禁用空闲后重置为慢启动,让长连接保持较大窗口。对于高延迟网络(如卫星链路),可调大net.ipv4.tcp_rmem和net.ipv4.tcp_wmem,并启用net.ipv4.tcp_congestion_control=cubic(Cubic对高BDP网络更友好)。
4. 实操指南:从抓包分析到内核调优,解决真实世界问题
4.1 用tcpdump和Wireshark定位连接问题:看懂每个包在说什么
诊断TCP问题,第一步永远是抓包。tcpdump是服务器端的利器,Wireshark是桌面端的显微镜。我们以一个典型的“连接超时”问题为例:
现象:客户端curl http://example.com卡住,30秒后报错Connection timed out。
排查步骤:
在客户端抓包(排除服务端问题):
# 抓取目标IP的80端口,保存为pcap $ sudo tcpdump -i any -w client.pcap host 192.168.1.100 and port 80 $ curl http://192.168.1.100用Wireshark打开client.pcap,过滤TCP握手:
- 过滤表达式:
tcp.flags.syn == 1 or tcp.flags.ack == 1 and tcp.len == 0 - 查看SYN包:是否有发出?目标IP和端口是否正确?
- 查看SYN-ACK包:是否收到?若无,问题在服务端防火墙或服务未监听。
- 若有SYN-ACK,但无后续ACK:客户端可能路由错误,或本机防火墙拦截了ACK。
- 过滤表达式:
关键字段解读:
No.列:包序号,用于跟踪顺序。Time列:相对于首包的时间戳,看延迟。Source/Destination:确认IP和端口。Protocol:确认是TCP。Info列:最核心!显示SYN,SYN, ACK,ACK,PSH, ACK,FIN, ACK等标志,以及seq,ack,win(窗口大小)。- 若Info显示
[TCP Port numbers reused],说明端口复用冲突。 - 若
win=0,表示接收方窗口关闭,需查应用层是否卡住。 - 若
[TCP Retransmission],表示该包是重传,网络可能丢包或延迟高。
- 若Info显示
进阶技巧:在Wireshark中右键某个TCP流 →Follow → TCP Stream,可将整个连接的数据按应用层视角重组,直观看到HTTP请求/响应内容,快速判断是协议层问题还是应用层问题。
4.2 内核参数调优实战:不是盲目调大,而是理解每个参数的“职责”
Linux内核提供了数十个TCP相关参数,但真正影响性能的常用参数不过十来个。调优不是“越大越好”,而是匹配业务场景。以下是我在不同项目中验证过的关键参数:
| 参数 | 默认值 | 推荐值(高并发Web) | 作用原理 | 风险提示 |
|---|---|---|---|---|
net.ipv4.tcp_tw_reuse | 0 | 1 | 允许TIME_WAIT套接字被重用(仅客户端有效)。对服务端无效。 | 仅适用于客户端场景(如服务端调用外部API),服务端开启无意义。 |
net.ipv4.tcp_fin_timeout | 60 | 30 | TIME_WAIT状态持续时间(秒)。缩短可更快释放端口。 | 过短(<30)可能增加迷途包风险,需权衡。 |
net.ipv4.tcp_slow_start_after_idle | 1 | 0 | 连接空闲后是否重置为慢启动。设为0保持大窗口。 | 对长连接(如WebSocket)有益,但对短连接可能浪费带宽。 |
net.ipv4.tcp_rmem | 4096 131072 6291456 | 4096 524288 16777216 | 接收缓冲区最小/默认/最大值(字节)。增大默认值提升吞吐。 | 过大(>16MB)可能耗尽内存,需配合net.core.rmem_max。 |
net.ipv4.tcp_wmem | 4096 16384 4194304 | 4096 524288 16777216 | 发送缓冲区最小/默认/最大值(字节)。 | 同上,需配net.core.wmem_max。 |
net.ipv4.tcp_congestion_control | cubic | bbr | 拥塞控制算法。BBR(Google开发)不依赖丢包,更适合高带宽高延迟网络。 | BBR需Linux 4.9+,且某些旧设备兼容性差,上线前务必全链路测试。 |
调优实操(以Ubuntu 22.04为例):
# 临时生效(重启失效) $ sudo sysctl -w net.ipv4.tcp_fin_timeout=30 $ sudo sysctl -w net.ipv4.tcp_rmem="4096 524288 16777216" $ sudo sysctl -w net.ipv4.tcp_wmem="4096 524288 16777216" # 永久生效:写入/etc/sysctl.conf $ echo "net.ipv4.tcp_fin_timeout = 30" | sudo tee -a /etc/sysctl.conf $ echo "net.ipv4.tcp_rmem = 4096 524288 16777216" | sudo tee -a /etc/sysctl.conf $ sudo sysctl -p # 重载配置 # 验证是否生效 $ sysctl net.ipv4.tcp_fin_timeout $ ss -i 'dst 192.168.1.100:80' | grep -o 'rto:[0-9]*'注意:
tcp_rmem和tcp_wmem的三个值分别对应“最小/默认/最大”。内核会根据连接的RTT和BDP(带宽延迟积)自动在范围内调整默认值。例如,1Gbps带宽、100ms RTT的链路,BDP=12.5MB,此时默认接收窗口若仅为256KB,将严重限制吞吐。将默认值设为512KB,内核可据此计算出更合理的初始窗口。
4.3 应用层编程避坑:setsockopt的正确打开方式
很多性能问题源于应用层代码对TCP特性的误用。以下是几个高频坑点及解决方案:
坑点1:未设置SO_KEEPALIVE,长连接悄无声息死亡
TCP连接空闲时,双方内核不会主动通知对方。若中间NAT设备老化了连接映射,或服务端进程崩溃,客户端可能数小时后才发现。
解法:启用保活机制。int enable = 1; setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(enable)); // 可选:设置保活参数(Linux) int idle = 60; // 空闲60秒后开始探测 int interval = 10; // 每10秒探测一次 int count = 3; // 连续3次失败则断连 setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)); setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));坑点2:send()返回部分成功,未循环发送
send()函数返回值是“本次成功发送的字节数”,可能小于请求长度(尤其在非阻塞模式下)。若不检查返回值并循环发送,数据会截断。
解法:封装可靠的发送函数。ssize_t safe_send(int sockfd, const void *buf, size_t len) { size_t sent = 0; while (sent < len) { ssize_t n = send(sockfd, (const char*)buf + sent, len - sent, 0); if (n < 0) { if (errno == EINTR) continue; // 被信号中断,重试 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 非阻塞,缓冲区满,需等待可写事件 return -1; } return -1; // 其他错误 } sent += n; } return sent; }坑点3:recv()未处理EAGAIN/EWOULDBLOCK,阻塞模式下误判为连接关闭
在非阻塞socket上,recv()无数据时返回-1且errno=EAGAIN,这不代表连接关闭,只是暂时无数据。若代码将其等同于recv()==0(对端关闭),会导致逻辑错误。
解法:严格区分错误码。ssize_t n = recv(sockfd, buf, sizeof(buf), 0); if (n > 0) { // 正常接收n字节 } else if (n == 0) { // 对端关闭连接 } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 无数据,继续等待 } else { // 真正的错误 } }
5. 常见问题速查与独家排障经验
5.1 连接建立阶段问题
| 现象 | 可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
| SYN包发出,无SYN-ACK返回 | 1. 服务端未监听该端口 2. 服务端防火墙(iptables/nftables)拦截 3. 中间网络设备(如负载均衡器)未转发 | ss -tlnp | grep :80(查端口)sudo iptables -L -n -v | grep 80(查防火墙)tcpdump -i any port 80(在服务端抓包) | 启动服务、开放防火墙端口、检查LB配置 |
| SYN-ACK返回,但客户端不发ACK(卡在SYN_SENT) | 1. 客户端本机防火墙拦截ACK 2. 客户端路由错误,ACK发往错误地址 | sudo iptables -L -n -v | grep SYN(查客户端防火墙)ip route get 192.168.1.100(查路由) | 开放客户端防火墙、修正路由表 |
| 连接建立后立即断开(RST包) | 1. 服务端应用层拒绝连接(如Nginx配置了limit_conn)2. 客户端发送了非法SYN包(如错误的TCP选项) | Wireshark中看RST包的Info列,是否有[RST, ACK]或[RST]服务端 journalctl -u nginx查日志 | 检查应用层限流配置、验证客户端TCP栈实现 |
5.2 数据传输阶段问题
| 现象 | 可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
| 传输缓慢,吞吐量远低于带宽 | 1. 接收窗口(rwnd)过小 2. 拥塞窗口(cwnd)增长缓慢 3. 网络丢包率高 | ss -i查rwnd和cwndping -R查路由及丢包mtr 192.168.1.100查中间节点丢包 | 调大tcp_rmem、启用tcp_slow_start_after_idle=0、排查网络链路 |
| 大量重传(Retransmission) | 1. 物理链路丢包(网线/光模块故障) 2. 交换机缓存溢出 3. 接收方处理不过来(应用层卡顿) | ethtool eth0查网卡错误计数cat /proc/net/dev查tx_droppedWireshark中看重传包的 Info列 | 更换网线/光模块、调大交换机缓存、优化应用层处理逻辑 |
| 连接频繁断开(FIN/RST) | 1. 中间NAT设备老化连接映射 2. 服务端设置了过短的 timeout3. 客户端网络不稳定 | tcpdump抓包看FIN/RST来源(客户端还是服务端)服务端查应用日志 | 启用SO_KEEPALIVE、调大 |