资讯动态

C# Socket TCP/UDP测试工具开发:从收发调试到自动化回归

发布时间:2026/10/13 7:10:36 来源:尧图企业网站定制
简介C# SOCKET TCP/UDP 测试工具及完整源代码为需要掌握网络通信的C#开发者提供一套可直接运行的测试与学习环境。资源包共1135个文件约10.52MB以142个cs源码、37个dll动态库、15个exe可执行程序为核心配以422个png图标、118个gif演示图片、102个ico图标及多个配置文件与工程文件既能看到Socket客户端/服务端的实现细节也能直接运行编译好的工具验证TCP可靠连接与UDP实时传输。代码覆盖Socket实例创建、IP与端口绑定、监听连接、Send/Receive数据交换等关键操作并涉及错误处理与并发连接管理可执行文件便于快速测试网络通讯、检查数据收发与延迟。已有1112人学习下载适合从入门到进阶的C#网络编程者用于对照源码、调试协议通信场景。1. C# Socket测试工具为什么是联调现场的第一件武器TCP 联调最怕的就是两边都说自己没问题。服务端程序说包已经发出客户端程序说没收到两边的日志看着都对问题就卡在“中间那一段”。这时候抓包太麻烦写临时程序又太慢最直接的办法是拿出一个能自己看见收发全过程的东西一个 C# 写的 Socket TCP/UDP 测试工具。它能建连、能发能收、能显示十六进制原始数据关键是源代码就在手边协议再特殊也能改成自己想要的。这篇文章就把这个工具的选型逻辑、核心代码、踩坑点一次说清楚适合做上位机、服务端、嵌入式联调以及任何需要手工验证协议字段的人。2. 先把通信模型理顺TCP 与 UDP 的选型为什么决定工具写法2.1 TCP 和 UDP 在测试工具里的本质区别很多初学者把 TCP 和 UDP 当成“两个差不多的 Socket”等到写工具的时候才发现两者的代码流程完全不一样。TCP 是面向连接的字节流收发没有消息边界你 Send 一次对方可能分两次 Receive 到UDP 是面向报文的一次 SendTo 对应一次 ReceiveFrom边界天然存在。这个区别直接影响测试工具的功能设计。TCP 模式的工具必须处理连接状态、连接断开、重连、粘包半包这些事UDP 模式要处理的是绑定端口、每包独立收发、丢包重发策略。所以工具里的“协议模式”下拉框不是摆设它切换的是两套完全不同的收发逻辑。维度TCPUDP连接状态面向连接需要 Accept/Connect无连接不需要握手数据边界字节流无边界需要自行分包报文一次收发一个完整数据报可靠性有确认重传数据有序到达不保证到达不保证顺序调试重点粘包、半包、断线重连丢包、端口绑定、报文格式测试工具必须做的连接管理、发送接收缓冲区本地端口绑定、逐包记录做测试工具时我一般会把 TCP 和 UDP 的收发核心直接拆成两个独立会话类。虽然都用 Socket 类但 TCP 用 NetworkStream 读写更方便UDP 必须用 Socket 的 ReceiveFrom/SendTo。混在一个类里只会让 if 分支越来越多最后自己都不认识。2.2 同步、Begin/End、AsyncEventArgs三套 Socket 模型怎么选C# 里写 Socket 接收有常见三种姿势。第一种是同步阻塞一个线程卡在 Receive 上代码直白适合测试工具这种连接数不多的场景第二种是 BeginReceive/EndReceive 回调模型异步回调里处理数据但要注意回调线程和 UI 线程的切换第三种是 SocketAsyncEventArgs性能和复用性最好但缓冲区复用逻辑复杂测试工具里没有必要为了性能去卷这一层。我推荐测试工具用“同步接收 独立工作线程”的组合。接收线程里循环 Receive数据到了丢给 UI 线程刷新发送直接用 Socket.Send不发大文件的话完全不会卡。同步模型最大的优点是出错路径简单要么收到要么抛异常你可以在异常里统一做断线处理排错时看一眼堆栈就知道问题在哪。如果你坚持用异步回调记得在 UI 关闭时要做好线程退出逻辑。BeginReceive 回调在 Socket 关闭后仍然可能触发回调里再去访问已释放的控件就是经典的 ObjectDisposedException。这套逻辑不是不能写而是对测试工具来说收益不匹配投入产出比太低。2.3 最小可跑通的 TCP Echo 服务端从监听循环到收发流程写一个测试工具前先要有一个基准通信代码。最小可用的 TCP 服务端核心如下TcpListener listener new TcpListener(IPAddress.Any, 9000); listener.Start(); while (true) { TcpClient client await listener.AcceptTcpClientAsync(); _ HandleClientAsync(client); // 每个连接一个独立任务 } async Task HandleClientAsync(TcpClient client) { using (client) using (NetworkStream stream client.GetStream()) { byte[] buffer new byte[4096]; int read; while ((read await stream.ReadAsync(buffer, 0, buffer.Length)) 0) { // Echo 回显把收到的原样写回 await stream.WriteAsync(buffer, 0, read); } } }这段代码里 TcpListener 监听所有网卡的 9000 端口AcceptTcpClientAsync 接受客户端连接。每个客户端连接被交给独立的 HandleClientAsync 任务多个连接不会互相阻塞。NetworkStream 的 ReadAsync 会一直读到对方关闭连接或断开时才返回 0所以工具里断线检测的顺序就是ReadAsync 返回 0 → 对方正常关闭ReadAsync 抛异常 → 网络异常。连接数是测试工具的一个大参数。单个工具里同时开 50 个 TCP 连接做压力验证并不罕见此时 listen backlog 要同步调。backlog 是 TcpListener.Start() 的第二个参数默认值在某些系统上不够用做并发测试时要显式传一个较大的值比如 100 或者 200否则客户端连接会被内核拒绝。2.4 UDP 收发不能漏掉 Bind无连接模型的最小代码UDP 的代码比 TCP 短但有一个动作极其容易漏接收前必须 Bind 一个本地端口。很多人写 UDP 测试工具时只写 SendTo不写 Bind结果发出去的包对端能收到对端的回复却永远收不到。Socket udpSocket new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // 绑定本地端口否则收不到任何回复 udpSocket.Bind(new IPEndPoint(IPAddress.Any, 0)); EndPoint remoteEndPoint new IPEndPoint(IPAddress.Any, 0); byte[] buffer new byte[65535]; int length udpSocket.ReceiveFrom(buffer, ref remoteEndPoint); Console.WriteLine($收到 {remoteEndPoint} 的数据{BitConverter.ToString(buffer, 0, length)});Bind 的第二个参数传 0 表示让系统自动分配本地端口适合只收不发短报文如果你要和服务端固定端口通信就把本地端口显式指定。ReceiveFrom 里的 remoteEndPoint 必须提前初始化它会被填充为对端地址很多新手直接传 null 或者传一个未初始化的 EndPoint运行时直接抛 ArgumentNullException。这里的 buffer 长度也有讲究。UDP 单包最大 65507 字节测试工具处理普通协议帧时用 65535 一次收全包是最省心的。不要像 TCP 那样用 4096 的小缓冲UDP 如果缓冲区小于实际到达的报文超出的部分会被内核直接截断丢弃这种问题连异常都不会抛排查时最折磨人。3. 把工具做成能干活的样子收发、编码、定时任务3.1 十六进制收发显示层必须和字节层分离Socket 收发本质上全是字节但联调时你看到的往往是十六进制字符串。测试工具里一定要把“字节层”和“显示层”拆开底层收发用 byte[]界面显示用 Hex 字符串。不拆的后果是遇到 0x00 这种不可见字符文本模式下直接消失整个帧的长度都对不上。public static string BytesToHex(byte[] data, int offset, int count) { StringBuilder sb new StringBuilder(count * 3); for (int i offset; i offset count; i) { if (i offset) sb.Append( ); sb.Append(data[i].ToString(X2)); } return sb.ToString(); } public static byte[] HexToBytes(string hex) { hex hex.Replace( , ).Replace(\r, ).Replace(\n, ); if (hex.Length % 2 ! 0) throw new ArgumentException(十六进制字符串长度必须是偶数); byte[] result new byte[hex.Length / 2]; for (int i 0; i result.Length; i) result[i] Convert.ToByte(hex.Substring(i * 2, 2), 16); return result; }BytesToHex 里每个字节转两位大写十六进制中间加空格便于肉眼分组阅读。HexToBytes 则做逆向解析先把空格换行全部干掉再校验长度必须是偶数最后两个字符一组转成字节。Convert.ToByte 的第二个参数 16 表示按十六进制解析如果输入里混入非法字符比如 “GG”这里会直接抛 FormatException。这两个方法的边界条件值得注意显示时 count 参数控制显示长度TCP 接收缓冲区可能有残留数据不要直接把整个 buffer 转成 Hex要用实际接收长度 read 来做截断。解析时要去掉所有空白字符再判断长度因为用户粘贴的报文可能带有换行或者制表符。这两个“小坑”在工具里的出现频率比协议本身的问题还要高。3.2 编码切换ASCII、UTF8 还是 GBK十六进制解决了二进制报文的问题但文本协议还是得用字符串收发。测试工具里最常见的翻车点在于设备端用 GBK 编码工具默认用 UTF8结果中文全部变成乱码甚至因为字节长度不同导致协议帧长度错位。我一般会给工具配上三种编码选项ASCII、UTF8、GBK外加一个“按字节处理”的选项。实现很简单发送时用 Encoding.GetEncoding(gb2312).GetBytes(text)接收时用对应编码把 byte[] 转成字符串。关键点是编码切换必须同时影响收发两条链路不能只改一边。还有一点进阶习惯报文里同时含文本和二进制字段时不要试图用字符串表示整个报文。正确做法是十六进制模式收需要看可读字符时再单独用编码解析选中的字节区域。这样才能保证二进制字段不会被编码转换破坏。3.3 定时发送与自动重连测试工具里的两个实用功能联调时经常要验证“设备长时间不发数据服务端会不会掉线”或者持续发心跳看服务端是否正常响应。这时定时发送就派上用场了。实现用 System.Timers.Timer 最顺手但有一个细节Timer 的 Elapsed 事件在后台线程触发事件里直接调用 UI 控件会跨线程抛异常要先通过 Invoke 回到 UI 线程。Timer timer new Timer(); timer.Interval 1000; // 发送间隔单位毫秒 timer.AutoReset true; // 定时重复执行 timer.Elapsed (s, e) { // 后台线程不能直接操作 UI 控件 uiContext.Post(_ { if (socket.Connected) socket.Send(payload); else TryReconnect(); }, null); }; timer.Start();重连的退避策略值得认真设计。TCP 断开后立刻重连失败概率很高因为服务端可能还在处理旧连接或者端口处于 TIME_WAIT 状态。我一般用递增间隔第一次等 1 秒第二次等 2 秒最多等到 15 秒封顶连续重连成功后再把间隔重置回 1 秒。这个策略用在测试工具里足以模拟大多数真实客户端的重连行为。定时发送还有一个隐藏需求发送内容往往是动态的比如每帧序号递增。工具里要把“发送内容模板”和“动态部分”分离定时器每触发一次先替换模板里的序号占位符再 Send。否则你只能发固定内容验证不了服务端对递增序号的逻辑。3.4 收发日志测试工具里最被低估的排错依据收到一条错误响应时第一反应往往是去翻协议文档但我建议先翻测试工具的收发日志。日志要记录四条信息时间戳、方向发送还是接收、长度、内容。只记录内容是远远不够的收发的时间间隔很多时候就是问题的根源比如服务端处理慢导致客户端超时重发。日志内容不要只存 Hex 字符串同时把 ASCII 可读版本也记录下来方便肉眼快速判断这段内容是命令还是纯二进制。文件日志要按天分文件避免单个文件无限增长。日志刷盘的频率也要控制每收一帧就写一次磁盘会让工具卡到没法用常见做法是收集一定数量的日志后批量写入。4. Socket 测试工具最容易踩的 5 个坑与排查方法4.1 服务端只 Accept 一次后续客户端连不上现象工具作为服务端启动时第一个客户端连接正常收发关掉重连后客户端一直卡在 Connect服务端也没有任何新连接进来的迹象。原因监听循环写法错误。很多人写完 Accept 后处理完第一个连接就退出循环或者循环写在某个分支里导致后续连接不再被 Accept。TcpListener 的 Accept 是一次性的必须放在 while 循环里持续调用。解决把 AcceptTcpClientAsync 放在 while(true) 循环中每个连接交给独立任务处理。同时在 Accept 逻辑外层加 try-catch捕获 SocketException 并判断异常码特别是 10004句柄被中断和 995操作被取消这两类异常在关闭监听时必然出现要作为正常退出分支处理而不是弹错误框。4.2 UDP 只发不收对端回复石沉大海现象UDP 测试发送功能正常对端设备显示收到了数据但工具的接收区永远空白。原因创建 UDP Socket 后没有调用 BindSocket 处于未绑定端口状态。SendTo 会自动分配一个临时端口但接收回复时内核无法把数据路由到这个临时的、你可能根本不关心的端口上。解决初始化 UDP Socket 后立刻绑定本地端口。固定端口用于生产联调自动分配端口用于快速测试。排查时先看 UDP 监听端口是否被占用Windows 下用 netstat -ano 看 UDP 监听是否正常占用时工具会抛 SocketException 10048要么换端口要么在 Bind 前设置 ExclusiveAddressUse 为 false。4.3 TCP 关闭连接时的半关闭状态导致的第二次发送失败现象第一次收发正常然后工具主动断开连接。再次建立新连接后发送数据没有任何错误抛出但服务端收不到。或者第二次发送直接报 SocketException 10053。原因关闭 Socket 时没有正确处理半关闭状态。直接调用 Socket.Close底层可能还有未发送完的缓冲数据或者服务端仍在发送而客户端关闭了接收通道。新连接是好的但旧连接的数据残留和服务端的半关闭状态纠缠在一起发送看起来成功实际被内核丢弃。解决关闭前执行 Socket.Shutdown(SocketShutdown.Both)然后循环 Receive 直到返回 0表示双方都完成了发送关闭最后再 Close。测试工具里这个“优雅关闭”流程要封装成一个单独的方法不要直接在各处调 Close。另外确认发送用的是同一个连接实例有些工具重连后旧的 Socket 引用没清发送发到了已关闭的句柄上。4.4 十六进制解析时的空格和大小写陷阱现象用户从别的工具里复制了一段报文粘贴到工具的发送框点发送后提示格式错误或者发出去的字节和服务端期望的不一致。原因复制的报文可能带行号、空格、制表符甚至每行末尾有不可见字符。HexToBytes 解析时没做全格式清理只去掉了部分空格一遇到换行或者行首的地址偏移就整个解析失败。解决解析前按顺序清理所有空白字符包括空格、\t、\r、\n。大小写统一交给 Convert.ToByte 处理它本身支持大小写。判断长度时先清理再判断避免因为残留空格导致“长度为奇数”的假错误。清理后的字符串如果包含非十六进制字符要给出明确提示并定位到具体字符位置而不是抛一个看不懂的异常。4.5 异步接收缓冲区复用导致的报文错乱现象用 SocketAsyncEventArgs 写接收时高频通信一段时间后偶尔出现一帧数据里混入了上一帧的尾部字节或者数据明明对不同 session 发的却串到了同一个接收回调里。原因SocketAsyncEventArgs 的缓冲区在一次接收操作未完成时被再次传入新的接收操作。典型场景是接收回调里启动了下一个 ReceiveAsync但上一个操作的 Buffer 还没有被消费完同一块 byte[] 被同时写入和读取数据互相覆盖。解决测试工具里最简单可靠的做法是每个接收操作都传入一个独立缓冲区或者接收回调里先把数据完整 Copy 到业务队列再启动下一次接收。不要直接在回调里引用 Buffer 异步处理。Services 端做网关、多客户端转发这类场景尤其要注意数据必须先进入队列按会话 ID 隔离再进入协议解析否则并发一上来必翻车。5. 测试工具的源码怎么组织才不越改越乱5.1 核心层、协议层、UI 层三层就够测试工具虽小但如果所有代码堆在窗体事件里改到后期就是一场灾难。我习惯拆三层Socket 核心层负责连接、收发、断开协议层负责 Hex 解析、编码转换、帧格式处理UI 层只做显示和用户操作。核心层不引用任何 UI 控件这样才能脱离界面独立测试。接口设计上核心层向外暴露事件而不是方法返回值。接收数据到达时触发 DataReceived 事件连接状态变化时触发 ConnectionStateChanged 事件。UI 层订阅事件并更新界面。这个设计让工具可以随时换界面甚至把同一个核心接到命令行版本上做自动化都不用改通信代码。5.2 一个可复用的 ClientSocket 封装把状态机藏起来TCP 客户端的连接、发送、断开有状态变化封装的核心是把这些状态管理收敛在一个类里public class ClientSocket : IDisposable { Socket socket; public event Actionbyte[] DataReceived; public event Actionstring StateChanged; public bool Connected socket ! null socket.Connected; public void Connect(string ip, int port) { socket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect(ip, port); StateChanged?.Invoke(已连接); StartReceive(); } void StartReceive() { byte[] buffer new byte[8192]; socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, asyncResult { try { int length socket.EndReceive(asyncResult); if (length 0) { byte[] data new byte[length]; Array.Copy(buffer, 0, data, 0, length); DataReceived?.Invoke(data); StartReceive(); } } catch (SocketException ex) { StateChanged?.Invoke($连接异常{ex.SocketErrorCode}); } }, null); } public void Send(byte[] data) { socket.Send(data); } public void Dispose() { try { if (socket ! null) { socket.Shutdown(SocketShutdown.Both); socket.Close(); } } catch { /* 关闭期间的异常忽略 */ } } }这个封装里 BeginReceive 的异步回调链要注意每次 EndReceive 结束必须重新调用 StartReceive形成连续接收。接收到的数据立刻拷贝到独立数组再触发事件避免下次 BeginReceive 覆盖同一块 buffer。Connect 方法是同步的测试工具里可以接受这个卡顿如果你要做大批量自动连接再把 Connect 改成 ConnectAsync。Dispose 里的 try-catch 不是偷懒是必须的。测试工具可能在任意状态被用户强关关闭期间线程可能正阻塞在收发上此时 Shutdown 和 Close 抛出的异常都是预期内的直接忽略才能保证工具干净退出。5.3 跨线程更新 UIControl.BeginInvoke 的用法和边界接收事件在后台线程触发UI 更新必须回到主线程。WinForms 里用 Control.BeginInvokeWPF 里用 Dispatcher。不要用 Invoke它在 UI 线程繁忙时会造成死锁。BeginInvoke 是异步的把更新操作排队到 UI 线程不会阻塞通信线程。void OnDataReceived(byte[] data) { string hex BytesToHex(data, 0, data.Length); if (outputBox.IsHandleCreated) { outputBox.BeginInvoke(new Action(() { outputBox.AppendText(${DateTime.Now:HH:mm:ss.fff} 收 ({data.Length}字节): {hex}\r\n); })); } }这里必须用 IsHandleCreated 做保护因为窗口关闭过程中 handle 可能已销毁BeginInvoke 会抛异常。另外高频收发时BeginInvoke 的消息堆积会导致 UI 处理不过来、界面越来越卡。优化方式是积攒一批数据统一刷新比如每 100 毫秒刷新一次接收区而不是每收到一帧就刷一次。连续的 Hex 显示本身就有可读性需求分组刷新反而更适合肉眼阅读。5.4 配置化把 IP、端口、编码、报文模板写进配置文件测试工具每次换环境都要重新输入 IP 和端口如果这些写死在代码里换一批设备就要重新编译。配置文件是测试工具的标配。WinForms 下用 App.config 或者一个简单的 JSON 文件都可以核心是把下面这几类配置和代码分离目标 IP 和端口、本地监听端口、编码类型、默认发送报文、定时发送间隔。configuration appSettings add keyTargetIP value192.168.1.100 / add keyTargetPort value8080 / add keyLocalPort value0 / add keyEncodingName valuegb2312 / add keyAutoSendInterval value1000 / /appSettings /configuration配置文件的价值不只是省事它让工具的适用范围边界清晰同一份编译产物给不同项目联调时只需改配置。对于经常要反复切换多套参数的用户可以在工具界面做“场景保存”功能把一组 IP、端口、编码、报文组合导出一个场景文件切场景一键加载。这一步做完工具基本就到了可以交付给测试组日常使用的程度。6. 把测试工具升级成协议模拟器自动化回归的进阶用法测试工具做到能手动收发已经满足联调需求了但重复性的验证工作不能永远靠手点。下一步是把工具里的收发序列变成可重复执行的脚本。我一般用最简单的做法定义一个用例为“连接 → 发送指定报文 → 等待响应 → 断言响应内容 → 断开”用脚本驱动核心层跑一遍结果自动判定通过或者失败。// 伪代码示例实际可用 C# 脚本引擎或 Lua 实现 testCase(登录-查询余额-退出) { connect(192.168.1.100, 8080); send(hex(00 01 10 00 00 00 00)); let response receive(timeout: 3000); assertEqual(response, hex(00 01 10 10 00 00 00 00)); disconnect(); }这个升级带来的收益是质变的收到新固件时把历史用例脚本全部跑一遍哪条协议不兼容立刻就能定位。不用再等现场反馈。而且脚本驱动和手动模式共用同一个核心层升级不影响既有功能。做协议模拟器时有一个习惯值得养成每次新用例跑通顺手把它固化成一条自动化用例。测试工具的价值恰恰是从“手动发现”走向“自动回归”这个环节全部打通之后才完全体现出来的。做了一年多回头看的经验是宁可前期多写一个断言也不要让任何一条验证逻辑成为不可复现的黑匣子。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑