我正在编写一个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。
我想知道是否可以对这些方法中的任何一种进行更改,以使它们一致地工作。第一种方法有时有效,有时有效。第二种方法始终有效。我愿意使用这些方法中任何一种的修改版本,或者如果我完全走错了路,也可以使用全新的方法。
正如您所怀疑的,您在 Windows 上看到不一致的设备发现,因为您的 SSDP M-SEARCH 消息间歇性地发送到 127.0.0.1 (localhost),而不是预期的多播地址 239.255.255.250。
您需要按照以下步骤操作:
我对此进行了测试,并将我的 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()