在局域网通信开发中,UDP广播是一个非常实用的功能,常见于设备发现、服务宣告、心跳检测等场景。比如你写一个上位机程序,需要在启动时自动找到网段内的下位机设备,通常的做法就是向广播地址发一个探测报文,设备收到后回送自己的IP和端口。C#标准库里的UdpClient类把底层Socket操作封装得比较到位,用它实现广播收发比直接操作Socket省事不少。本文从广播地址的原理讲起,逐步给出完整的发送端和接收端实现,并说明几个实际部署中容易出问题的细节。

一、先弄明白广播地址是怎么回事
UDP广播本质上是向一个特殊的IP地址发送数据包,这个地址被称为广播地址。最简单粗暴的是受限广播地址255.255.255.255,发到这个地址上的报文会被当前局域网内的所有主机接收,路由器不会把它转发到外网。另一种是定向广播地址,需要根据子网计算得出,比如你的机器IP是192.168.1.100,子网掩码是255.255.255.0,那么定向广播地址就是192.168.1.255。
两者的区别值得注意。255.255.255.255在小网络里用起来最方便,不需要知道本机IP,但它有个限制:部分操作系统或网络设备在多网卡环境下表现不一致,报文可能只从默认路由对应的网卡发出。定向广播则可以明确指定从哪个网段发出去,在多网卡的服务器上更可控,不过使用定向广播时,接收方所在主机的防火墙或路由配置可能需要放行。开发阶段建议先用255.255.255.255跑通流程,遇到多网卡问题再切换到定向广播。
另外一个前提知识是:发送UDP广播前,必须开启Socket的广播权限,对应SocketOptionName.Socket的Broadcast选项。UdpClient提供了一个便捷的EnableBroadcast属性,设置为true即可,否则发送时会抛出SocketException,提示“在其上下文中,该请求的地址无效”。这是新手最常遇到的第一个报错。
二、发送端实现:发送广播报文
发送端的逻辑很直接:创建UdpClient,开启广播权限,把报文转成字节数组,调用Send方法发往广播地址的指定端口即可。下面是完整代码:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class UdpBroadcastSender
{
static void Main()
{
// 创建UdpClient并随机绑定一个本地端口
using (UdpClient sender = new UdpClient())
{
// 必须开启广播权限,否则发送会失败
sender.EnableBroadcast = true;
// 广播目标地址和端口
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Broadcast, 9000);
// 准备要发送的数据
string message = "DEVICE_DISCOVERY_REQUEST";
byte[] data = Encoding.UTF8.GetBytes(message);
try
{
int sent = sender.Send(data, data.Length, remoteEP);
Console.WriteLine($"已发送 {sent} 字节到 {remoteEP}");
}
catch (SocketException ex)
{
Console.WriteLine("发送失败: " + ex.Message);
}
}
}
}
代码中IPAddress.Parse("255.255.255.255")明确指定了受限广播地址。如果想用定向广播,改成实际的网段广播地址,比如IPAddress.Parse("192.168.1.255")。端口9000是接收方监听的端口,发送方自身使用的本地端口是系统随机分配的,这一点和TCP不同,UDP不需要事先建立连接。
实际项目中更常见的做法是“一发一收”:发送探测报文后,在同一个UdpClient上等待设备的应答。因为应答会回到发送时所用的本地端口,所以复用同一个UdpClient实例即可收到回包。为了避免程序卡死,建议给UdpClient设置接收超时:
sender.Client.ReceiveTimeout = 3000; // 3秒无应答则抛出异常
try
{
IPEndPoint responder = new IPEndPoint(IPAddress.Any, 0);
byte[] reply = sender.Receive(ref responder);
Console.WriteLine($"收到 {responder} 的应答: {Encoding.UTF8.GetString(reply)}");
}
catch (SocketException)
{
Console.WriteLine("等待应答超时");
}
这里的ref参数是UdpClient.Receive方法的设计特点,调用后该变量会被填充为实际发送方的地址信息,通过它能拿到应答设备的IP和端口,正好满足设备发现场景的需求。
三、接收端实现:监听并响应广播
接收端需要绑定到广播报文的目标端口,也就是发送端使用的9000端口。关键点在于,广播报文会同时送达局域网内所有监听该端口的主机,如果同一台机器上要跑多个接收实例做测试,就必须开启端口共享:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class UdpBroadcastReceiver
{
static void Main()
{
UdpClient receiver = new UdpClient();
// 允许多个进程绑定同一端口,接收广播必须设置
receiver.Client.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress, true);
receiver.Client.Bind(new IPEndPoint(IPAddress.Any, 9000));
Console.WriteLine("开始监听 UDP 9000 端口的广播...");
while (true)
{
IPEndPoint remote = new IPEndPoint(IPAddress.Any, 0);
byte[] data = receiver.Receive(ref remote);
string msg = Encoding.UTF8.GetString(data);
Console.WriteLine($"来自 {remote}: {msg}");
// 回送应答,告知对方本机信息
byte[] ack = Encoding.UTF8.GetBytes("DEVICE_ACK:" + Dns.GetHostName());
receiver.Send(ack, ack.Length, remote);
}
}
}
注意代码里绑定的地址是IPAddress.Any,表示接收发往本机任意网卡的9000端口数据,这是最稳妥的写法。如果不设置ReuseAddress,第二个绑定同一端口的进程会直接抛出“通常每个套接字地址只允许使用一次”的异常。另外应答时直接用接收用的UdpClient回发即可,目标地址就是remote变量里记录的发送方地址。
上面的写法是同步阻塞式接收,Receive方法会一直挂起线程直到有数据到达。对于控制台小程序没问题,但如果放在WinForm或WPF界面程序里,直接在UI线程调用会卡死界面,这时应该放到后台线程,或者改用异步方式。
四、异步接收与注意事项
.NET Framework 4.5以上或.NET Core项目推荐使用ReceiveAsync异步接收,配合async/await写起来非常清爽:
static async Task ReceiveLoopAsync(UdpClient receiver, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
UdpReceiveResult result = await receiver.ReceiveAsync();
string msg = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"来自 {result.RemoteEndPoint}: {msg}");
}
catch (ObjectDisposedException)
{
break; // UdpClient被释放,退出循环
}
catch (SocketException)
{
// 忽略个别异常继续监听
}
}
}
异步方式的好处是不占用线程等待,UI程序里调用也不会卡界面。退出程序时调用receiver.Close(),挂起的ReceiveAsync会抛出ObjectDisposedException,捕获后就能干净地结束循环。
最后提醒几个实际部署中容易忽视的问题。第一,Windows防火墙默认会拦截入站UDP,调试时若收不到报文,先检查防火墙对程序或端口的放行情况,这是排错时最该先确认的一环。第二,UDP不保证送达,设备发现的探测报文建议连续发两三次提高命中率,接收端要做去重处理。第三,报文长度控制在1400字节以内比较安全,超过网络MTU会触发分片,丢包概率明显上升。第四,生产环境别忘了加try-catch包裹网络操作,拔网线、切网络这类异常在SocketException里都会体现,程序要有重试或提示机制。