ARTICLE DETAIL

资讯详情

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

TCP与UDP原理与实战:握手、协议栈与iperf3排障指南

TCP与UDP原理与实战:握手、协议栈与iperf3排障指南 我调试网络问题最怕遇到一种情况链路明明通着但数据就是传得不对。要么客户端报Address already in use要么抓包软件里刷出一排TCP Dup ACK要么UDP打流时丢包率忽高忽低。很多刚入门的同事把锅甩给交换机或网卡其实问题往往出在传输层——你对UDP和TCP这两个协议理解到什么程度直接决定你的排查效率。这篇文章就基于我实际项目里踩过的坑把TCP和UDP的原理、三次握手、四次挥手、协议栈细节、iperf3打流和端口测试这些内容串起来讲一遍偏实战少讲空话。1. TCP与UDP到底在解决什么问题1.1 先搞清楚网络为什么要分“连接”和“无连接”在TCP/IP协议栈里IP层做的其实非常单纯它只负责把数据包从源地址送到目的地址不关心包和包之间有没有关系。可应用层干的事从来不是“丢一个包就完”比如传输一个文件可能要几千个数据包控制一台机械臂则需要每个指令都被对方准确确认。UDP和TCP就是在IP层之上针对“怎么组织这些数据包”给出了两种不同的方案。UDP用户数据报协议非常直白应用层把一段数据交给UDP层UDP加上源端口、目的端口和校验和后直接就塞进IP包里发出去。所谓“无连接”不是没有端点而是发送之前不需要任何协商发完就完了。它就像往远处扔一个包裹地址写了能不能完整到达不完全受控。TCP传输控制协议则完全不同。它会在发送数据之前与对端建立一条逻辑连接并且在连接期间维护序号、确认号、滑动窗口、重传计时器等一系列状态。如果UDP是“寄信”TCP更像是“打通电话”先拨号、对方接听、双方确认线路畅通才开始说话。一个关心“数据放出去了”一个关心“数据可靠送达并且顺序正确”这就是两者最根本的分歧。1.2 为什么会有“协议栈”这种说法常听到“TCP协议栈”“UDP协议栈”很多人以为是某个独立软件包其实是指操作系统内核里对应该协议的完整实现。一个完整的TCP协议栈包括连接管理、数据分片与重组、滑动窗口、超时重传、拥塞控制等模块UDP协议栈则轻得多基本只有端口映射、校验和计算和数据交付。实操层面协议栈差异最直观的体现是CPU占用和内存行为。TCP连接多了以后内核要维护大量定时器和状态队列连接数几万时CPU和内存会肉眼可见地涨。UDP则几乎没有连接状态信息内核只负责查端口、算校验和、丢进socket接收队列处理开销非常小。这也是为什么视频流、语音通话、游戏加速这类高带宽低延迟场景宁可承担少量丢包也优先选UDP。还有一个容易被忽略的点TCP协议栈内置拥塞控制它会根据丢包和延迟主动降低发送速度UDP完全不管这些应用发多快它就跑多快。所以局域网里用TCP打流跑出来的吞吐往往低于UDP这不是UDP“更快”而是UDP少了那套“自我克制”的机制。后面讲iperf3 UDP打流时我会专门验证这一点。2. TCP的核心机制连接是如何建立、传输与断开的2.1 三次握手为什么必须三次两次不行吗TCP连接建立离不开三次握手。整个过程看着简单客户端发送SYN序号设成x服务器收到后回SYNACK序号设成y确认号是x1客户端再回一个ACK确认号是y1。到此双方才算真正建立连接。为什么必须是三次我常用对讲机类比。A呼叫B“听到请回话”B回答“我听到了”如果A不回复“我也听到了”B就无法确认“A能听见B的回答”。TCP要求的是双向确认必须有一个对确认的确认所以最少三次。实际抓包里握手阶段还能看到MSS、窗口大小、时间戳、SACK Permitted等选项。MSS最大报文段长度是双方协商出来的通常取路径MTU减去IP头和TCP头。默认以太网MTU是1500字节MSS一般是1460字节。如果TCP协议栈没有开启SACK出现丢包时重传效率会差很多。我一个同事调内网传输慢抓包发现SYN里没有SACK选项导致链路上偶尔丢一个包就要等待超时重传整体带宽被拖垮。2.2 四次挥手不是每次断连都能看到完整四步断开连接需要四次交互主动方发FIN被动方回ACK被动方再发FIN主动方回ACK。很多人问为什么不是三次原因在于TCP允许“半关闭”主动方发FIN只表示“我这边没有数据要发了”但被动方可能还有数据没发完所以ACK和FIN不能合并必须分两步。四次挥手里最折磨人的状态是TIME_WAIT和CLOSE_WAIT。TIME_WAIT是主动关闭方发出最终ACK后停留的状态Linux上默认通常要等60秒两倍MSL。为什么等这么久因为最后一个ACK如果丢了对端会重发FIN主动方必须能再回一次ACK同时还要防止旧连接的延迟数据包混进新连接。TIME_WAIT本身没问题但它会占住本地端口大量堆积时就会引发“Address already in use”这个我们到第5章细说。2.3 可靠传输序号、确认号、滑动窗口与快速重传TCP可靠传输的基础是“序号确认号”。发送方按字节编号接收方收到数据后回ACK告诉对方“我期待的下一个字节序号是多少”。如果发送方迟迟没收到ACK就会超时重传。但“超时重传”太慢了所以TCP设计出快速重传接收方一旦发现某个包丢了但后续包还在到达就会连续重复ACK同一个序号。发送方收到三次重复ACK就认定丢包立刻重发不用等超时。网络排查中常见的TCP Dup ACK就是这套机制在起作用。出现大量Dup ACK不一定代表网络马上断了而是说明链路上出现轻微乱序或丢包。只靠“发一个等一个”效率太低所以TCP引入了滑动窗口。接收方在ACK里携带自己的窗口大小发送方在窗口范围内可以连续发送多个包不必等待逐个确认。窗口越大链路利用率越高。用iperf3测TCP吞吐上不去先检查两端窗口是否被限制这是一个非常关键的排查点。2.4 TCP端口号、四元组与单连接实验的认知很多新手会把“端口号”和“进程”绑死其实TCP连接是用四元组唯一标识的源IP、源端口、目的IP、目的端口。同一个本地端口可以承载大量连接只要对端IP或端口不同就不是同一条连接。这也是nginx作为反向代理时能用几十个worker进程维护几十万TCP连接的原因。所谓TCP单连接实验就是客户端与服务端只建立一条TCP连接用iperf3单线程跑吞吐。这个实验的意义在于排除多连接并发带来的干扰单独看一条长连接的带宽上限。我在公司内网做过一次标定单条TCP连接能跑550Mbps但四条并发连接能跑到950Mbps。这说明单连接的窗口或拥塞控制算法没有把链路占满而不是网卡不够快。3. UDP协议栈特征与调试方法3.1 UDP协议栈为什么轻却依旧容易出问题UDP协议栈没有连接表、没有重传队列、没有拥塞窗口。内核收到UDP数据包解析出端口后直接放进对应socket的接收队列如果端口上没有socket监听内核就回一个ICMP Port Unreachable。有人以为“UDP能收到回包就说明端口通”其实这个回包恰恰说明目标端口是关着的。UDP调试难就难在“无状态”。TCP通过connect立刻能判断端口是否开放UDP却没有任何连接状态可供参考。我判断UDP链路是否正常通常分三步走第一两端Ping得通第二目标端口确实有进程在监听第三用一个能识别的应用层报文去试收到预期响应才算通。还有一点很容易被忽视UDP没有流控应用层设置的发送速率必须自己负责。如果应用层创建了一个发送线程以固定速率往外灌包而对端处理不过来内核缓冲区就满了之后的包被直接丢弃。这种问题不是UDP“不稳定”而是没有背压机制。3.2 UDP端口测试与iperf3打流我常用的两种方法UDP端口测试最常用的是ncnetcat和iperf3。nc的用法很简单在接收端执行nc -u -l 5000在发送端执行echo test | nc -u 目标IP 5000。接收端如果打印出test说明UDP报文到了对方机器但还不能证明应用层正常因为nc只是把包收进协议栈。真正想测UDP链路质量我建议用iperf3。服务端启动iperf3 -s -p 5001客户端执行iperf3 -c 目标IP -u -b 100M -p 5001。这里的-b是目标发送带宽不是网卡限速。跑完以后服务端会输出实际接收速率、丢包率和抖动Jitter。我做千兆局域网打流时一般先以500M试几秒再逐步加大到900M观察丢包率从哪个点开始飙升。如果带宽还没到物理上限就开始丢包那问题多半出在网卡中断合并、驱动队列深度或交换机背板而不是UDP本身。嵌入式场景里我更喜欢写一个固定报文做UDP探测。比如往目标IP的5000端口发一串十六进制数据另一端用Wireshark抓包确认报文的源端口、目的端口和负载内容是否一致。Zynq这类FPGA做以太网调试时我也会在PS端用这套方法先验证UDP通路再往上加业务逻辑。3.3 UDP校验和、MTU分片与乱序三个必须记住的坑UDP报文头只有8字节其中校验和字段在IPv4下可以设为0。校验和覆盖伪首部和UDP报文一旦出错接收方直接丢弃。但因为UDP没有重传机制一个校验错误的包丢就丢了上层不做恢复数据就缺一块。MTU分片是UDP应用更常见的坑。TCP因为有MSS协商报文会控制在一个MTU内UDP应用如果不做分段一个包可能几KB甚至几十KB。IP层会把大包拆成多个分片其中一个分片丢了整个UDP报文都会被丢弃。我调过一例视频传输问题应用层一包设成32KB途经MTU 1500的链路抓包看到大量分片丢失后来把数据块控制在1200字节以内丢包率立刻降了下来。乱序问题同样不容忽视。UDP没有序列号接收方按到达顺序把数据交给上层。当网络里出现负载均衡、等价路由或多链路聚合时原本按顺序发出的包可能后发先至。打流报告里的Jitter升高往往同时伴随乱序排查方向不能只看丢包。4. TCP与UDP的横向对比和选型思路4.1 一张决策表看懂TCP和UDP的核心区别很多面试题喜欢对比这两个协议我更愿意用一张“工程决策表”来收尾这类讨论维度TCPUDP连接状态面向连接三次握手建立无连接直接发包传输单位字节流无消息边界数据报保留消息边界可靠性确认、重传、有序交付尽力而为可能丢包乱序流量控制滑动窗口、拥塞控制无流控速率由应用决定头部开销至少20字节8字节典型场景文件传输、远程登录、Modbus TCP、数据库语音、视频、游戏消息、状态上报这里有一个被反复误读的说法“TCP可靠UDP不可靠。”准确讲TCP检测到丢包就重传所以对端最终能拿到完整数据UDP不提供这种保证但只要链路质量好、报文尺寸合理UDP也能做到接近“可靠”。可靠性是协议行为的结果不是承诺。4.2 工业自动化为什么常用Modbus TCP而不直接用UDP以工业自动化为例三菱FX5U支持Modbus TCP主站功能常见用法就是PLC通过TCP连接读写远程设备的线圈和寄存器。为什么这里要用TCP因为控制器发出一个写线圈命令后必须确认对端真的执行了。如果直接用UDP回包丢失时就会出现“命令实际执行了但上位机以为没执行”的灾难。C#写Modbus TCP客户端和Java写类似客户端本质上都要面对同一个问题TCP是字节流不能保证一次read就读到完整报文。所以Modbus TCP会在报文头用7字节MBAP头标明后续长度应用层必须按长度字段循环读取。我记得早期接手一个项目代码里只调用了一次Read结果在跨网段时频繁出现解析错乱后来改成“先读7字节头再根据长度字段读完整报文”问题才彻底消失。换个场景如果只是设备状态上报比如传感器每秒上报一次温度丢一个采样点无所谓下一条数据还能补上。这种业务用UDP就非常合适没有连接状态代码简单不需要处理重连、粘包和超时一个sendto完事。选型没有绝对优劣关键是看你的业务能否容忍丢包和乱序。4.3 用打流“标定”链路TCP单连接和UDP打流的组合用法有同事问“TCP标定原理”到底指什么这里说的“标定”在工程里通常指用已知流量测量链路性能再反过来调整参数。最常用的组合就是TCP单连接实验加UDP打流。先用iperf3跑TCP单线程测出这条连接的实际吞吐再在同一条链路上用iperf3 UDP打流固定发送速率对比两端速率和丢包率。这两种结果放在一起能判断瓶颈到底在物理链路还是TCP协议本身。我之前测一块Zynq以太网板卡UDP打流能跑到940Mbps接近千兆线速但TCP单连接只有500Mbps。这个对比说明板卡的物理收发没有问题瓶颈在TCP协议栈的窗口和拥塞控制参数。后来调整了TCP窗口大小并开启窗口自动缩放TCP单连接才跑到900Mbps以上。所谓标定就是把链路各层的能力逐项量化。5. 常见问题排查与避坑技巧5.1 Java TCP客户端重连时报“Address already in use”怎么办这是出现频率最高的生产问题之一。关键词在于TIME_WAIT。客户端如果固定bind了一个本地端口连接关闭后这个端口会进入TIME_WAIT状态持续几十秒。如果重连代码没有设置SO_REUSEADDR或者没有改用系统分配的临时端口内核就会拒绝绑定报“Address already in use”。我的处理顺序是先看是不是固定端口导致改成端口0让系统分配如果业务必须固定端口就在socket上设置SO_REUSEADDR再不行就调整系统参数Linux下用net.ipv4.ip_local_port_range扩大临时端口范围Windows下用netsh interface tcp show global查看TCP参数必要时修改timedwaitdelay。nginx作为反向代理时如果出现大量TIME_WAIT占用连接端口我还会配合调节keepalive超时时间降低短连接重复建连的频率。5.2 Docker报错“ports are not available”的经验总结容器启动时报error response from daemon: ports are not available: exposing port tcp 0.0.0.0:xxxx本质上就是宿主机端口被占用或超出可用范围。我遇到过两种典型情况第一宿主机上已有进程监听同一端口即使容器内部程序没冲突宿主机映射也失败第二系统临时端口段被短连接耗光尤其在高频创建TCP连接的开发机上很容易出现。排查顺序建议是先ss -tlnp看端口是否被监听如果没监听再检查系统动态端口范围。Linux下看ip_local_port_rangeWindows下用netsh int ipv4 show dynamicport tcp。很多Docker映射失败其实是映射端口和本地已监听的端口撞车直接换个端口最省事。5.3 TCP Dup ACK和乱序重传怎么定位到具体原因抓包看到大量TCP Dup ACK时不要急着关SACK先判断丢包发生在哪个方向。比如客户端下载大文件服务端发出来的数据包如果到达客户端时顺序乱了客户端就会重复ACK一个更早的序号服务端收到三次重复ACK后快速重传。这个时候优先查网络拓扑里的等价路由、链路聚合或双网卡绑定是否导致不同包走了不同路径。还有一种情况是接收缓冲区太小数据包其实到了网卡但socket缓冲区已满内核直接丢弃应用层看到的现象是“读不完整”。抓包能看到包但上层就是缺数据。此时可以用setsockopt调大SO_RCVBUF或在系统层面调整rmem_max。做高并发TCP接入时还要关注文件描述符上限、连接超时时间和内存上限这三者经常互相影响。5.4 常见问题速查表现象可能原因快速检查手段TCP连接建立失败端口未监听、防火墙丢SYN、半连接队列满ss -s看SYN_RECVtelnet ip port验证大量TIME_WAIT短连接太多主动关闭方没有调优ss -tan state time-wait统计大量CLOSE_WAIT服务端没有关闭对端断开的socket代码检查read返回0时是否closeUDP丢包严重MTU分片、接收缓冲区满、网卡队列不足ping大包iperf3 -u丢包率抓包见Dup ACK链路乱序、轻微丢包检查等价路由与链路聚合策略本地端口被占TIME_WAIT堆积、端口范围太小看ip_local_port_range调整内核参数6. 应用层到内核实操心得与细节补充6.1 理解TCP流边界和UDP报文边界才能不写错代码我教新人写TCP/IP socket程序时一定会问一个问题你用TCP发送两次send接收方用多少次recv能收完答案是不一定。TCP是字节流协议层不保存消息边界两个send的数据可能合并成一次recv也可能被拆成多次。应用层必须定义自己的消息帧格式比如头部长度字段、分隔符或者固定结构体。C语言实现TCP socket编程时最常见的bug就是调用一次recv以为能读到一个完整消息结果数据被拆开。我的做法是每个连接维护一个动态缓冲区先读够头部根据头部里的长度字段再读指定字节。LabVIEW上位机和NI实时机TCP交互时也有同样问题TCP Read VI一次能读到的字节数取决于底层缓冲区和当前到达数据量很多人在“信息量查询”这一步卡住本质就是没有按消息边界处理而是试图一次拿完。UDP则完全不同recvfrom一次返回一个完整的数据报不存在合并或拆包问题。但这个特性也意味着你收到一个UDP包时如果应用层协议本身自带长度字段不要轻易拿它判断“包是否完整”因为UDP已经保证要么整个包到达要么整个包被丢弃。6.2 TCP协议包如何修改调试中怎么干预有些场景需要模拟异常网络或修改TCP协议包比如测试快速重传、校验MSS、甚至改变窗口大小。最轻量的办法是抓包后用tcpedit这类工具离线修改pcap再回放如果要干预实时通信可以在中间机上用iptables配合modprobe内核模块修改每个包的TOS、DSCP或MSS。修改TCP包的MSS是很实用的操作。比如一条隧道MTU比链路MTU小TCP握手时的MSS值超过隧道限制数据包就会分片或丢包。我常用iptables -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1200把中间设备的MSS强制改成1200许多“能Ping通但网页打不开”或“TCP大包不通”的问题就是这么解决的。不过这个操作只影响途经这台设备的新连接对已经建立的连接无效所以改完要重连测试。6.3 用一段Python代码快速做TCP和UDP连通性测试这里放一个我常用的最小化连通性测试代码。TCP侧非常直接连接成功就是端口通import socket def tcp_check(ip, port, timeout3): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) try: s.connect((ip, port)) print(TCP connect ok to, ip, port) return True except Exception as e: print(TCP connect failed:, e) return False finally: s.close()UDP侧需要多沟通一步因为“UDP通不通”不能靠connect判断需要发一个应用层能识别的报文import socket def udp_probe(ip, port, payloadbping, timeout3): s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(timeout) sent s.sendto(payload, (ip, port)) print(udp packet sent, bytes:, sent) try: data, addr s.recvfrom(1024) print(udp response:, addr, data) except socket.timeout: print(udp no response, port may be closed or filtered)这段代码也可以用在ESP01S这类WiFi模组调试中。模组通过AT指令连接TCP或UDP后PC端脚本向模组IP和端口发数据固件收到后再回传这样就能快速验证整条链路。做UDP探测时一定要提前确认对端应用层会回什么报文否则收到一个ICMP Port Unreachable也会误判为“UDP通了”。最后再分享一个小经验很多刚做网络调试的人喜欢把一个奇怪的故障归因到“协议有问题”但我调试下来最终多数问题出在缓冲区、端口复用和MTU分片而不是TCP或UDP本身的实现。你要做的是先定性再定量用抓包和打流工具把状态数据拿出来再谈优化。这样一套流程走下来大部分传输层问题都能在十分钟内定位到方向。
返回列表