物联网 · 盛世宏博 · UDP广播 · 节点发现 · 自动注册 · 内网多播

关键词:UDP广播、温湿度采集、传感器节点发现、自动注册、内网通信、机房监控、无中心发现、设备枚举 标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控
传统部署模式下,每台以太网温湿度传感器都需要手动配置IP、端口、从站地址,然后在采集平台中逐条录入。一个中型机房20~30个测点,配置工作量尚可接受;但如果是多机房、多楼层、上百个节点,手动录入就是一场噩梦。
广播发现解决的核心问题:
传统模式 | 广播发现模式 |
|---|---|
逐台配置IP | 传感器上电即自动广播 |
手动记录MAC-IP映射 | 平台自动接收并注册 |
新增节点需改配置 | 平台自动发现新节点 |
IP变更需重新录入 | 节点重新广播即更新 |
部署耗时:30分钟/台 | 部署耗时:插电即可 |
适用场景:同一广播域内的局域网环境(单个VLAN/子网),传感器数量5~200台。
┌─────────────────────────────────────────────────────────────┐
│ 机房内网(同一广播域) │
│ │
│ ┌──────────────┐ UDP广播(255.255.255.255) ┌───────┐│
│ │ 采集平台 │◄────────────────────────────────│传感器A ││
│ │ (监听端口 │ "I'm here" + MAC + IP │(上电) ││
│ │ 9999) │ └───────┘│
│ │ │ UDP广播 ┌───────┐│
│ │ 维护节点表 │◄────────────────────────────────│传感器B ││
│ │ {MAC→IP} │ "I'm here" + MAC + IP │(上电) ││
│ └──────────────┘ └───────┘│
│ ▲ ┌───────┐│
│ │ UDP广播 │传感器C ││
│ └───────────────────────────────────────│(上电) ││
│ "I'm here" + MAC + IP └───────┘│
└─────────────────────────────────────────────────────────────────┘发现请求(平台→广播):
┌────────┬────────┬──────────┬────────┐
│ Magic │ Ver │ Cmd │ Seq │
│ 2B │ 1B │ 1B │ 4B │
│ 0xAA55 │ 0x01 │ 0x01(查询)│ 递增 │
└────────┴────────┴──────────┴────────┘
发现响应(传感器→平台单播):
┌────────┬────────┬──────────┬────────┬─────────┬────────┬──────────┐
│ Magic │ Ver │ Cmd │ Seq │ MAC(6B) │ IP(4B)│ 设备类型 │
│ 2B │ 1B │ 1B │ 4B │ │ │ 2B │
│ 0xAA55 │ 0x01 │ 0x02(应答)│ 对应请求│ │ │ 0x0001 │
└────────┴────────┴──────────┴────────┴─────────┴────────┴──────────┘
心跳广播(传感器→广播,周期性):
┌────────┬────────┬──────────┬────────┬─────────┬────────┬──────────┐
│ Magic │ Ver │ Cmd │ Seq │ MAC(6B) │ IP(4B)│ 运行状态 │
│ 0xAA55 │ 0x01 │ 0x03(心跳)│ 递增 │ │ │ 2B │
└────────┴────────┴──────────┴────────┴─────────┴────────┴──────────┘# discovery_server.py - UDP广播发现服务
import socket
import struct
import threading
import time
from dataclasses import dataclass, field
from typing import Dict, Set
MAGIC = 0xAA55
CMD_QUERY = 0x01
CMD_REPLY = 0x02
CMD_HEARTBEAT = 0x03
DISCOVERY_PORT = 9999
@dataclass
class SensorNode:
mac: str
ip: str
device_type: str = "TH-SENSOR"
last_seen: float = 0
online: bool = True
seq: int = 0
class DiscoveryServer:
def __init__(self, bind_ip="0.0.0.0", port=DISCOVERY_PORT):
self.bind_ip = bind_ip
self.port = port
self.nodes: Dict[str, SensorNode] = {} # mac -> node
self.sock = None
self.running = False
self._seq = 0
def start(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.sock.bind((self.bind_ip, self.port))
self.running = True
# 启动接收线程
t = threading.Thread(target=self._recv_loop, daemon=True)
t.start()
# 启动定期查询线程
t2 = threading.Thread(target=self._query_loop, daemon=True)
t2.start()
print(f"[Discovery] Server started on {self.bind_ip}:{self.port}")
def _recv_loop(self):
while self.running:
try:
data, addr = self.sock.recvfrom(1024)
self._handle_packet(data, addr)
except Exception as e:
print(f"[Discovery] Recv error: {e}")
def _handle_packet(self, data: bytes, addr):
if len(data) < 8:
return
magic, ver, cmd, seq = struct.unpack('>HBBI', data[:8])
if magic != MAGIC:
return
if cmd == CMD_REPLY and len(data) >= 22:
# 解析传感器应答
mac_bytes = data[8:14]
mac = ':'.join(f'{b:02X}' for b in mac_bytes)
ip = '.'.join(str(b) for b in data[14:18])
dev_type = struct.unpack('>H', data[18:20])[0]
if mac not in self.nodes:
print(f"[Discovery] ✅ 新传感器上线: MAC={mac} IP={ip}")
self.nodes[mac] = SensorNode(mac=mac, ip=ip, last_seen=time.time())
else:
node = self.nodes[mac]
if node.ip != ip:
print(f"[Discovery] 📝 IP变更: {node.ip} → {ip}")
node.ip = ip
node.online = True
node.last_seen = time.time()
elif cmd == CMD_HEARTBEAT and len(data) >= 20:
mac_bytes = data[8:14]
mac = ':'.join(f'{b:02X}' for b in mac_bytes)
ip = '.'.join(str(b) for b in data[14:18])
status = struct.unpack('>H', data[18:20])[0]
if mac in self.nodes:
self.nodes[mac].last_seen = time.time()
self.nodes[mac].online = (status == 0)
else:
# 未知节点心跳,可能是新设备
print(f"[Discovery] ⚠️ 未知节点心跳: MAC={mac} IP={ip}")
def _query_loop(self):
"""定期发送发现查询"""
while self.running:
self._send_query()
time.sleep(30) # 每30秒查询一次
def _send_query(self):
self._seq += 1
packet = struct.pack('>HBBI', MAGIC, 0x01, CMD_QUERY, self._seq)
self.sock.sendto(packet, ('<broadcast>', self.port))
def get_nodes(self) -> Dict[str, SensorNode]:
return self.nodes
def get_online_nodes(self) -> Dict[str, SensorNode]:
return {k: v for k, v in self.nodes.items() if v.online}
def stop(self):
self.running = False
self.sock.close()/* sensor_discovery.c - 传感器端广播发现(嵌入式C) */
#include <lwip/udp.h>
#include <lwip/netif.h>
#define DISCOVERY_PORT 9999
#define MAGIC 0xAA55
static uint32_t g_seq = 0;
void send_discovery_reply(struct udp_pcb *pcb, const ip_addr_t *dst, uint16_t cmd) {
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, 22, PBUF_RAM);
if (!p) return;
uint8_t *d = (uint8_t *)p->payload;
uint8_t mac[6];
get_mac_addr(mac); // 读取MAC地址
// 填充报文头
d[0] = 0xAA; d[1] = 0x55; // Magic
d[2] = 0x01; // Version
d[3] = cmd; // CMD (0x02=应答, 0x03=心跳)
d[4] = (g_seq >> 24) & 0xFF; d[5] = (g_seq >> 16) & 0xFF;
d[6] = (g_seq >> 8) & 0xFF; d[7] = g_seq & 0xFF;
// MAC地址
memcpy(d + 8, mac, 6);
// IP地址
ip_addr_t *ip = netif_default->ip_addr;
d[14] = ip4_addr1(ip); d[15] = ip4_addr2(ip);
d[16] = ip4_addr3(ip); d[17] = ip4_addr4(ip);
// 设备类型
d[18] = 0x00; d[19] = 0x01; // 温湿度传感器
// 运行状态(心跳用)
d[20] = 0x00; d[21] = get_system_status();
udp_sendto(pcb, p, dst, DISCOVERY_PORT);
pbuf_free(p);
g_seq++;
}
// 上电时发送应答
void on_power_on() {
ip_addr_t broadcast;
IP4_ADDR(&broadcast, 255, 255, 255, 255);
send_discovery_reply(discovery_pcb, &broadcast, 0x02);
}
// 心跳定时器回调(每60秒)
void heartbeat_timer_cb() {
ip_addr_t broadcast;
IP4_ADDR(&broadcast, 255, 255, 255, 255);
send_discovery_reply(discovery_pcb, &broadcast, 0x03);
}# auto_register.py - 发现→注册→采集联动
from discovery_server import DiscoveryServer
from collector import MultiSiteCollector # 前文定义的采集引擎
class AutoRegistration:
"""将发现的传感器自动注册到采集引擎"""
def __init__(self, discovery: DiscoveryServer, collector: MultiSiteCollector):
self.discovery = discovery
self.collector = collector
self.registered = set() # 已注册的MAC集合
def check_new_nodes(self):
"""检查新发现的节点并注册"""
for mac, node in self.discovery.get_online_nodes().items():
if mac not in self.registered:
print(f"[AutoReg] 注册新传感器: {mac} @ {node.ip}")
# 注册到采集引擎
self.collector.register_sensor(
sensor_id=mac,
ip=node.ip,
port=502, # Modbus TCP默认端口
protocol="modbus_tcp"
)
self.registered.add(mac)
def start_monitor(self, interval=10):
"""启动监控循环"""
import time
while True:
self.check_new_nodes()
time.sleep(interval)问题:传感器每次上电都广播,平台反复注册
解决:以MAC为唯一键,已存在则更新IP,不重复创建机制:心跳超时判定离线
心跳间隔:60秒
超时阈值:180秒(3个周期未收到心跳 → 标记离线)
离线后处理:从采集引擎中暂停该节点,但保留注册信息风险 | 防护手段 |
|---|---|
大量传感器同时上电 | 传感器端加随机延迟(0~5秒)再发首次广播 |
平台频繁查询 | 查询间隔≥30秒,支持指数退避 |
广播报文被交换机抑制 | 配置交换机广播限速(但仍放行UDP 9999) |
问题:任何人都能发送查询报文获取传感器列表
增强方案:
□ 查询报文加HMAC签名(共享密钥)
□ 传感器只响应已配对平台的查询
□ 发现完成后平台发送"静默"命令,传感器停止广播# 过滤UDP广播发现报文
udp.port == 9999
# 报文示例:
# 平台查询:
# 0000 aa 55 01 01 00 00 00 01
# 传感器应答:
# 0000 aa 55 01 02 00 00 00 02 00 1a 2b 3c 4d 5e 06 c0 a8 01 64 00 01 00 00
# |Magic|Ver|Cmd| Seq | MAC(6B) | IP(192.168.1.100) |Dev|Status|255.255.255.255在某些系统上被禁止,改用子网定向广播(如192.168.1.255)。解决:测试两种广播方式,选择可用的。 SO_BINDTODEVICE或绑定到具体接口IP。 SO_BROADCAST选项。解决:setsockopt(SOL_SOCKET, SO_BROADCAST, 1)。 UDP广播发现机制的核心价值在于将"人找设备"变成"设备找平台"。传感器上电即广播自身信息,平台自动发现、自动注册、自动纳入采集,大幅降低部署工作量。协议设计上保持简洁(Magic+MAC+IP+Cmd),实现上注意广播风暴防护和离线检测。对于跨VLAN场景,需配合UDP中继或组播方案。广播发现不是万能的——它只适用于同一广播域内的初始发现和自动注册,正式数据采集仍建议切换到单播TCP/UDP模式以保证可靠性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。