☰
TCP_UDP_PerformanceTest:自研压测工具实战与避坑指南
2026/10/12 5:01:40 网站建设 项目流程

简介:TCP_UDP_PerformanceTest 是一款面向网络编程开发者与系统运维人员的传输层协议性能测试工具,用于对比 TCP 与 UDP 在真实网络环境下的吞吐量、延迟与丢包率表现,帮助判断高并发低延迟场景下应选用哪种协议。资源包共 5 个文件,压缩后约 79KB,包含可执行主程序、封装底层通信逻辑的 dll 库、记录服务器地址与端口等运行参数的 config 配置文件、存储测试结果或参数的 xml 文档以及授权信息文件,结构紧凑、开箱即用。用户可自定义发送数据量、发送速率与测试持续时间,模拟不同负载条件,观察 TCP 的滑动窗口与拥塞控制机制同 UDP 无连接快速转发之间的实际差异。目前已有 1639 人学习下载,适合从事网络编程、协议调优与系统性能优化的技术人员作为协议选型与排错参考。

1. 从一次压测翻车说起:TCP_UDP_PerformanceTest 到底测什么

很多人第一次听到 TCP_UDP_PerformanceTest 测试工具,脑子里浮现的是「跑个 iperf 不就完了」。我最早也这么想,直到某次给一个跨平台数据同步模块做验收,用现成工具跑出来的数字漂亮得离谱,上线后真实业务却频繁超时。回头一查,工具默认只测单向大包吞吐,而我们的业务是大量小包双向交互,还夹着突发流量。那次翻车让我意识到,通用工具测的是「链路极限」,而 TCP_UDP_PerformanceTest 这类自研测试工具要回答的是「我的程序在真实协议行为下能扛多少」。

它本质上是一套可控的客户端-服务端压测框架:TCP 侧关注连接建立、并发连接数、吞吐与延迟随负载的变化;UDP 侧关注丢包率、乱序、抖动以及应用层重传策略。适合谁?做网关、消息中间件、实时音视频信令、物联网上报通道的工程师。你不需要它来测网卡线速,你需要它来复现「第 800 个连接进来时延迟突然翻三倍」这种问题。下面按「先立住原理、再动手复现、最后避坑」的顺序拆开讲。

2. 协议行为差异决定了测试工具的设计骨架

2.1 TCP 与 UDP 的测试目标根本不是一回事

TCP 的测试核心是「连接生命周期 + 流控表现」。一条 TCP 连接从三次握手到四次挥手,中间经历慢启动、拥塞避免、快重传,这些状态机的行为会直接反映在吞吐曲线上。所以 TCP_UDP_PerformanceTest 的 TCP 模块必须能控制:并发连接数、每条连接的发包大小、发送间隔、是否复用连接。如果你只测单连接大包,慢启动阶段占比很小,数字好看但没意义。

UDP 的测试核心是「无连接下的可靠性代价」。UDP 本身不保证送达,所以工具要能统计:发送计数、接收计数、序列号缺口、到达时间戳。抖动(jitter)的计算依赖时间戳差值,丢包率依赖序列号连续性。很多自研工具在这里偷懒,只统计收发包数,结果丢包和乱序混在一起,排查时根本分不清是网络问题还是应用层处理慢。

提示:TCP 测的是「协议帮你做了多少」,UDP 测的是「协议没帮你做,你自己补了多少」。两者的指标口径不能混用。

2.2 为什么不用现成工具而要自己写

iperf3 足够好,但它不暴露应用层语义。比如你想测「每条连接发送 200 字节业务报文,服务端解析后回 50 字节响应」,iperf 只能模拟字节流,无法模拟这种请求-响应模式。再比如你想在 UDP 测试里加入「每 10 个包做一次确认」的应用层重传逻辑,现成工具没有这个钩子。

自研 TCP_UDP_PerformanceTest 的价值在于:把业务逻辑抽象成可配置的「报文模板 + 发送策略 + 统计维度」。常见做法是定义一个测试场景配置,包含协议类型、并发数、报文大小分布、发送速率、持续时间,然后由统一的统计模块收集两端数据。这样你测的不是「网络能跑多快」,而是「我的程序在这个网络行为下表现如何」。

2.3 最小可复现的测试拓扑与依赖

在动手写代码前,先把拓扑定下来。最简拓扑是两台机器直连或经一台交换机,一台跑服务端,一台跑客户端。如果只有一台机器,可以用回环地址,但回环不经过物理网卡,测不出真实网卡的中断合并、队列积压问题,只能验证逻辑正确性。

依赖方面,Python 方案需要标准库 socket、threading、time、statistics;如果要画图,加 matplotlib。Go 方案用标准库 net、time、sync 即可,部署更省心。下面以 Python 为例,因为它改起来快,适合快速验证想法。生产级压测建议用 Go 或 C,减少解释器开销对结果的污染。

3. 用 Python 把 TCP_UDP_PerformanceTest 的最小闭环跑通

3.1 服务端:同时监听 TCP 与 UDP 的骨架

先写服务端,它要能同时处理 TCP 连接和 UDP 数据报。TCP 部分用多线程,每个连接一个线程;UDP 部分用单线程循环收包,按序列号统计。代码里关键点是设置 SO_REUSEADDR,避免重启时端口占用。

import socket import threading import time import struct TCP_PORT = 9001 UDP_PORT = 9002 BUFFER_SIZE = 65535 def handle_tcp_client(conn, addr): """处理单个 TCP 连接:收多少回多少,记录耗时""" total_bytes = 0 start = time.time() try: while True: data = conn.recv(BUFFER_SIZE) if not data: break total_bytes += len(data) conn.sendall(data) # 回显,模拟请求-响应 except ConnectionResetError: pass finally: elapsed = time.time() - start print(f"[TCP] {addr} 接收 {total_bytes} 字节,耗时 {elapsed:.3f}s") conn.close() def tcp_server(): srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", TCP_PORT)) srv.listen(128) print(f"[TCP] 监听 {TCP_PORT}") while True: conn, addr = srv.accept() t = threading.Thread(target=handle_tcp_client, args=(conn, addr), daemon=True) t.start() def udp_server(): srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", UDP_PORT)) print(f"[UDP] 监听 {UDP_PORT}") recv_count = 0 lost_count = 0 last_seq = -1 while True: data, addr = srv.recvfrom(BUFFER_SIZE) if len(data) < 4: continue seq = struct.unpack("!I", data[:4])[0] recv_count += 1 if last_seq >= 0 and seq != last_seq + 1: lost_count += seq - last_seq - 1 last_seq = seq # 每 1000 包打印一次统计 if recv_count % 1000 == 0: print(f"[UDP] 已收 {recv_count},估算丢包 {lost_count}") if __name__ == "__main__": threading.Thread(target=tcp_server, daemon=True).start() udp_server()

逻辑说明:TCP 服务端对每个连接起一个线程,收到数据后原样回发,这样客户端可以测往返时延。UDP 服务端解析前 4 字节作为序列号,通过序列号缺口估算丢包。参数方面,BUFFER_SIZE设 65535 是为了兼容最大 UDP 数据报;listen(128)是 backlog,压测高并发时如果这个值太小,新连接会被拒绝,现象是客户端报 Connection refused。

3.2 客户端:控制并发数与发送速率

客户端要能配置并发连接数、每条连接发送多少轮、每轮报文大小。TCP 客户端用线程池模拟并发,UDP 客户端用固定速率发送并记录时间戳。

import socket import threading import time import struct import statistics SERVER_IP = "127.0.0.1" TCP_PORT = 9001 UDP_PORT = 9002 PAYLOAD_SIZE = 200 ROUNDS = 500 def tcp_worker(worker_id, results): """单个 TCP 连接:发送 ROUNDS 次,记录每次 RTT""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((SERVER_IP, TCP_PORT)) rtts = [] payload = b"x" * PAYLOAD_SIZE for _ in range(ROUNDS): t0 = time.perf_counter() s.sendall(payload) s.recv(PAYLOAD_SIZE) rtts.append((time.perf_counter() - t0) * 1000) # 转毫秒 s.close() results[worker_id] = rtts def run_tcp_test(concurrency): results = {} threads = [] start = time.time() for i in range(concurrency): t = threading.Thread(target=tcp_worker, args=(i, results)) t.start() threads.append(t) for t in threads: t.join() elapsed = time.time() - start all_rtt = [r for rtts in results.values() for r in rtts] print(f"[TCP] 并发 {concurrency},总耗时 {elapsed:.3f}s") print(f"[TCP] RTT 平均 {statistics.mean(all_rtt):.2f}ms," f"P95 {statistics.quantiles(all_rtt, n=20)[18]:.2f}ms") def run_udp_test(rate_pps, duration): """按固定速率发送 UDP 包,记录发送时间戳""" s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) interval = 1.0 / rate_pps seq = 0 send_times = [] end_time = time.time() + duration while time.time() < end_time: t0 = time.perf_counter() data = struct.pack("!I", seq) + b"y" * (PAYLOAD_SIZE - 4) s.sendto(data, (SERVER_IP, UDP_PORT)) send_times.append(t0) seq += 1 # 简单速率控制:忙等补偿 next_send = t0 + interval while time.perf_counter() < next_send: pass s.close() print(f"[UDP] 发送 {seq} 包,目标速率 {rate_pps}pps,实际 " f"{seq / duration:.0f}pps") if __name__ == "__main__": run_tcp_test(concurrency=50) run_udp_test(rate_pps=2000, duration=10)

逻辑说明:TCP 客户端每个线程独立建连,记录每次请求的 RTT,最后汇总平均和 P95。P95 比平均值更能反映尾部延迟,压测时如果 P95 远大于平均,说明有连接被调度或拥塞拖慢。UDP 客户端用忙等做速率控制,精度比time.sleep高,但会占满一个 CPU 核,生产压测机要预留核心。参数rate_pps是每秒包数,duration是持续秒数,两者相乘就是总包数。

3.3 统计口径:丢包、抖动、吞吐怎么算才不骗自己

丢包率不能只靠「发送数减接收数」,因为 UDP 服务端可能还没处理完。正确做法是客户端发送时记录序列号,服务端按序列号统计缺口,测试结束后两端对账。抖动用相邻包到达间隔的差值计算,公式是jitter = mean(|(recv[i] - recv[i-1]) - (send[i] - send[i-1])|),但需要两端时钟同步,单机测试可以忽略时钟偏差。

吞吐统计要区分「应用层吞吐」和「链路吞吐」。应用层吞吐只算有效载荷,链路吞吐要加上协议头。TCP 测试里如果只发 200 字节小包,协议头占比很高,报链路吞吐会虚高。我一般两个都报,并注明口径。

指标计算方式常见误用
丢包率序列号缺口 / 总发送用收发包数直接相减,忽略乱序
抖动到达间隔差值的均值不做时钟同步直接算
TCP 吞吐有效字节 / 耗时把回显字节也算进去
P95 延迟排序后第 95 百分位用平均值代替,掩盖尾部

4. 参数调优与结果解读:别让默认值骗了你

4.1 并发数、报文大小、发送间隔的联动关系

这三个参数不是独立的。并发数上去后,如果报文大小不变,总带宽需求线性增长,可能先撞到网卡队列上限。报文大小影响协议头开销和分片行为,UDP 超过 MTU 会分片,丢一片整个包废掉。发送间隔决定瞬时速率,间隔太小会让接收端缓冲区溢出。

我一般按「先定报文大小,再定单连接速率,最后加并发」的顺序调。比如业务报文 200 字节,单连接 500pps,先测 1 连接确认基线,再逐步加到 10、50、100 连接,观察 P95 延迟的拐点。拐点出现的并发数就是当前配置下的安全水位。

4.2 操作系统缓冲区与工具参数的对齐

Linux 下net.core.rmem_max和wmem_max决定 socket 缓冲区上限。如果工具里设了SO_RCVBUF但超过系统上限,会被静默截断。UDP 测试尤其明显:接收缓冲区太小,高速率下直接丢包,但丢包原因不是网络而是本机处理不过来。

# 查看当前缓冲区上限 sysctl net.core.rmem_max net.core.wmem_max # 临时调大(压测机专用,别在生产随便改) sudo sysctl -w net.core.rmem_max=16777216 sudo sysctl -w net.core.wmem_max=16777216

改完后在代码里显式设置SO_RCVBUF,并打印实际生效值验证。TCP 的TCP_NODELAY也要开,否则小包会被 Nagle 算法攒着发,RTT 测出来偏大。

4.3 结果解读:什么时候数字好看但没意义

如果 TCP 测试只跑单连接大包,吞吐接近线速,这不代表你的服务能扛高并发。如果 UDP 测试丢包率为零但发送速率只有 100pps,也不代表链路稳定。判断标准是:测试场景是否覆盖了业务真实的行为分布。我习惯在报告里附上「场景-指标」对照表,注明每个数字对应的并发数、报文大小、持续时间,避免拿一个数字到处套。

注意:压测结果受测试机 CPU、网卡型号、交换机缓冲影响很大。换一台机器数字可能差一倍,所以基线要在目标部署环境上测。

5. 避坑与排查:那些让结果失真的细节

5.1 现象:UDP 丢包率忽高忽低,重跑差异大

原因通常是接收端缓冲区溢出或 CPU 调度抖动。UDP 没有流控,发送端速率超过接收端处理能力时,内核直接丢包,且丢包位置随机。解决方法是先降速率找到零丢包上限,再逐步加压;同时把接收线程绑到独立 CPU 核,减少调度干扰。

5.2 现象:TCP 并发上到 200 后大量连接超时

先看 backlog 和文件描述符限制。listen的 backlog 太小,已完成三次握手的连接会排队等待 accept,超时后客户端报错。ulimit -n默认 1024,200 并发加上其他开销可能触顶。解决:backlog 设 512 以上,ulimit -n调到 65535,并在代码里用连接池复用。

5.3 现象:RTT 测出来比 ping 大很多

检查是否开了TCP_NODELAY,以及是否在循环里做了字符串拼接、日志打印等耗时操作。Python 的 GIL 在高并发下会让线程切换开销变大,RTT 包含等待 GIL 的时间。解决:压测客户端用多进程替代多线程,或换 Go 重写。

5.4 现象:吞吐数字超过网卡标称速率

多半是把双向流量加在一起算了,或者统计窗口内包含了测试准备阶段。检查统计起止点是否只覆盖发送/接收循环,以及是否把回显字节重复计数。TCP 回显测试里,客户端发送 200 字节、收到 200 字节,有效吞吐应按 200 算,不是 400。

5.5 现象:测试跑久了内存持续上涨

常见于结果列表无限追加。每次 RTT 都存进 list,跑百万次就是百万个浮点数。解决:用固定窗口滑动统计,或只保留百分位需要的样本量。UDP 服务端的序列号统计也要注意整数溢出,用 Python 的 int 没问题,但 C 里要用 64 位。

6. 进阶:把测试工具变成可复用的性能基线

最后一章说一个我常用的技巧:把 TCP_UDP_PerformanceTest 的输出固化成 JSON,每次代码变更后自动跑一遍,和基线对比。这样性能回归能在合并前发现,而不是上线后靠用户投诉。

import json def save_report(tcp_result, udp_result, path="perf_report.json"): report = { "tcp": { "concurrency": tcp_result["concurrency"], "avg_rtt_ms": round(tcp_result["avg_rtt"], 2), "p95_rtt_ms": round(tcp_result["p95_rtt"], 2), }, "udp": { "target_pps": udp_result["target_pps"], "actual_pps": round(udp_result["actual_pps"], 1), "loss_rate": round(udp_result["loss_rate"], 4), }, "timestamp": time.time(), } with open(path, "w") as f: json.dump(report, f, indent=2) print(f"报告已写入 {path}")

逻辑说明:把关键指标序列化后,可以用脚本对比两次运行的差异。阈值建议:P95 RTT 波动超过 20%、丢包率超过 0.1% 就告警。参数path按日期命名,方便回溯。这个习惯帮我拦下过好几次「功能没问题但性能悄悄退化」的提交。

另一个进阶方向是加入流量整形,用tc命令模拟弱网,观察工具在丢包 5%、延迟 50ms 下的表现。命令示例:

# 在服务端网卡上模拟 50ms 延迟和 5% 丢包 sudo tc qdisc add dev eth0 root netem delay 50ms loss 5% # 测试完清除规则 sudo tc qdisc del dev eth0 root

这样测出来的数字更接近真实跨机房场景。我一般会在 CI 里跑两轮:一轮本地回环做功能验证,一轮弱网模拟做鲁棒性验证。弱网那轮不追求高吞吐,只看程序是否崩溃、重传逻辑是否生效。

血泪经验是:别在压测机上跑其他任务,别用无线网络,别忽略测试机的散热降频。有一次我测出来的数字比预期低 30%,查了半天代码,最后发现是笔记本电源模式设成了节能。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询