使用套接字或 UdpClient 通过 LAN 广播 UDP 数据报时无法收到已知回复

Unable to receive known reply when broadcasting UDP datagrams over LAN using Sockets or UdpClient

我搜索了 2 天,发现很多很多 questions/answers 似乎是同一个问题,但有一些不同,但是 none 似乎确实提供了解决方案。

我正在实现一个无需 OEM 控制器即可直接控制 DMX 系统(ColorKinetics 设备)的库。这涉及通过驱动照明设备的路由器与连接到我的家庭 LAN 的 Ethernet-enabled 电源 (PDS) 进行通信。 PDS 在特定端口 (6038) 上运行,并响应通过网络广播的格式正确的数据报。

我可以成功地广播一个简单的 DMX 消息(Header + DMX 数据),它被 PDS 接收并应用于连接的照明设备,所以 one-way 通信不是问题。

我的问题是我现在正在尝试实现设备发现功能以检测 LAN 上的 PDS 和附加灯,但我无法接收(绝对)从PDS。我可以成功传输指示设备回复的数据报,我可以在 WireShark 中看到回复,但我的应用程序未检测到回复。

我还在另一台机器上尝试了 运行 一个简单的侦听器应用程序,它可以检测到初始广播,但也听不到 return 数据报,但是我认为这是行不通的,因为return 数据包被发送到原始发件人 IP 地址。

我最初尝试通过 UdpClient 实施,然后通过 Sockets 实施,无论我指定什么选项和参数,两者都会产生相同的结果。

这是我当前非常简单的功能测试代码,目前使用 Sockets

byte[] datagram = new CkPacket_DiscoverPDSRequestHeader().ToPacket();
Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);    
IPEndPoint ep = new IPEndPoint(IPAddress.Parse("192.168.1.149"), 6039);

public Start()
{    
    // Start listener
    new Thread(() =>
    {
        Receive();
    }).Start();

    sender.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    sender.EnableBroadcast = true;
    // Bind the sender to known local IP and port 6039
    sender.Bind(ep);
}

public void Send()
{            
    // Broadcast the datagram to port 6038
    sender.SendTo(datagram, new IPEndPoint(IPAddress.Broadcast, 6038));
}

public void Receive()
{
    Socket receiver = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    receiver.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    receiver.EnableBroadcast = true;

    // Bind the receiver to known local IP and port 6039 (same as sender)
    IPEndPoint EndPt = new IPEndPoint(IPAddress.Parse("192.168.1.149"),6039);
    receiver.Bind(EndPt);

    // Listen
    while (true)
    {
        byte[] receivedData = new byte[256];

        // Get the data
        int rec = receiver.Receive(receivedData);

        // Write to console the number of bytes received
        Console.WriteLine($"Received {rec} bytes");
    }    
}

发送方和接收方绑定到一个 IPEndPoint,本地 IP 和端口 6039。我这样做是因为我可以看到每次我初始化一个新的 UdpClient,系统会动态分配一个传出端口,PDS 会将数据发送回该端口。这样做,我可以说监听器肯定在应该接收 PDS 响应 (6039) 的端口上监听。我相信,因为我将选项 ReuseAddress 设置为 true,所以这应该不是问题(没有抛出异常)。

Start() 创建一个新线程来包含侦听器,并在发送客户端上初始化选项。

Send()成功广播PDS在6038端口接收到的16字节数据报,并生成回复到6039端口(在WireShark中看到)

Receive() 没有收到数据报。如果我将侦听器绑定到端口 6038,它将接收原始的 16 字节数据报广播。

这里是 WireShark 数据:

Wireshark

我已经研究过使用像 SharpPCap 这样的库,正如许多答案所建议的那样,但是最新版本中似乎存在一些兼容性问题,我不够聪明,无法规避,这妨碍了基本的在我的系统上正常运行的示例。似乎这种基本功能不应该需要那种类型的外部依赖。我还看到许多其他 questions/answers 问题相似,但通过为 SocketUdpClient 设置 this-or-that 参数解决了这个问题,我已经尝试了每个组合无效。

我还启用了通过 windows 防火墙的访问权限,允许端口使用,甚至完全禁用了防火墙,但都没有成功。我不认为问题出在我的路由器上,因为消息正在发送到 Wireshark。

更新 1

根据建议,我相信我将侦听器 Socket 置于混杂模式,如下所示:

    Socket receiver = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
    receiver.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true);
    receiver.EnableBroadcast = true;

    IPEndPoint EndPt = new IPEndPoint(IPAddress.Parse("192.168.1.149"), 0);

    receiver.Bind(EndPt);
    receiver.IOControl(IOControlCode.ReceiveAll, new byte[] { 1, 0, 0, 0 }, null);

这导致侦听器收到各种网络流量,包括出站请求,但仍然没有传入回复。

更新 2

正如 Viet 所建议的,请求数据报中存在某种寻址问题,其格式如下:

    public class CkPacket_DiscoverPDSRequest : BytePacket
    {
        public uint magic = 0x0401dc4a;
        public ushort version = 0x0100;
        public ushort type = 0x0100;
        public uint sequence = 0x00000000;
        public uint command = 0xffffffff;
    }

如果我将 command 字段更改为我的广播地址 192.168.1.149' or192.168.255.255`,我的侦听器将开始检测 return 数据包。诚然,我不知道这个字段应该代表什么,我最初的猜测是只是输入一个广播地址,因为数据报的目的是发现网络上的所有设备。显然不是这样,虽然我仍然不确定它的确切点。

不管怎样,谢谢你的帮助,这是进步。

正如您已经注意到的,发送广播不需要绑定,但它使用随机源端口。

如果您将代码调整为不绑定发件人,您的侦听器应该再次按预期运行:

Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);    
sender.EnableBroadcast = true;
Thread read_thread;

public Start()
{    
    // Start listener
    read_thread = new Thread(Receive);
    read_thread.Start();
}

您遇到的问题是操作系统内核只将数据包传送到一个套接字绑定程序(先到先得)。

如果您想要真正的并行读取访问,您需要查看嗅探示例,例如:.

由于您只想从同一个 ip/port 获取广播源,您只需要先让接收器绑定即可。 如果您在启动接收线程后和绑定发送方之前添加一个短暂的休眠,您将能够看到预期的结果。

public Start()
{    
    // Start listener
    new Thread(() =>
    {
        Receive();
    }).Start();

    Thread.Sleep(100);

    sender.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    sender.EnableBroadcast = true;
    // Bind the sender to known local IP and port 6039
    sender.Bind(ep);
}

额外注意:您可以使用 netcat 从 linux 盒子快速测试您的 udp 套接字:

# echo "hello" | nc -q -1 -u 192.168.1.149 6039 -

- 编辑 -

第 2 部分问题

“255.255.255.255”的源地址无效。 用两个改变源 ip 的相同数据包进行了快速测试:

https://i.stack.imgur.com/BvWIa.jpg

只有具有有效源 IP 的那个被打印到控制台。

Received 26 bytes
Received 26 bytes

所以实际上我的问题出在传出数据报的格式上。 command 字段需要是本地子网 192.168.xxx.xxx 上的地址,而不是 255.255.255.255... 无论出于何种原因,这都会导致数据包在到达我的应用程序之前在某处被过滤,尽管 WireShark 仍然可以看到它。这在这类工作中可能是常识,但由于对网络编程以及此接口的细节相对无知,所以我没有考虑过。

进行更改可以让简单的 UdpClient send/receive 完美运行。

非常感谢 Viet Hoang 帮我找到这个!