2

我正在尝试使用一些 Java 代码发现 ONVIF 设备。具体来说,我正在尝试获取他们的设备服务地址(我相信这只是他们的 IP 地址?),正如ONVIF 核心规范(在第 4.3 节中)所指出的那样,“成功的发现提供了设备服务地址。一旦客户端拥有设备服务地址它可以通过设备服务接收详细的设备信息...”。最终获得网络上 ONVIF 设备的详细信息是我的目标。一般来说,我也在寻找一些与使用 ONVIF 规范有关的指导。

我对 Web 服务世界(以及一般网络)还是新手,所以如果我说任何愚蠢的话,请原谅我。但是,我自己为此付出了很多努力:我已经阅读了大量的ONVIF Core SpecONVIF Application Programmer's GuideWS-Discovery Specification。如果可以的话,我会总结一下我所知道的,这样你就可以告诉我我是否走在正确的轨道上:

  1. “Web 服务”是在 IP 网络上使用独立于平台和语言的 Web 服务标准(例如 XML、SOAP 和 WSDL)的标准技术的名称。基本思想是我们希望能够从任何编程语言调用有效的方法/函数(服务)。
  2. Web 服务通常托管在服务器上。但在 ONVIF 用例中,Web 服务提供商是 ONVIF 设备(例如,IP 摄像机)。因此,为了从任何语言与设备交互,我们使用 Web 服务操作/调用,因为 Web 服务调用可以用任何语言实现。
  3. XML 是数据描述语法(使用它是因为它与语言无关;任何语言都可以解析它)。SOAP 是用于来回获取注入 SOAP 的 XML 文档的通信协议(基本上,进行我们的方法调用)。WSDL 用于描述服务(它是基于 XML 的 Web 服务接口描述)。我在这里下载了用于设备管理的 WSDL ,并通过 WSDL 编译器wsimport(由 JDK 提供)从 WSDL 中生成 Java 类以在我的代码中使用。但我知道调用这些方法将在设备发现之后进行,对吗?
  4. ONVIF 设备是根据 WS-Discovery 规范发现的。如ONVIF 应用程序程序员指南中第 13 页和第 14 页所述,您发送一条Probe消息,匹配探测器约束的设备发回一条消息ProbeMatch

这是我开始感到困惑的地方。我究竟如何在 Java 中发送此消息?ONVIF Application Programmer's Guide 在第 15 页提供了一些伪代码,但我不知道如何实现它。该指南中的第 4.3.1 节特别是我坚持的内容。我知道“范围”和“类型”只是您可以嵌入到探针中的约束,但它们不是必需的(根据WS discovery spec的第 5 页)。由于我想发现所有设备,我认为我不需要任何限制即可开始,对吧?

因此,该指南还在第 110 页提供了用于发现的示例 SOAP 消息。从中删除类型声明(因为我不想要那个约束),我知道我要发送的 SOAP 消息将是(我相信?)这个:

<?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>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
a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2005/04/discovery/Pr
obe</w:Action>
 </e:Header>
 <e:Body>
 <d:Probe>
 </d:Probe>
 </e:Body>
</e:Envelope>   

而且我也了解 WS-Discovery 技术使用地址 239.255.255.259 和 UDP 端口 3702 ......但这就是我得到的结束。如何在 Java 中将该 SOAP 消息发送到该地址和端口?如何读取响应(我认为我会以注入 SOAP 的 XML 文档的形式返回 ProbeMatch 消息,因此我需要解析该 XML 以获取XAddrs,但不确定)。我是否需要以某种方式将该 SOAP 消息的 UDP 广播发送到该地址?

TL;DR:我相信要进行 ONVIF 设备发现,我需要将上面的 SOAP 消息发送到 UDP 端口 3702 上的地址 239.255.255.259。我不知道如何在 Java 中执行此操作,只是在寻找一些指导;我什至不确定我是否在正确的轨道上进行设备发现。

4

2 回答 2

4

经过反复试验,我找到了(另一种)解决我的问题的方法。在大多数情况下,mpromonet 可能是要走的路,我只是想避免使用像 Apache 这样相当大的依赖项。我还认为这应该可以通过一些简单的 UDP 消息传递来实现。

该解决方案也基于 SO 用户 Thomas 的有用代码。我主要只是通过删除线程来简化他的代码,并添加了一些注释。同样,他的解决方案可能比我的更好(性能更高);但是,对于初学者(比如我)来说,我的可能更容易理解。

这是代码:

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.*;

import javax.xml.namespace.QName;
import javax.xml.soap.*;
import java.util.*;

public class ONVIFDeviceDiscoveryFIN {

   // Following constants are related to Discovery process
   public static final int WS_DISCOVERY_TIMEOUT = 4000; // 4 seconds. Time to wait to receive a packet
   public static final int WS_DISCOVERY_PORT = 3702; 
   public static final String WS_DISCOVERY_ADDRESS_IPv4 = "239.255.255.250";

   // note that the probe below MUST be given a unique urn:uuid. Devices will NOT reply if the urn:uuid is not unique! 
   public static final String WS_DISCOVERY_PROBE_MESSAGE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + 
        "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:tns=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\">\r\n" + 
        "   <soap:Header>\r\n" + 
        "      <wsa:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action>\r\n" + 
        "      <wsa:MessageID>urn:uuid:5e1cec36-03b9-4d8b-9624-0c5283982a00</wsa:MessageID>\r\n" + 
        "      <wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>\r\n" + 
        "   </soap:Header>\r\n" + 
        "   <soap:Body>\r\n" + 
        "      <tns:Probe>\r\n" + 
        "         <tns:Types>tds:Device</tns:Types>\r\n" + // Constraint to find just ONVIF devices hopefully? Recall we are sending a probe on the 192.168.0.50 network; if we have no constraints, it would find everything there! WS-Discovery generally is for much more than ONVIF, like printers and stuff
        "      </tns:Probe>\r\n" + 
        "   </soap:Body>\r\n" + 
        "</soap:Envelope>";

   private static ArrayList<String> getResponsesToProbe(String uuid) throws IOException{
       // TODO: add in ability to send scope and type constraints
       // NOTE: We do need to know the address of the network interface to discover devices on...
       // Function composes and sends a Probe to discover devices on the network. uuid is the urn:uuid to put in the probe. Functions returns all the SOAP-Infused XML responses (all the ProbeMatches).

       // give the probe a unique urn:uuid (we must do this for each probe!). This is generated outside function
       final String probe = WS_DISCOVERY_PROBE_MESSAGE.replaceAll("<wsa:MessageID>urn:uuid:.*</wsa:MessageID>", "<wsa:MessageID>urn:uuid:" + uuid + "</wsa:MessageID>");

       // set up the "sender and receiver"; this is the socket that we send our probe from, and where we receive back the ProbeMatch responses.
       // NOTE:  that we do need to know the address of the network interface to discover devices on... (port could be anything)
       final int port = 55000;
       DatagramSocket senderAndReceiver = new DatagramSocket(port, InetAddress.getByName("192.168.0.50")); // so you do need to know the address of your network interface to discover devices on...
       senderAndReceiver.setSoTimeout(WS_DISCOVERY_TIMEOUT);

       // send the probe 
       DatagramPacket probeMsg = new DatagramPacket(probe.getBytes(), probe.length(), InetAddress.getByName(WS_DISCOVERY_ADDRESS_IPv4), WS_DISCOVERY_PORT);
       senderAndReceiver.send(probeMsg);

       // read in the responses
       ArrayList<String> responses = new ArrayList(); // this is the collection of all SOAP-infused XML ProbeMatch responses
       byte[] receiverBuffer = new byte[8192];
       DatagramPacket receiverPacket = new DatagramPacket(receiverBuffer, receiverBuffer.length); // this is the packet that receive the response in. Get's updated with the next response on each call to .receive()
       while (true) {
           try {
               senderAndReceiver.receive(receiverPacket);
               responses.add(new String(receiverPacket.getData()));

           } catch (SocketTimeoutException e) {
               // System.out.println("Socket read timeout; taken to mean that there is no more responses -- i.e., no more Probe Matches");
               break;
           }
       }

       // close the socket
       senderAndReceiver.close();

       return responses;

   }

   public static void main(String[] args) throws IOException, SOAPException {


       final String uuid = UUID.randomUUID().toString(); // generate the uuid to add to the Probe message

       ArrayList<String> responses = getResponsesToProbe(uuid); // responses is a collection of all the SOAP-infused XML ProbeMatches . It's all of our responses to the probe; it's basically the devices we've discovered!

    }

}

关于使用它的一些注意事项:

  1. 要使用此解决方案,您需要知道要查看的“网络接口”。在我的代码中,这是 192.168.0.50 。这是我要查找的相机所在的网络。要找到它,请在 cmd 提示符下运行arp -a命令(不确定如何在 Mac 或 Linux 上执行此操作),然后找到相机的 IP。它所属的接口是您要用作“192.168.0.50”的接口。在我有限的理解中,这些接口基本上是对您的网络进行分段,因此您需要选择正确的接口来查找设备。我认为(?)Thomas 的代码通过查找所有这些网络接口来避免这个问题。这是在他的代码的第 81-100 行中完成的。

  2. 发送时,您必须为您的 Probe 提供一个唯一的 UUID。这是我这样做的错误之一;我在 Probe(在WS_DISCOVERY_PROBE_MESSAGE)中使用硬编码的 UUID 进行了测试。这将有助于发现设备一次;但在那之后,如果您发送具有相同 UUID 的探测,设备似乎根本不会回复。你也不会得到任何错误响应,因此我很难找出答案。就好像设备保留了所有接收到的 Probe 的 UUID 的一些内部日志;如果您发送带有旧 UUID 的探测,它只会拒绝它。或者至少,我正在测试的符合 ONVIF 标准的相机(AXIS M3045-V)就是这种情况。我不确定 ONVIF 规范是否需要这种行为,但至少在 AXIS M3045-V 中很明显。

  3. 注意:SOAP 通常依赖 HTTP 进行传输;但这里我们在 UDP 之上使用它。

我希望这可以帮助任何尝试做类似事情的人。如果有什么我可以帮忙的,请告诉我;在这一点上,我已经阅读了大量的文档,所以我可以伸出援助之手!

于 2019-06-05T13:04:46.860 回答
2

使用 CXF WSDiscoveryClient,您可以探测 ONVIF 设备。
默认情况下,WSDiscoveryClient 使用 WS-discovery 1.1,ONVIF 使用 WS-discovery 1.0,因此您需要启用 WS-discovery 1.0。
一个简短的实现可能是:

import java.util.List;
import javax.xml.ws.EndpointReference;
import org.apache.cxf.ws.discovery.WSDiscoveryClient;

public class Main 
{
    public static void main(String[] args) 
    {
        WSDiscoveryClient client = new WSDiscoveryClient();
        client.setVersion10(); // use WS-discovery 1.0
        client.setDefaultProbeTimeout(1000); // timeout 1s

        System.out.println("Probe:" + client.getAddress());
        List<EndpointReference> references = client.probe();

        System.out.println("Nb answsers:" + references.size());
        for (EndpointReference ref : references)
        {
            System.out.println(ref.toString());
        }
    }
}
于 2019-05-30T21:33:02.393 回答