C# UDP通讯实战:从UdpClient收发到Wireshark抓包排错
2026/9/16 17:40:34 网站建设 项目流程

简介:这套C# UDP通信示例资源面向需要快速掌握UDP协议与网口通讯测试的.NET开发者,提供客户端与服务端双工程源码,涵盖UdpClient发送、接收、IPEndPoint端点配置及局域网内数据收发等关键操作,适合初学者对照学习或在项目中直接复用。压缩包共63个文件,体积仅117KB,主要包含18个C#源码文件、6个可直接运行的exe程序、配套config配置文件、resources/resx资源文件以及sln工程文件,结构清晰,便于直接打开调试与验证。已有314人学习下载。通过这份资源,读者不仅能看到完整的UDP通信代码实现,还能结合示例了解网口通讯中的常见细节,如端口设置、IP地址指定、接收阻塞处理等,并据此完成基础的通讯联通性测试,为后续深入网络编程打下基础。

1. 两个工程一套协议:C#UDP.zip 到底给了什么

拿到C#UDP.zip的第一眼,多数人会以为是个单文件示例,解压后看到的却是C_UDP_ClientC_UDP_Server两个独立工程,各自带.sln.v12.suo,编译目标锁定在 Visual Studio 2013 那套工具链上。这不是随手的 demo,而是一份最小可运行的「网口通讯」对照实验:一端发、一端收,端口和 IP 全部硬编码,把 UDP 无连接、不可靠、头部只有 8 字节这几个特性直接摆在你面前。适合正在做 C# 上位机、需要和设备走 UDP 报文的新手,也适合想快速验证「对端到底收没收到」的老手。它最大的价值不在代码量,而在于给你一个可以马上改 IP、改端口、改报文的骨架,省掉从System.Net.Sockets名字空间摸索起手的半小时。下面按选型、实现、抓包验证、坑位排错的顺序拆。

2. UdpClient 与 Socket 的选型,以及双工程结构拆解

2.1 为什么这个包用 UdpClient 而不是裸 Socket

C# 里做 UDP 有两条路:Socket类和UdpClient封装类。这两者在功能上等价,UdpClient内部持有的是一个Socket实例,只不过把ReceiveFrom/SendTo的端点参数、缓冲区分配、关闭逻辑包了一层。

对比项UdpClientSocket(SocketType.Dgram)
构造复杂度一行指定端口需指定 AddressFamily、SocketType、ProtocolType
收包写法Receive(ref IPEndPoint)自动带回发送方ReceiveFrom(buffer, ref EndPoint)需自行转型
异步支持ReceiveAsync/SendAsyncReceiveFromAsync/SendToAsync
广播开关EnableBroadcast = trueSetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true)
适用场景业务报文收发、快速原型需要精细控制 TTL、复用地址、原始套接字

这份资源选了UdpClient,取舍很清楚:网口通讯场景下报文通常是定长结构体或短字符串,不需要改动内核级套接字选项,用封装类能让C_UDP_ClientC_UDP_Server的代码都压在几十行以内。如果你后面要做组播、要设SO_REUSEADDR让多个进程抢同一个端口,那就得退回裸Socket

2.2 两个工程的文件构成与启动顺序

解压后目录大致是这样:

C_UDP_Client/ ├── C_UDP_Client.sln ├── C_UDP_Client.v12.suo └── C_UDP_Client/ ├── Program.cs ├── Properties/AssemblyInfo.cs └── C_UDP_Client.csproj C_UDP_Server/ ├── C_UDP_Server.sln ├── C_UDP_Server.v12.suo └── C_UDP_Server/ ├── Program.cs └── Properties/AssemblyInfo.cs

.v12.suo是 VS2013 的用户选项文件,二进制格式,记录断点、窗口布局,和编译无关。真正要关心的是两个Program.cs。启动顺序必须是先 Server 后 Client:服务端UdpClient(port)绑定端口后进入阻塞式Receive,客户端才发得出去。反过来先跑客户端,第一次Send不会报错——UDP 不握手,内核直接把包丢出去,没人监听就静默丢弃,你会以为发成功了,其实对端根本没起。

2.3 编译环境从 VS2013 迁到新版的处理

.v12.suo对应的工程格式是 VS2013,用 VS2019 / VS2022 打开会提示升级工具集。常见做法是右键解决方案 → 重定解决方案目标,或者在.csproj里把ToolsVersion<TargetFrameworkVersion>换成v4.7.2/v4.8。如果只想命令行验证,msbuild C_UDP_Server.sln /p:Configuration=Release也能编,但前提是本机装了对应版本的 MSBuild。不想折腾工程文件的话,直接把两个Program.cs复制到新建的控制台项目里,依赖只有System.NetSystem.Net.Sockets,零第三方包。

3. 收发包的代码实现:从绑定端口到数据解析

3.1 服务端:绑定端口与阻塞接收循环

服务端的核心就两步,先构造UdpClient绑定本地端口,再在一个while里反复ReceiveReceive是同步阻塞调用,没包进来就卡在那一行不动,这对单线程示例够用。

using System; using System.Net; using System.Net.Sockets; using System.Text; class Program { static void Main(string[] args) { // 绑定本地 8000 端口,监听所有网卡 UdpClient server = new UdpClient(8000); // 不限定位对端,接收任意来源的数据报 IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); Console.WriteLine("Server listening on 8000 ..."); while (true) { // 阻塞等待,直到有数据报到达 byte[] buffer = server.Receive(ref remoteEP); string msg = Encoding.UTF8.GetString(buffer); Console.WriteLine("From {0}:{1} -> {2}", remoteEP.Address, remoteEP.Port, msg); // 原路回一个应答,方便客户端确认链路通 byte[] ack = Encoding.UTF8.GetBytes("ACK " + msg); server.Send(ack, ack.Length, remoteEP); } } }

new UdpClient(8000)这个构造重载等价于Bind0.0.0.0:8000,意味着监听本机所有网卡,局域网内别的设备也能发进来。remoteEP初值给IPAddress.Any, 0只是占位,Receive(ref remoteEP)执行后会被填成真实发送方的地址和端口。回发的Send用的是同一个UdpClient实例,这样源端口保持 8000,客户端看到的应答来源端口一致,不容易被防火墙当成无关流量拦掉。

3.2 客户端:构造目标端点与发送

客户端第一次Send时,如果之前没调用过Connect,系统会自动分配一个临时源端口。目标端点里 IP 和端口缺一不可。

using System; using System.Net; using System.Net.Sockets; using System.Text; class Program { static void Main(string[] args) { UdpClient client = new UdpClient(); // 不绑定固定端口,由系统分配 client.Client.ReceiveTimeout = 3000; // 3 秒应答超时 IPEndPoint target = new IPEndPoint( IPAddress.Parse("192.168.1.100"), 8000); // 服务端地址 for (int i = 0; i < 5; i++) { string text = "HELLO-" + i; byte[] data = Encoding.UTF8.GetBytes(text); client.Send(data, data.Length, target); // 无连接,直接甩出去 Console.WriteLine("Sent: " + text); } try { IPEndPoint from = new IPEndPoint(IPAddress.Any, 0); byte[] resp = client.Receive(ref from); Console.WriteLine("Reply: " + Encoding.UTF8.GetString(resp)); } catch (SocketException ex) { Console.WriteLine("No reply within timeout: " + ex.SocketErrorCode); } finally { client.Close(); } } }

这里有几个参数值得说明。ReceiveTimeout通过client.Client访问底层Socket设置,单位毫秒,不设的话Receive会无限期阻塞,调试时很容易让人以为程序死了。循环里连发 5 个包,UDP 不保证顺序也不保证全到,收不到第 2 个包是正常现象,不要写依赖序号的业务逻辑。客户端不主动Bind,源端口每次运行都可能变,如果服务端有基于源端口的会话表,就必须显式new UdpClient(localPort)固定住。

3.3 报文边界:为什么 UDP 不会粘包但要按包处理

TCP 有粘包问题,UDP 没有——Receive一次返回一个完整数据报,Datagram 边界由内核保留。但反过来,如果你用大于 1472 字节(以太网 MTU 1500 减去 IP 头 20 和 UDP 头 8)的缓冲发数据,IP 层会分片,任一碎片丢失整个报文就废了。报文设计上,常见做法是首部 4 字节放魔数、2 字节放长度、2 字节放序号,后面才是负载。

字段长度说明
Magic4 字节固定值如 0x55AA55AA,用于过滤杂包
Length2 字节负载长度,网络字节序
Seq2 字节递增序号,用于检测丢包
Payload变长业务数据,建议不超过 1400 字节

解析时用BitConverter.ToUInt16前记得处理大小端,Windows 是小端,如果对端是嵌入式设备发的大端序,要用IPAddress.HostToNetworkOrder转一遍,否则长度字段会读成天文数字。

4. 网口通讯测试:抓包、打流与常见失败排查

4.1 用 Wireshark 过滤 UDP 并算包间隔

代码跑起来只是第一步,真正确认「网口通讯」是否正常,得看线上流量。Wireshark 里常用两个过滤表达式:

udp.port == 8000 udp.srcport == 8000 && ip.dst == 192.168.1.50

第一条只看 8000 端口的收发,第二条限定方向,避免双向包混在一起。想看前两包的时间差,在过滤栏敲udp,然后菜单 Statistics → Conversations,切到 UDP 标签页,能看到每个会话的包数和时间跨度;要单看某两包间隔,选中第一包,在Frame里记下Time since previous frame,或者用tcp.time_delta的类似思路加一列自定义时间差。发包间隔稳定在设定的 100ms 附近,说明应用层节拍正常;如果间隔抖动到几百毫秒,问题多半在发送端的Thread.Sleep精度或者 UI 线程阻塞,这跟 C# 循环数据采集和 UI 刷新卡顿是同一类根因——主线程被Receive占住,界面就卡死。

4.2 用 iperf3 做 UDP 打流摸底

代码还没写完、想先确认两台机器的网络通路和最大吞吐,用 iperf3 最快。服务端先起:

iperf3 -s -u -p 5201

客户端按固定码率打 UDP 流:

iperf3 -c 192.168.1.100 -u -p 5201 -b 50M -t 30 -l 1400

-u指定 UDP,-b 50M是目标带宽 50 Mbps,-t 30跑 30 秒,-l 1400指定每个数据报 1400 字节,贴近实际业务报文大小。结果里关注 Jitter 和 Lost/Total 两列,抖动大说明链路或对端处理有瓶颈,丢包率高说明带宽定高了。这个数字可以当作你 C# 程序的性能上限参考,代码发送速率超过它,丢包就是网络的问题,不是代码的问题。

4.3 收不到包的六种常见原因

按排查顺序列一下我一般会看的点。

  1. 服务端没起或崩了。先确认进程在,端口在netstat -ano | findstr 8000里有UDP行。
  2. 防火墙拦了入站 UDP。Windows 默认对未监听过的 UDP 端口会弹提示,点了「取消」就静默丢弃,去高级安全防火墙给程序或端口加放行规则。
  3. IP 写错或不在同一网段。192.168.1.x192.168.0.x之间没路由就是不通,先ping通再说 UDP。
  4. 端口被占用。UdpClient构造时如果端口被独占且没设ReuseAddress,会抛SocketException,错误码 10048。
  5. 发到了广播地址但没开广播。255.255.255.255发送前必须client.EnableBroadcast = true,否则抛 10013 权限错误。
  6. 网卡多张,绑到了错的接口。笔记本同时有有线和无线时,IPAddress.Any是通的,但指定IPAddress.Parse("192.168.1.100")就只在那一张卡上收。

提示:排查时先把发送端和接收端放在同一台机器上用127.0.0.1跑通,再换成真实 IP,能排掉一半环境问题。

5. 进阶技巧:把同步阻塞改成异步并保住 UI 响应

示例代码用的是同步Receive,放进 WinForm 或 WPF 这类 C# 上位机里,主线程一卡,界面就是「未响应」。改法是换成ReceiveAsync,配合CancellationToken做优雅退出:

private UdpClient _udp; private CancellationTokenSource _cts; private async Task ReceiveLoopAsync() { _udp = new UdpClient(8000); while (!_cts.IsCancellationRequested) { try { // 异步等待,不占用 UI 线程 UdpReceiveResult result = await _udp.ReceiveAsync(); string msg = Encoding.UTF8.GetString(result.Buffer); // 回到 UI 线程更新控件 this.Invoke(new Action(() => { listBoxLog.Items.Add($"{result.RemoteEndPoint}: {msg}"); })); } catch (ObjectDisposedException) { break; // 关闭时正常抛出,直接退出循环 } catch (SocketException ex) { // 记录错误码,继续循环,避免偶发 ICMP 端口不可达把循环打断 Console.WriteLine(ex.SocketErrorCode); } } } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { _cts.Cancel(); _udp?.Close(); // Close 会让 await ReceiveAsync 抛 ObjectDisposedException }

关键在于CloseReceiveAsync的配合:异步等待没法被CancellationToken直接打断,最可靠的做法就是关掉UdpClient,让挂起的ReceiveAsyncObjectDisposedException,在catch里跳出循环。另外,Windows 上如果对端不在,UDP 发包可能收到 ICMP 端口不可达,反映到下一次ReceiveAsync上就是SocketException(错误码 10054),这种异常应该吞掉继续循环,而不是让整个接收线程退出。UI 更新必须Invoke回主线程,直接在后台线程改控件照样会抛跨线程异常。这套结构套进任何 C# 上位机采集程序都通用,接收、解析、刷新三段分开,卡顿自然就少了。

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

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

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

立即咨询