
简介一份面向网络协议学习与调试的TCP/UDP助手源码包适合正在学习C、C#网络编程或需要快速搭建Socket调试环境的开发者。资源共62个文件包含7个C源码、6个头文件、可直接运行的exe及配套Qt运行库DLL另有配置与说明文本压缩包约18.71MB目录中还保留工程配置和编译中间文件便于研究Qt界面与TCP/UDP收发逻辑。已有1880人学习下载。压缩包内SocketTool为可直接运行的调试工具SocketToolSrc提供完整源码覆盖UDP套接字创建、收发数据以及TCP客户端/服务端连接、读写等关键流程并结合C#网络类与C套接字库帮助读者对照理解两种语言下的Socket实现。readme与device等说明文件有助于快速掌握工程结构和调试方法对想结合协议原理与真实代码进行网络调试、排查通信问题的开发者是一份实用的参考资料。1. TCP和UDP网络调试助手带源码的价值不是多一个工具是多一条改数据的路做设备联调的朋友都经历过这种场面手里一个串口转网口的模块协议文档写着4字节长度头 JSON 负载随便下个调试助手只能手动敲 hex每次都要数着字节往里塞。更要命的是对端有时会回一帧错位数据你要在收到的流里按偏移抠字段——不给源码的工具只能对着十六进制屏幕干瞪眼。这个标题里的 TCPUDP 网络调试助手C# 负责 TCP 链路、C 负责 UDP 链路两侧收发都摊在源码层等于把数据的入口和出口都交到你手里。适合谁三类人。第一类是写上位机、做设备联调的工程师工具能带上协议解析扩展能力比什么都值。第二类是刚开始碰 C 或 C# 的 Socket 编程、想找一个能跑的最小骨架的开发者。第三类是手里已经有调试助手、但被收发逻辑卡住的人——这种源码包真正值钱的地方不是拿来用是拿来改。2. 为什么TCP交给C#、UDP交给C协议特性决定分工拿到TCP 用 C#、UDP 用 C这个设定第一反应是拗口为什么不全用一个语言拆开想就明白了TCP 调试和 UDP 调试要盯的东西根本不一样。TCP 盯着连接状态三次握手、四次挥手、断线重连这些 C# 的封装省心。UDP 盯着数据本身数据报边界、丢包、缓冲区C 对裸 socket 和字节数组的控制更直接。两门语言各管一段不是炫技是被联调现场逼出来的分工。2.1 TCP三次握手在调试助手里的存在感C#的连接状态管理恰好对得上TCP 调试助手里连接状态不是后台细节而是界面上的第一信息。用户打开工具第一眼要看连接上了没。TCP 三次握手的三个包SYN、SYN-ACK、ACK在 C# 底层被 TcpClient.Connect 处理掉了但调试时要让自己看见这个过程。常见的做法是在 BeginConnect 回调里打一条handshake done日志确认握手完成同步 Connect 在 3 秒超时后会抛 SocketException异常类型能区分超时和对端拒绝这个信息对定位问题很有用。一个我踩过不少次的坑界面用 Connected 属性判断状态但 Connected 只表示上一次曾建立过连接不代表当前通不通。TCP 链路在对端崩溃后仍然显示 Connected直到发数据才暴露问题。C# 的 TcpClient.Client.Poll 可以检查连接活跃性但最可靠的办法是自己发心跳帧500ms 一次没回包就标记重连。这个逻辑用 System.Timers.Timer 写起来非常顺手这也是 C# 做 TCP 调试工具的天然优势。C# 的异常体系在联调时也帮了大忙。SocketException 的 SocketErrorCode 字段直接给出 AddressAlreadyInUse、ConnectionRefused、TimedOut 这些枚举值不用像 C 语言里那样查 errno 对照表。日志里把 SocketErrorCode 打出来现场的人照着枚举名就能判断是端口被占、对端拒绝还是网络不通省掉一堆来回确认的时间。2.2 UDP无连接特性C的sendto/recvfrom天生适合调试UDP 没有握手、没有流边界调试 UDP 最常做的事是发一个包看回包。这个过程中要直接操纵数据报、来源地址、端口。C 的 sendto/recvfrom 把目标地址、来源地址全摆在函数参数里调试信息一目了然。C# 也能做但中间隔着 SocketAsyncEventArgs 封装出错信息反而绕了一圈。C 还有一个独门优势UDP 数据报可以天然映射到结构体上。对端设备发来一个 24 字节的状态帧前 2 字节设备 ID后 4 字节时间戳中间 12 字节传感器值。直接定义一个结构体把 recvfrom 的缓冲区强转成指针按字段取就完了。嵌入式联调里这是常规操作换成 C# 得走 Marshal 或手写偏移计算代码一多就容易错位。UDP 没有 TCP 那样的状态机调试助手反而要更关注协议栈边界一个 UDP 数据报最大载荷 65507 字节超过 MTU 会被 IP 层分片对端重组失败就丢包。组播调试还要过 IGMP网卡没加入组播组数据到不了应用层。这些细节 C 的 socket 选项都能直接操作遇到组播问题用 setsockopt 加 IP_ADD_MEMBERSHIP 就能解决这也是标题里 UDP 源码用 C 落地最实际的原因。2.3 混合语言不是炫技TCP的短板和UDP的短板正好两种补法表面看是语言分工背后是短板互补。TCP 的短板在于连接管理、粘包缓存、重连策略。C# 的异步编程模型、Timer 体系和垃圾回收让这些逻辑写起来不用太操心内存。UDP 的短板在于字节缓冲控制、丢包统计、原始数据 dump。C 对内存布局的控制力强结构体对齐、字节序转换都可以精细处理。如果两段都塞给一门语言要么是 C# 在 UDP 大数据报上性能绕要么是 C 写 UI 界面痛苦日常联调两种都不讨好。实际项目里这类调试助手常常是 C# 主程序做界面和 TCPC 子进程或动态库承载 UDP 收发两边通过共享日志窗口或队列通信。所以后面我说TCP 用 C#、UDP 用 C不是硬凑是现实项目里反复验证过的分工。2.4 调试助手的通用数据管线从文本框到网线的五步和三个出错点不管用哪种语言调试助手的收发管线都一样用户在输入框输入内容按当前模式ASCII 或 Hex解析成 byte[]决定是否自动拼接长度头、校验和调用 send 或 sendto 发送接收线程收到数据按长度头拆分后回显最容易翻车的是第 2、3 步。C# 字符串默认 UTF-16转 byte[] 必须显式指定 Encoding.ASCII 或 Encoding.UTF8否则中文和特殊字符会写成两套结果。C 的 char* 传进 sendto 之前要确认中间没有隐式的宽字符转换。Hex 解析要做容错同时接受0A 0B0a0b0A,0B三种写法。这一步值得写一个 Helper 函数后面三章都会用到。第三个出错点是日志。调试工具叠加协议解析之后日志里如果只有一串 hex复盘时完全无从下手。我习惯在每条收发记录里加三样东西时间戳到毫秒、方向标记RX/TX、以及这一帧对应的文本说明。C# 侧用 Stopwatch 记耗时C 侧用 QueryPerformanceCounter 记两边格式对齐联调时对着日志就能把时序还原出来。提示日志格式越早统一越好。我见过 C# 那边打 UTC、C 那边打本地时间对端设备又是开机秒数三方一对比对线都对不上。3. 用C#把TCP调试助手跑通TcpListener、粘包处理和三个必调参数TCP 这边我用 C# 写原因上一章说过连接管理和 UI 处理太省心了。但省心不意味着能偷懒TCP 是字节流协议没有报文边界调试助手最重要的隐蔽工程就是粘包处理。这一章给一个能直接编译的最小骨架再把粘包方案和参数边界讲透。3.1 一个能开始联调的C# TCP服务端TcpListener加异步读骨架先用最基础的方式把收发跑通。下面代码只做一件事监听一个端口接受客户端连接把收到的字节按 hex 打印出来。using System; using System.Net; using System.Net.Sockets; public class TcpDebugServer { private TcpListener _listener; private TcpClient _client; private readonly byte[] _recvBuffer new byte[1460 * 8]; // 缓冲对齐到8个MTU public void Start(int port) { _listener new TcpListener(IPAddress.Any, port); _listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); // 避免TIME_WAIT导致重启失败 _listener.Start(10); // backlog挂起连接队列长度 _listener.BeginAcceptTcpClient(OnAccept, null); Console.WriteLine($[TCP] listening on {port}); } private void OnAccept(IAsyncResult ar) { _client _listener.EndAcceptTcpClient(ar); Console.WriteLine($[TCP] client connected: {_client.Client.RemoteEndPoint}); // 立刻挂下一次接受支持多客户端轮流接入 _listener.BeginAcceptTcpClient(OnAccept, null); var stream _client.GetStream(); stream.BeginRead(_recvBuffer, 0, _recvBuffer.Length, OnRead, stream); } private void OnRead(IAsyncResult ar) { var stream (NetworkStream)ar.AsyncState; int len; try { len stream.EndRead(ar); } catch (SocketException ex) { Console.WriteLine($[TCP] read error: {ex.SocketErrorCode}); _client.Close(); return; } if (len 0) { Console.WriteLine([TCP] client closed); _client.Close(); return; } Console.WriteLine($[TCP] recv {len} bytes: BitConverter.ToString(_recvBuffer, 0, len)); stream.BeginRead(_recvBuffer, 0, _recvBuffer.Length, OnRead, stream); } public void Send(byte[] data) { if (_client ! null _client.Connected) { _client.GetStream().Write(data, 0, data.Length); } } }代码逻辑说明Start 里先设 ReuseAddress再开监听。BeginAcceptTcpClient 的回调在 .NET 线程池线程上执行拿到客户端后马上再挂一次 BeginAcceptTcpClient这样下一个客户端才能接进来。BeginRead 每次最多读 _recvBuffer.Length 字节但 TCP 是流一次读到的可能是一个包的一半也可能是两个包拼在一起这就是下一节的粘包来源。参数说明_recvBuffer 大小 1460*8 是我推荐的值。1460 是标准以太网 MTU 1500 减去 TCP 头后的常见 MSS乘 8 为了一次 read 能容纳局域网常见的突发数据。调太大会浪费内存调太小则一次 read 拿不全一帧。backlog 参数 10 足够这是还没被 accept 的连接队列长度不是最大连接数。3.2 粘包半包处理长度前缀方案的完整代码与边界参数调试 TCP 时一定会遇到对端一次发 1000 字节界面里收到两次一次 300、一次 700。这不是丢包是 TCP 栈把流切开了。调试助手里必须做组包。最通用的做法是4 字节长度头 payload长度头在前用户发送时自动拼上。using System; using System.Collections.Generic; using System.IO; using System.Net.Sockets; public class TcpPacketDecoder { private MemoryStream _cache new MemoryStream(); private readonly byte[] _recvBuffer new byte[1460 * 8]; // 把这个方法接在 OnRead 里替代直接打印 public void Feed(NetworkStream stream, int receivedCount) { _cache.Write(_recvBuffer, 0, receivedCount); while (_cache.Length 4) // 至少能读长度头 { _cache.Position 0; byte[] head new byte[4]; _cache.Read(head, 0, 4); int payloadLen BitConverter.ToInt32(head, 0); if (payloadLen 0 || payloadLen 1024 * 1024) { // 长度越界说明协议错乱了清空缓存重新对齐 _cache.SetLength(0); return; } if (_cache.Length 4 payloadLen) { // 半包数据还没到齐等待下一次 break; } byte[] payload new byte[payloadLen]; _cache.Position 4; _cache.Read(payload, 0, payloadLen); HandlePacket(payload); // 把完整包交给上层逻辑 // 移除已消费的数据保留剩余字节 byte[] rest _cache.ToArray(); _cache.SetLength(0); _cache.Write(rest, 4 payloadLen, rest.Length - 4 - payloadLen); } stream.BeginRead(_recvBuffer, 0, _recvBuffer.Length, OnRead, stream); } private void HandlePacket(byte[] payload) { Console.WriteLine($[TCP] packet payload {payload.Length} bytes: BitConverter.ToString(payload)); } }逻辑说明MemoryStream 当累积缓存每次 read 回来的数据先写进去再循环拆包。拆包时先读 4 字节长度头判断缓存里是否已经有完整 payload没有就 break 等下次有就取走 payload 并把剩余字节搬回缓存头部。这里最容易写错的是最后一步移除已消费数据后源和目标重叠我用 ToArray 再重写的方式规避了 Array.Copy 的重叠指针问题。参数说明payloadLen 上限 1MB 是防呆用的真实调试的单帧不会超过几十 KB。如果对端是嵌入式设备长度头常常是大端序网络字节序这时要把 BitConverter.ToInt32(head, 0) 替换成BitConverter.ToInt32(head.Reverse().ToArray(), 0)否则解析出的长度是颠倒的粘包逻辑会立刻错乱收到几帧后长度越界清缓存。3.3 TCP调试助手必调的三个参数连接超时、keepalive和backlog第一个是连接超时。C# 的 TcpClient.Connect 默认可能等很久才失败局域网联调设 3 秒足够。新版 .NET 可以直接设client.ConnectTimeout 3000旧框架里一般用 Task.Run 包一层 WaitAsync。超时后要区分是网络不通还是对端拒绝SocketErrorCode 是 TimedOut 查网线是 ConnectionRefused 查对端服务是否启动。第二个是 keepalive。TCP 自带的 keepalive 默认 2 小时才探测一次调试场景毫无意义。我一般做应用层心跳System.Timers.Timer 每 500ms 发一个 4 字节的心跳帧对端回包则记录最近活跃时间连续 3 次没回就主动 Close 并标记重连。心跳帧要单独定义不能和业务数据混在一起否则协议解析会串。第三个是 backlog。监听 socket 的挂起队列长度就是我代码里的Start(10)。客户端多了会出现能建连但发不出去的假象——连接在队列里还没被 accept数据流阻塞TCP 窗口降为 0。遇到这种问题第一反应查 backlog不是查业务代码。联调场景 10 就够了不需要调大。4. 用C把UDP调试助手跑通recvfrom轮询、缓冲区调参和收发统计UDP 这边用 C 写核心是 socket 创建、bind、recvfrom 循环这三件事。UDP 没有连接概念bind 之后立刻能收发不需要 listen。但正因为没有连接很多参数要靠自己盯recvfrom 的阻塞超时、接收缓冲区大小、数据报截断边界。这一章给一个完整可编译的骨架再把参数讲清楚。4.1 最小C UDP调试骨架socket绑定、recvfrom轮询与回包下面代码是 Windows Winsock2 的实现。换成 Linux 的话把 WSAStartup 和 WSACleanup 去掉#include winsock2.h换成sys/socket.h和netinet/in.hsocket/ bind/ recvfrom 的接口完全一致。#include cstdio #include cstring #include string #include winsock2.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), wsa) ! 0) { printf(WSAStartup failed\n); return -1; } SOCKET sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock INVALID_SOCKET) { printf(socket() failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; } sockaddr_in local{}; local.sin_family AF_INET; local.sin_port htons(6000); // 绑定本地端口 local.sin_addr.s_addr htonl(INADDR_ANY); if (bind(sock, (sockaddr*)local, sizeof(local)) SOCKET_ERROR) { printf(bind() failed: %d\n, WSAGetLastError()); closesocket(sock); WSACleanup(); return -1; } char buf[65507]; // UDP 最大载荷避免 recvfrom 截断 sockaddr_in from{}; int fromLen sizeof(from); printf(UDP debugger listening on port 6000\n); while (true) { memset(buf, 0, sizeof(buf)); fromLen sizeof(from); // 每次调用前重置否则部分实现会截断 int ret recvfrom(sock, buf, sizeof(buf), 0, (sockaddr*)from, fromLen); if (ret 0) { printf(recv %4d bytes from %s:%d\n, ret, inet_ntoa(from.sin_addr), ntohs(from.sin_port)); // 回一条 ack让对方确认链路是通的 const char* ack ack; sendto(sock, ack, strlen(ack), 0, (sockaddr*)from, sizeof(from)); } else if (ret SOCKET_ERROR) { printf(recvfrom error: %d\n, WSAGetLastError()); if (WSAGetLastError() WSAETIMEDOUT) { continue; // 超时继续轮询 } break; } } closesocket(sock); WSACleanup(); return 0; }逻辑说明socket 第一个参数 AF_INET 表示 IPv4第三个参数 IPPROTO_UDP 指定 UDP 协议。bind 把本地端口固定到 6000这是调试助手最常见的行为——你知道数据从哪个端口出来防火墙和抓包都更容易确认。recvfrom 阻塞等待数据from 结构体在返回时填入发送方地址这样才知道回包发给谁。参数说明buf 声明成 65507 字节这是 UDP 数据报的理论上限也是调试时最容易忽略的地方。如果 buf 太小一个较大的数据报会被截断剩余部分直接丢弃还没法靠重传找回。fromLen 每次调用前必须重置成 sizeof(from)否则 Windows 下可能返回截断的地址。4.2 用SO_RCVTIMEO给recvfrom加超时界面轮询不卡死阻塞式 recvfrom 在调试助手里有个麻烦对端一直不发数据线程就永远停在那。要加超时Windows 下最省事的是 setsockopt 设 SO_RCVTIMEOint timeoutMs 1000; // 1 秒超时 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)timeoutMs, sizeof(timeoutMs));设了超时后recvfrom 超时会返回 SOCKET_ERRORWSAGetLastError() 是 WSAETIMEDOUT。上面的主循环里专门处理了这个分支超时继续轮询这样界面线程可以每秒钟醒来一次刷新状态。超时参数调多少是玄学吗不是。500ms 到 1000ms 是常用区间太小会让 while 循环空转CPU 占用升高太大则界面上的最近活跃时间和丢包统计更新迟钝。Linux 下我更推荐用 poll 或 epoll_wait 包一层把 recvfrom 的阻塞模式换成事件驱动但 Windows 联调场景 SO_RCVTIMEO 已经够用。4.3 UDP调试助手必调的缓冲区参数SO_RCVBUF、数据报上限和发送缓冲UDP 有三个容易踩的缓冲参数。第一个是 socket 接收缓冲区。UDP 没有重传机制内核缓冲区满了以后直接丢包。Windows 默认的 SO_RCVBUF 在几十 KB 的量级局域网里一台设备突发多帧数据就可能撑爆。我一般设 1MBint rcvBufSize 1024 * 1024; setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (const char*)rcvBufSize, sizeof(rcvBufSize));第二个是应用层缓冲区就是上面代码里的 buf[65507]。它决定了单次 recvfrom 能取回多大的数据报。65507 是 UDP 载荷上限设这个值永远不会因为缓冲不够丢帧。第三个是发送缓冲区 SO_SNDBUF这个一般不用动。sendto 要么把整个数据报放入内核队列要么立刻失败返回 WSAEINVAL 或 WSAENOBUFS不会像 TCP 那样慢启动慢慢挤。真正要关注的是发送频率——调试助手连续高频 sendto 时如果发送队列积压需要在应用层做流控比如固定 10ms 一个包。4.4 收发包统计与分包组包UDP调试里的计数和长度边界UDP 调试助手的界面通常要显示几个数字本地端口、已收包数、已发包数、最近来源地址。统计要做到真实有个细节要在 recvfrom 成功返回的路径上计数而不是界面定时器里数缓存。我维护一个volatile long recvCount每次 recvfrom 成功后 InterlockedIncrement界面只管读值。UDP 的分包组包和 TCP 的粘包是两回事。TCP 粘包要合并流UDP 超过 MTU 的包会被 IP 层分片重组失败就丢应用层不需要也不能接管。调试助手能做的只有一件事显示收到的数据报长度并且在长度接近 1472以太网 payload时提示接近 MTU可能有分片风险。C# 那边如果要用 UDP 发送大包需要自己在应用层拆包这也是 c# udp 编程里常说的分包组包——但调试工具本身不该替用户拆拆了反而掩盖了对端设备的分片问题。提示调试 UDP 时确认 MTU 的最快方法是发一个 1473 字节的包看对端是否收得到完整数据。收不到或者只收到前 1472 字节基本就是分片丢弃。5. 网络调试助手避坑TCP重连、UDP丢包和界面卡死的五个排查记录调试助手的代码不难写难得是让它一直可靠运行。下面五条是我在不同项目里真实遇到、并最后定位到原因的问题。每一条都按现象→原因→解决的顺序写。5.1 现象TCP断开后立刻重连失败报 AddressAlreadyInUse原因TCP 主动断开的一方会进入 TIME_WAIT 状态端口被内核占用约 30 秒。调试助手每次断开后立刻重新监听同一个端口bind 就报 WSAEADDRINUSE。这在快速反复重连的联调场景里特别常见。解决监听 socket 在 Start 前设置 ReuseAddress_listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);注意一定要在 bind/listen 之前设否则不生效。另一个省事的办法是调试助手退出时不主动 Close让系统接管 TIME_WAIT但遇到断开后秒重连的场景还是靠 ReuseAddress 稳妥。5.2 现象UDP广播发出去本机收不到对端却能收到原因Windows 默认情况下sendto 发到 255.255.255.255 广播地址后本机网卡没有加入广播组收不到自己的广播回显。这不是丢包是网卡根本没把广播包送到应用层。解决bind 之后加一行int broadcast 1; setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (const char*)broadcast, sizeof(broadcast));如果是组播地址比如 239.0.0.1还要加 IP_ADD_MEMBERSHIP 加入组播组。很多调试工具收不到组播的排查帖最后都发现是忘了这一步。5.3 现象C#界面一收数据就卡死窗口转圈无法点击原因BeginRead 的回调跑在线程池线程不是 UI 线程。回调里直接操作 ListBox、TextBox 会触发跨线程异常。很多人的解决是给控件加 Invoke但 Invoke 是同步的UI 线程正忙时回调会一直等界面越卡越死。解决用Control.BeginInvoke替代 Invoke它是异步的不会等 UI 线程空闲。更稳的做法是队列分离回调线程把收到的数据塞进 ConcurrentQueueUI 定时器每 500ms 取一批批量刷新。后者在高速收包时能显著减少 UI 刷新次数。5.4 现象收到的数据在界面上显示成乱码ASCII模式怎么切都不对原因调试助手hex 显示和ASCII 显示两套逻辑对数据的解释不同。最常见的是把 UTF-8 编码的中文当 ASCII 逐字节显示三个字节变成一个乱码串或者把 GBK 编码按 Latin-1 截断一个字剩半个。解决收包数据先当二进制看hex 视图永远显示原始字节。只有用户明确选择文本视图时才做解码解码优先用 Encoding.UTF8解码失败的位置标注出来。我联调时默认开 hex 视图确认是文本协议再切 ASCII因为二进制协议里出现 0D 0A 太正常了ASCII 模式会误判成换行。5.5 现象C侧sendto偶尔返回SOCKET_ERROR错误码是10049或10022原因目标地址结构体没填对。sin_port 直接赋了十进制端口号而没做 htons或者 sin_addr 没调用 inet_pton 转换导致地址无效。这种错误在界面填IP:端口字符串时最容易出现。解决sendto 之前把对端地址打印一遍printf(send to %s:%d\n, inet_ntoa(dest.sin_addr), ntohs(dest.sin_port));这一步能直接看到端口是否变成了一个奇怪的大数。我每次写完 UDP 发送代码都用这一句自查能省半小时抓瞎。另一个常见原因是析构时先关闭了 socket 再调 sendto代码里要保证 socket 生命周期覆盖所有发送点。6. 进阶验证回环自检与自动重发让调试助手自己证明没骗你6.1 回环自检先证明本机链路是通的拿到一个新的调试助手源码第一件事不是连真实设备而是做回环自检。TCP 服务端开启后客户端连 127.0.0.1UDP 监听 6000 端口再用一个发送 socket 往 127.0.0.1:6000 发数据。如果收不到说明代码里有逻辑问题不用急着怀疑网卡和防火墙。回环自检还能验证粘包逻辑一次往 TCP 服务端发 500 字节代码里把发送拆成两次 write第一次 200、第二次 300看界面输出是拼成一条 500 字节的完整日志。拼对了粘包逻辑就没问题。这一步在对接真实设备之前做定位成本最低。6.2 自动重发把重复劳动交给定时器联调时经常要反复发同一组数据手动点按钮累且容易漏。给调试助手加一个循环发送功能界面里填间隔毫秒定时器到点后把当前输入框的内容走一遍完整管线发出去。我一般把最小间隔设为 10ms太小的间隔会让 UDP 发送队列积压反而掩盖真实时序。C# 的 System.Timers.Timer 和 C 的 SetTimer 都能做这件事关键点在于都要用独立的发送线程不能占住 UI 线程。自动重发要同时支持次数限制和手动停止两个开关防止联调完忘关后台一直刷包。这个功能做完调试助手才算真正能自己验证自己回环自检证明链路在代码层是通的自动重发证明长时间运行不卡。我这些年换了不少调试工具最后留下的都是自己能证明自己没骗人的那一款。希望这篇 TCPUDP 网络调试助手的拆解和落地记录能让你少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取