广播或单播获取有关收到的UDP消息的信息

时间:2018-06-29 13:49:00

标签: python sockets networking udp broadcast

大家好!

我正在开发一个Python模块,该模块允许使用UDP套接字通过本地IP网络发现和配置客户端设备。 我正在基于现有协议构建解决方案,因此该解决方案可以与现有软件一起使用,但是只能使用一个端口。 因此,我无法通过对不同的消息类型使用不同的端口来解决问题。 但是协议的安全性很大程度上取决于应将请求发送到广播还是直接发送给设备,从而决定应接受还是拒绝该请求。

我的主要问题是:我找不到在Python中区分单播消息(在同一端口/套接字上接收)的广播消息的方法。

此外,我正在寻找适用于Linux和Windows的解决方案。 此外,在某些必须运行此服务的(嵌入式OpenWrt)设备上,很难安装用cPython编写的模块,因此我强烈希望仅在Linux端使用内置资源。 我也坚持使用Python,但是如果无法解决问题,我也可以利用外部工具。

我将很高兴获得任何帮助或指导。 (这是我关于stackoverflow的第一篇文章)

2 个答案:

答案 0 :(得分:1)

linux有一个套接字选项(SO_PKTINFO),实际上,套接字选项对此非常理想-它会准确报告数据报的发送对象。手册页上说它仅适用于Linux。但是,Windows WSARecvMsg页面似乎也表明它也受支持。

另请参阅以下答案:How to differentiate between UDP Broadcasts and Unicasts?

如果这还不够的话,最简单的方法是在接收端创建两个套接字。两者都可以使用相同的端口,但是将绑定到不同的地址。例如,假设您的本地IP地址是192.168.0.100。然后,您创建两个套接字:一个套接字绑定到本地IP,另一个套接字绑定到广播地址。

from socket import socket, AF_INET, SOCK_DGRAM
from select import select

usock = socket(AF_INET, SOCK_DGRAM)
usock.bind(('192.168.0.100', 9999))

bsock = socket(AF_INET, SOCK_DGRAM)
bsock.bind(('255.255.255.255', 9999))

while True:
    rrdy, wrdy, xrdy = select([usock, bsock], [], [])
    if usock in rrdy:
        in_msg, in_addr = usock.recvfrom(1024)
        print("Got unicast msg", in_msg, "from", in_addr)
    if usock in rrdy:
        in_msg, in_addr = bsock.recvfrom(1024)
        print("Got broadcast msg", in_msg, "from", in_addr)

这绝对适用于linux。我不确定它是否可以在Windows上运行,但可以预期会如此。

我在这里使用select来接收来自任何一个套接字的消息,但是如果易于管理,您可以轻松地为其中一个端口剥离一个单独的线程。

答案 1 :(得分:0)

所以我终于设法解决了我的问题。 汉密尔顿的答案非常恰当,使解决方案更加接近,但是正如他提到的那样,它可能无法在Windows上运行(也不会)。 Linux解决方案几乎可以同时在两者上使用,但是Wondows套接字有一些非常令人讨厌的差异:

  • 不能将套接字绑定到广播地址。 (n.n.n.255255.255.255.255均无效)
  • 您可以将套接字绑定到任意播(0.0.0.0)或任何有效的本地地址,并且它将仅接收单播消息。
  • 如果在绑定前设置了socketopt SO_BROADCAST,它也会接收单播和广播消息(并且您不会说出区别)。
  • 如果要将多个套接字绑定到同一端口,则必须在所有套接字上设置socketopt SO_REUSEADDR
  • 如果构造一个单播和“双向广播”套接字并绑定到同一端口,则最终都将接收到单播消息。
  • 如果您使用select(如在Linux示例中由这两个套接字供电)并且它们接收相同的单播消息,则select在第一轮中仅返回一个。 (这是Windows 10的个人实验)

我最终解决了这个问题:

import platform
from socket import gethostbyname, gethostname, socket, AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_BROADCAST, SO_REUSEADDR
from select import select

PORT = 9999
myPublicIP = gethostbyname(gethostname())
platform = platform.system()

bsock = socket(AF_INET, SOCK_DGRAM)
usock = socket(AF_INET, SOCK_DGRAM)

if platform == 'Windows':
    usock.setsockopt(SOL_SOCKET, SO_BROADCAST, 0)  # This isn't necessary
    usock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    bsock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
    bsock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    usock.bind(('', PORT))
    bsock.bind(('', PORT))
    # empty string means anycast
elif platform == 'Linux':
    # on Linux SO_BROADCAST only affects the sending
    usock.bind((myPublicIP, PORT))
    bsock.bind(('255.255.255.255', PORT))

while True:
    # This is the tricky part
    select([bsock, usock], [], [])
    # if a bcast msg received the first select gives back only the usock instead of both
    ready = select([bsock, usock], [], [])[0]
    # the second select will give back both at the same time
    if bsock in ready and usock in ready:
        bmsg = bsock.recvfrom(1024)
        umsg = usock.recvfrom(1024)
        in_msg, in_addr = bmsg
        print("Got broadcast msg", in_msg, "from", in_addr)
        if bmsg != umsg:
            in_msg, in_addr = umsg
            print("Got unicast msg", in_msg, "from", in_addr)
    else:
        if bsock in ready:
            in_msg, in_addr = bsock.recvfrom(1024)
            print("Got broadcast msg", in_msg, "from", in_addr)
        if usock in ready:
            in_msg, in_addr = usock.recvfrom(1024)
            print("Got unicast msg", in_msg, "from", in_addr)