如何用Java基于WS-Discovery规范发现ONVIF设备并获取详情
你的方向完全正确!咱们一步步来解决Java实现ONVIF设备发现的问题:
ONVIF设备发现确实遵循WS-Discovery协议,核心流程就是向多播地址 239.255.255.259:3702 发送Probe SOAP消息,设备返回的ProbeMatch中的XAddrs字段就是你要的设备服务地址(注意:这不是单纯的IP,是包含服务路径的完整URL,比如http://192.168.1.100/onvif/device_service,后续调用设备管理WSDL生成的代码时就用这个地址)。
另外你写的Probe SOAP消息基本正确,只有一个小细节要修正:w:Action标签里的a:mustUnderstand应该改成e:mustUnderstand(因为你没有声明a命名空间,而e是SOAP信封的命名空间),修正后的完整消息如下:
<e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dn="http://www.onvif.org/ver10/network/wsdl">
<e:Header>
<w:MessageID>uuid:84ede3de-7dec-11d0-c360-f01234567890</w:MessageID>
<w:To e:mustUnderstand="true">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>
<w:Action e:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>
</e:Header>
<e:Body>
<d:Probe>
</d:Probe>
</e:Body>
</e:Envelope>
下面是完整的Java代码实现,分为发送Probe消息和接收解析响应两部分:
1. 发送UDP多播消息
Java中用MulticastSocket处理多播通信,步骤很清晰:
- 创建
MulticastSocket并配置超时(避免无限等待) - 加入目标多播组
239.255.255.259 - 生成唯一的
MessageID(用UUID保证唯一性) - 将SOAP消息转为字节数组,发送到多播地址和端口
import java.net.*; import java.io.IOException; import java.util.UUID; public class ONVIFDiscovery { private static final String MULTICAST_ADDRESS = "239.255.255.259"; private static final int PORT = 3702; private static final int TIMEOUT = 5000; // 5秒超时,可根据网络调整 public static void main(String[] args) throws IOException { // 生成唯一的MessageID,每次发送都要换 String messageId = "uuid:" + UUID.randomUUID(); // 构造修正后的SOAP消息 String soapMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" " + "xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " + "xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" " + "xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\">" + "<e:Header>" + "<w:MessageID>" + messageId + "</w:MessageID>" + "<w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>" + "<w:Action e:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>" + "</e:Header>" + "<e:Body>" + "<d:Probe>" + "</d:Probe>" + "</e:Body>" + "</e:Envelope>"; // 初始化多播Socket,使用try-with-resources自动关闭资源 try (MulticastSocket socket = new MulticastSocket()) { socket.setSoTimeout(TIMEOUT); InetAddress group = InetAddress.getByName(MULTICAST_ADDRESS); socket.joinGroup(group); // 发送Probe消息 DatagramPacket packet = new DatagramPacket( soapMessage.getBytes("UTF-8"), soapMessage.getBytes("UTF-8").length, group, PORT ); socket.send(packet); System.out.println("Probe消息已发送,等待设备响应..."); // 循环接收响应,直到超时 byte[] buffer = new byte[4096]; while (true) { try { DatagramPacket responsePacket = new DatagramPacket(buffer, buffer.length); socket.receive(responsePacket); String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8"); // 解析响应中的设备服务地址 parseDeviceServiceAddress(response); } catch (SocketTimeoutException e) { System.out.println("\n超时,设备发现结束"); break; } } socket.leaveGroup(group); } }
2. 解析响应中的设备服务地址
收到的响应是ProbeMatch格式的SOAP消息,我们需要从中提取XAddrs字段。这里用Java的DOM解析实现(支持命名空间):
private static void parseDeviceServiceAddress(String soapResponse) { try { javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); // 必须开启命名空间支持,否则找不到对应标签 javax.xml.parsers.DocumentBuilder builder = factory.newDocumentBuilder(); org.w3c.dom.Document doc = builder.parse(new java.io.ByteArrayInputStream(soapResponse.getBytes("UTF-8"))); // 定义WS-Discovery的命名空间 String discoveryNs = "http://schemas.xmlsoap.org/ws/2005/04/discovery"; org.w3c.dom.NodeList probeMatchList = doc.getElementsByTagNameNS(discoveryNs, "ProbeMatch"); for (int i = 0; i < probeMatchList.getLength(); i++) { org.w3c.dom.Element probeMatch = (org.w3c.dom.Element) probeMatchList.item(i); org.w3c.dom.Node xAddrsNode = probeMatch.getElementsByTagNameNS(discoveryNs, "XAddrs").item(0); if (xAddrsNode != null) { String xAddrs = xAddrsNode.getTextContent().trim(); // XAddrs可能包含多个地址(空格分隔),取第一个即可 String serviceAddress = xAddrs.split(" ")[0]; System.out.println("\n找到设备服务地址:" + serviceAddress); } } } catch (Exception e) { System.err.println("解析响应出错:"); e.printStackTrace(); } } }
- 防火墙权限:确保你的程序有权限发送/接收UDP多播消息,防火墙要允许UDP 3702端口的通信
- UUID唯一性:每次发送Probe消息必须生成新的UUID作为
MessageID,避免和历史请求混淆 - 网络环境:多播消息通常不会跨路由传播,确保你的程序和ONVIF设备在同一个局域网内
- XAddrs格式:部分设备会返回多个地址,一般取第一个作为设备服务地址即可
内容的提问来源于stack exchange,提问作者Jared H.




