Python 套接字 SSDP 搜索在 Windows 上工作不一致

问题描述 投票:0回答:1

目标

我正在编写一个Python Windows应用程序,需要使用ssdp搜索可用的网络设备。搜索将用于获取特定设备类型的 IP 地址以建立连接。

问题

我们得到的 ssdp 搜索结果非常不一致。有时有效,有时失败。使用 Wireshark 和 Device Sniffer(来自 Meshcommander UPnP 工具),我们发现当失败时,ssdp M-SEARCH 会被发送到

127.0.0.1
,因此它永远不会到达网络。它何时起作用或何时失败似乎没有规律或原因。我确信这是 Windows 特有的,因为相同的代码在 Mac 上运行没有问题。

间歇性工作的代码:

import socket
import struct
import time

local_ip = '192.168.20.48'
ssdp_addr = "239.255.255.250"
ssdp_port = 1900
searchterm = 'ssdp:all'

m_msg = f"""\
M-SEARCH * HTTP/1.1\r\n\
Host: {ssdp_addr}:{ssdp_port}\r\n\
Man: "ssdp:discover"\r\n\
ST: {searchterm}\r\n\
MX: 2\r\n\r\n""".encode("utf-8")

if __name__ == "__main__":
    # Method 1, occassionally works. Most of the time sends to 127.0.0.1:port
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 5)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.settimeout(5)
    sock.sendto(m_msg, (ssdp_addr, ssdp_port)) # Sometimes works, sometimes doesn't...

    # If send worked, this will receive the response. But, if send is routed to 127.0.0.1,
    # nothing is received (which is expected given it is localhost)
    tLim = time.time() + 6
    while time.time() < tLim:
        try:
            data, addr = sock.recvfrom(4096)
            print(f"Received from {addr}: {data}")
        
        except socket.timeout:
            break

    sock.close()

PyPi 上的

ssdpy
库也会出现此问题,这并不奇怪,因为该库的来源与上述实现类似。

尝试的解决方案

我已经尝试了我能想到的每种类型的 Windows 配置(防火墙设置、服务、权限、切换网络...),但均无济于事。网上搜索后发现一个方法,发送的M-Search消息不再路由到

127.0.0.1
,但无法收到设备的响应!使用wireshark,我可以看到设备发送的响应,但代码不会读取它。尽管实现会发现网络上同时发生的随机 ssdp 流量。

import socket
import struct
import time

local_ip = '192.168.20.48'
ssdp_addr = "239.255.255.250"
ssdp_port = 1900
searchterm = 'ssdp:all'

m_msg = f"""\
M-SEARCH * HTTP/1.1\r\n\
Host: {ssdp_addr}:{ssdp_port}\r\n\
Man: "ssdp:discover"\r\n\
ST: {searchterm}\r\n\
MX: 2\r\n\r\n""".encode("utf-8")

if __name__ == "__main__":
    # Method 2
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((local_ip, ssdp_port))
    
    group = socket.inet_aton(ssdp_addr)
    mreq = struct.pack('4s4s', group, socket.inet_aton(local_ip))
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    sock.settimeout(5)
    
    ret = sock.sendto(m_msg, (ssdp_addr, ssdp_port))

    # CODE to listen for SSDP responses. This finds unrelated network upnp notifies, but 
    # does not get the response from the device. I can see the response from the device is 
    # being sent with wireshark
    tLim = time.time() + 6
    while time.time() < tLim:
        try:
            data, addr = sock.recvfrom(4096)
            print(f"Received from {addr}: {data}")
        
        except socket.timeout:
            break
        
    sock.close()

使用此方法,Wireshark 显示

M-SEARCH
,源/目标 =
192.168.20.48 / 239.255.255.250
以及设备源/目标 =
192.168.30.82 / 192.168.20.48

的响应

其中

192.168.20.48
= PC 本地 IP,
192.168.30.82
= 设备本地 IP。

帮助

我想知道是否可以对这些方法中的任何一种进行更改,以使它们一致地工作。第一种方法有时有效,有时有效。第二种方法始终有效。我愿意使用这些方法中任何一种的修改版本,或者如果我完全走错了路,也可以使用全新的方法。

python ssdp
1个回答
0
投票

正如您所怀疑的,您在 Windows 上看到不一致的设备发现,因为您的 SSDP M-SEARCH 消息间歇性地发送到 127.0.0.1 (localhost),而不是预期的多播地址 239.255.255.250。

您需要按照以下步骤操作:

  1. 识别主要本地 IP 地址
  2. 配置多播的 UDP 套接字
  3. 发送 M-SEARCH 消息
  4. 聆听回应

我对此进行了测试,并将我的 VirtualBox 网桥视为找到的主要 IP。 下面没有显示,我添加了对所有本地接口的搜索。

import socket
import struct
import time

def get_primary_ip(target_subnet='192.168.30.'):
    local_ips = socket.gethostbyname_ex(socket.gethostname())[2]
    for ip in local_ips:
        if ip.startswith(target_subnet):
            return ip
    return '127.0.0.1'

def send_ssdp_discover(search_target="ssdp:all", mx=2, target_subnet=None):
    ssdp_addr = "239.255.255.250"
    ssdp_port = 1900
    m_search = f"""M-SEARCH * HTTP/1.1\r
HOST: {ssdp_addr}:{ssdp_port}\r
MAN: "ssdp:discover"\r
ST: {search_target}\r
MX: {mx}\r
\r
""".encode('utf-8')
    # Get the primary LAN IP within the target subnet
    primary_ip = get_primary_ip(target_subnet)
    if primary_ip == '127.0.0.1':
        print(f"No active interface found in the {target_subnet} subnet.")
        return
    print(f"Primary LAN IP Address: {primary_ip}")
    # Create a UDP socket for Multicast
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        ttl = struct.pack('b', 2)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
        local_interface = socket.inet_aton(primary_ip)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, local_interface)
        sock.bind((primary_ip, 0))
        print(f"Using primary IP: {primary_ip}")
        # Send the M-SEARCH message
        sock.sendto(m_search, (ssdp_addr, ssdp_port))
        print(f"Sent M-SEARCH from {primary_ip} to {ssdp_addr}:{ssdp_port}")
        # Listen for responses
        sock.settimeout(mx + 1)
        start_time = time.time()
        while True:
            try:
                data, addr = sock.recvfrom(65507)
                print(f"Received response from {addr}:\n{data.decode('utf-8')}\n")
            except socket.timeout:
                print("No more responses.")
                break
            if time.time() - start_time > mx + 1:
                break
    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        sock.close()

if __name__ == "__main__":
    send_ssdp_discover()
© www.soinside.com 2019 - 2024. All rights reserved.