参考文章

Onvif协议:实现Probe命令来进行设备发现(discover)

一、简介

在onvif协议对接中,首先要明确服务器和客户端的身份

  • 服务器:通常是我们要对接的其他厂家的网络摄像头(IPC)
  • 客户端:通常是对接的ipc的设备程序,安防业内多称(NVR),当然其他软件工具也可称为客户端,如ONVIF Device Test Tool, vlc,ODM软件

我们要对接的是IPC摄像头,扮演的是客户端的身份。

在这里插入图片描述

二、基于ONVIF规范项目的开发基本流程

  1. 获取WSDL文件
  2. 通过gSOAP编译为c/c++文件
  3. 业务逻辑开发
  4. 编译发布

设备发现的实现请参考佬的文章:

Onvif协议:实现Probe命令来进行设备发现(discover)

三、原理介绍

1.获取WSDL文件

两种方法:

  • 可以在onvif官网下载需要的wsdl文件到本地

    • remotediscovery.wsdl :用于发现设备
    • devicemgmt.wsdl :用于获取设备参数
    • media.wsdl:用于获取视频流地址
    • ptz.wsdl:用于设备的PTZ控制
  • 也可以使用命令远程链接相关文件

    下载地址:https://www.onvif.org/profiles/specifications/

2.搭建onvif的开发环境:安装gSoap

​ onvif协议很复杂,我们需要借助工具来实现代码框架,这个工具叫gSOAP。该工具将soap协议进行封装为c/c++代码,这样我们就不需要关心soap协议,只需要考虑逻辑层就可以了。接下来我们就开始搭建环境。参考以下文章:

onvif规范的实现:使用gSOAP创建SOAP调用实例

https://blog.csdn.net/weixin_56438859/article/details/139335048

  • 在gSOAP编译成功后,会生成如下目录
    在这里插入图片描述

  • 在bin目录里有如下两个工具

    • wsdl2h:将wsdl协议文件生成头文件 ./wsdl2h -h 查看帮助
    • soapcpp2:根据上面的头文件生成源文件C/C++代码

3.设备搜索原理

​ 要访问一个IPC摄像头,或者说要调用IPC摄像头提供的WEB服务接口,就要先知道其IP地址,这就是「设备发现」的过程,或者叫「设备搜索」的过程。ONVIF规范并没有自己定义服务发现框架,而是复用了已经很成熟的WS-Discovery标准,WS-Discovery 协议使得服务能够被客户端发现。我们先了解下什么是WS-Discovery。

WS-Discovery原理
WS-Discovery:全称Web Services Dynamic Discovery。
官方技术规范:http://docs.oasis-open.org/ws-dd/discovery/1.1/os/wsdd-discovery-1.1-spec-os.html

​ 我们传统的Web Services服务调用的模式都是这样的:客户端在设计时就预先知道目标服务的地址(IP地址或者域名),客户端基于这个地址进行服务调用。那如果客户端预先不知道目标服务的地址该怎么办?

​ WS-Discovery(全称为Web Services Dynamic Discovery)标准就是用于解决该问题的,遵循该标准,客户端预先不知道目标服务地址的情况下,可以动态地探测到可用的目标服务,以便进行服务调用。这个过程就是「设备发现」的过程。

WS-Discovery定义了两种模式:Ad hoc模式和Managed模式。

  • Ad hoc模式: 客户端以多播(multicast)的形式往多播组(multicast group)发送一个Probe(探测)消息搜索目标服务,在该探测消息中,包含相应的搜寻条件。如果目标服务满足该条件,则直接将响应ProbeMatch消息(服务自身相关的信息,包括地址)回复给客户端。
  • Managed模式:即代理模式。Ad hoc模式有个局限性,只能局限于一个较小的网络。Managed模式就是为了解决这个问题的,在Managed模式下,一个维护所有可用目标服务的中心发现代理(Discovery Proxy)被建立起来,客户端只需要将探测消息发送到该发现代理就可以得到相应的目标服务信息。

单播、多播(组播)和广播的区别
WS-Discovery协议用到了多播,那什么是多播?

TCP/IP有三种传输方式:单播(Unicast)、多播(Multicast)和广播(Broadcast),在IPv6领域还有另一种方式:任播(Anycast)。任播在此不做介绍,以下简要介绍下单播、多播和广播的区别:

在这里插入图片描述

  • 单播(Unicast):一对一,双向通信,目的地址是对方主机地址。网络上绝大部分的数据都是以单播的形式传输的。如收发邮件、浏览网页等。
  • 广播(Broadcast):一对所有,单向通信,目的地址是广播地址,整个网络中所有主机均可以收到(不管你是否需要),如ARP地址解析、GARP数据包等。广播会被限制在局域网范围内,禁止广播数据穿过路由器,防止广播数据影响大面积的主机。
  • 多播(Multicast):也叫组播,一对多,单向通信,目的地址是多播地址,主机可以通过IGMP协议请求加入或退出某个多播组(multicast group),数据只会转发给有需要(已加入组)的主机,不影响其他不需要(未加入组)的主机。如网上视频会议、网上视频点播、IPTV等。

多播地址(Multicast Address)有很多,各个行业都不一样,IPC摄像头用的是239.255.255.250(端口3702),需要在头文件定义。


回到「设备发现」这个正轨上,搜索IPC有两种搜索方式:

  • 自己实现socket编程(UDP),通过sendto往多播地址发送探测消息(Probe),再使用recvfrom接收IPC的应答消息(ProbeMatch)。
  • 根据ONVIF标准的remotediscovery.wsdl文档,使用gSOAP工具快速生成框架代码,直接调用其生成的函数接口来搜索IPC。

从原理上来说,这两种方式归根结底是一样的,都是WS-Discovery协议,方式1是自己造轮子(自己码代码),方式2是利用gSOAP快速生成代码,我才有gSOAP快速生成代码。

四、相关数据结构和函数

1.数据结构

struct wsdd__ProbeType      req; 		// 用于发送Probe消息,定义在client\gsoap\import\wsdd10.h
struct wsdd__ProbeMatchType *probeMatch;

struct __wsdd__ProbeMatches rep; 		// 用于接收Probe应答,定义在client\application\soapStub.h

/* wsdx.h:94 */
#ifndef SOAP_TYPE___wsdd__ProbeMatches
#define SOAP_TYPE___wsdd__ProbeMatches (95)
/* Wrapper: */
struct SOAP_CMAC __wsdd__ProbeMatches {
      public:
        /** Optional element 'wsdd:ProbeMatches' of XML schema type 'wsdd:ProbeMatchesType' */
        struct wsdd__ProbeMatchesType *wsdd__ProbeMatches;
      public:
        /** Return unique type id SOAP_TYPE___wsdd__ProbeMatches */
        long soap_type() const { return SOAP_TYPE___wsdd__ProbeMatches; }
        /** Constructor with member initializations */
        __wsdd__ProbeMatches() : wsdd__ProbeMatches() { }
        /** Friend allocator */
        friend SOAP_FMAC1 __wsdd__ProbeMatches * SOAP_FMAC2 soap_instantiate___wsdd__ProbeMatches(struct soap*, int, const char*, const char*, size_t*);
};
#endif
/* wsdd10.h:480 */
#ifndef SOAP_TYPE__wsdd__Probe
#define SOAP_TYPE__wsdd__Probe (70)
typedef struct wsdd__ProbeType _wsdd__Probe;
#endif

/// @brief "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":ProbeType is a complexType.
///
/// struct wsdd__ProbeType operations:
/// - wsdd__ProbeType* soap_new_wsdd__ProbeType(struct soap*, int num) allocate and default initialize one or more values (array)
/// - soap_default_wsdd__ProbeType(struct soap*, wsdd__ProbeType*) default initialize members
/// - int soap_read_wsdd__ProbeType(struct soap*, wsdd__ProbeType*) deserialize from a source
/// - int soap_write_wsdd__ProbeType(struct soap*, wsdd__ProbeType*) serialize to a sink
/// - wsdd__ProbeType* soap_dup_wsdd__ProbeType(struct soap*, wsdd__ProbeType* dst, wsdd__ProbeType *src) returns deep copy of wsdd__ProbeType src into dst, copies the (cyclic) graph structure when a context is provided, or (cycle-pruned) tree structure with soap_set_mode(soap, SOAP_XML_TREE) (use soapcpp2 -Ec)
/// - soap_del_wsdd__ProbeType(wsdd__ProbeType*) deep deletes wsdd__ProbeType data members, use only on dst after soap_dup_wsdd__ProbeType(NULL, wsdd__ProbeType *dst, wsdd__ProbeType *src) (use soapcpp2 -Ed)
struct wsdd__ProbeType
{
/// Element reference "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01:""http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":Types.
    wsdd__QNameListType                  Types                          0;	///< Optional element.
/// Element reference "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01:""http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":Scopes.
    struct wsdd__ScopesType*             Scopes                         0;	///< Optional element.
/// @todo <any namespace="##other" minOccurs="0" maxOccurs="unbounded">
/// @todo Schema extensibility is user-definable.
///       Consult the protocol documentation to change or insert declarations.
///       Use wsdl2h option -x to remove this element.
///       Use wsdl2h option -d for xsd__anyType DOM (soap_dom_element):
///       wsdl2h maps xsd:any to xsd__anyType, use typemap.dat to remap.
/// @todo <anyAttribute namespace="##other">.
/// @todo Schema extensibility is user-definable.
///       Consult the protocol documentation to change or insert declarations.
///       Use wsdl2h option -x to remove this attribute.
///       Use wsdl2h option -d for xsd__anyAttribute DOM (soap_dom_attribute).
};

/// @brief "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":ProbeMatchesType is a complexType.
///
/// struct wsdd__ProbeMatchesType operations:
/// - wsdd__ProbeMatchesType* soap_new_wsdd__ProbeMatchesType(struct soap*, int num) allocate and default initialize one or more values (array)
/// - soap_default_wsdd__ProbeMatchesType(struct soap*, wsdd__ProbeMatchesType*) default initialize members
/// - int soap_read_wsdd__ProbeMatchesType(struct soap*, wsdd__ProbeMatchesType*) deserialize from a source
/// - int soap_write_wsdd__ProbeMatchesType(struct soap*, wsdd__ProbeMatchesType*) serialize to a sink
/// - wsdd__ProbeMatchesType* soap_dup_wsdd__ProbeMatchesType(struct soap*, wsdd__ProbeMatchesType* dst, wsdd__ProbeMatchesType *src) returns deep copy of wsdd__ProbeMatchesType src into dst, copies the (cyclic) graph structure when a context is provided, or (cycle-pruned) tree structure with soap_set_mode(soap, SOAP_XML_TREE) (use soapcpp2 -Ec)
/// - soap_del_wsdd__ProbeMatchesType(wsdd__ProbeMatchesType*) deep deletes wsdd__ProbeMatchesType data members, use only on dst after soap_dup_wsdd__ProbeMatchesType(NULL, wsdd__ProbeMatchesType *dst, wsdd__ProbeMatchesType *src) (use soapcpp2 -Ed)
struct wsdd__ProbeMatchesType
{
/// Size of array of struct wsdd__ProbeMatchType* is 0..unbounded.
   $int                                  __sizeProbeMatch               0;
/// Array struct wsdd__ProbeMatchType* of size 0..unbounded.
    struct wsdd__ProbeMatchType*         ProbeMatch                     0;
/// @todo <any namespace="##other" minOccurs="0" maxOccurs="unbounded">
/// @todo Schema extensibility is user-definable.
///       Consult the protocol documentation to change or insert declarations.
///       Use wsdl2h option -x to remove this element.
///       Use wsdl2h option -d for xsd__anyType DOM (soap_dom_element):
///       wsdl2h maps xsd:any to xsd__anyType, use typemap.dat to remap.
/// @todo <anyAttribute namespace="##other">.
/// @todo Schema extensibility is user-definable.
///       Consult the protocol documentation to change or insert declarations.
///       Use wsdl2h option -x to remove this attribute.
///       Use wsdl2h option -d for xsd__anyAttribute DOM (soap_dom_attribute).
};

/// @brief "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":ProbeMatchType is a complexType.
///
/// struct wsdd__ProbeMatchType operations:
/// - wsdd__ProbeMatchType* soap_new_wsdd__ProbeMatchType(struct soap*, int num) allocate and default initialize one or more values (array)
/// - soap_default_wsdd__ProbeMatchType(struct soap*, wsdd__ProbeMatchType*) default initialize members
/// - int soap_read_wsdd__ProbeMatchType(struct soap*, wsdd__ProbeMatchType*) deserialize from a source
/// - int soap_write_wsdd__ProbeMatchType(struct soap*, wsdd__ProbeMatchType*) serialize to a sink
/// - wsdd__ProbeMatchType* soap_dup_wsdd__ProbeMatchType(struct soap*, wsdd__ProbeMatchType* dst, wsdd__ProbeMatchType *src) returns deep copy of wsdd__ProbeMatchType src into dst, copies the (cyclic) graph structure when a context is provided, or (cycle-pruned) tree structure with soap_set_mode(soap, SOAP_XML_TREE) (use soapcpp2 -Ec)
/// - soap_del_wsdd__ProbeMatchType(wsdd__ProbeMatchType*) deep deletes wsdd__ProbeMatchType data members, use only on dst after soap_dup_wsdd__ProbeMatchType(NULL, wsdd__ProbeMatchType *dst, wsdd__ProbeMatchType *src) (use soapcpp2 -Ed)
struct wsdd__ProbeMatchType
{
/// Imported element reference "http://www.w3.org/2005/08/addressing":EndpointReference.
    _wsa__EndpointReference              wsa__EndpointReference         1;	///< Required element.
/// Element reference "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01:""http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":Types.
    wsdd__QNameListType                  Types                          0;	///< Optional element.
/// Element reference "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01:""http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":Scopes.
    struct wsdd__ScopesType*             Scopes                         0;	///< Optional element.
/// Element reference "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01:""http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":XAddrs.
    wsdd__UriListType                    XAddrs                         0;	///< Optional element.
/// Element reference "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01:""http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01":MetadataVersion.
    unsigned int                         MetadataVersion                1;	///< Required element.
/// @todo <any namespace="##other" minOccurs="0" maxOccurs="unbounded">
/// @todo Schema extensibility is user-definable.
///       Consult the protocol documentation to change or insert declarations.
///       Use wsdl2h option -x to remove this element.
///       Use wsdl2h option -d for xsd__anyType DOM (soap_dom_element):
///       wsdl2h maps xsd:any to xsd__anyType, use typemap.dat to remap.
/// @todo <anyAttribute namespace="##other">.
/// @todo Schema extensibility is user-definable.
///       Consult the protocol documentation to change or insert declarations.
///       Use wsdl2h option -x to remove this attribute.
///       Use wsdl2h option -d for xsd__anyAttribute DOM (soap_dom_attribute).
};

2.函数

定义在client\application\soapClient.cpp

soap_send___wsdd__Probe();  	// 向组播地址广播Probe消息
SOAP_FMAC5 int SOAP_FMAC6 soap_send___wsdd__Probe(struct soap *soap, const char *soap_endpoint, const char *soap_action, struct wsdd__ProbeType *wsdd__Probe)
{	struct __wsdd__Probe soap_tmp___wsdd__Probe;
	if (soap_action == NULL)
		soap_action = "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe";
	soap_tmp___wsdd__Probe.wsdd__Probe = wsdd__Probe;
	soap_begin(soap);
	soap_set_version(soap, 2); /* use SOAP1.2 */
	soap->encodingStyle = NULL; /* use SOAP literal style */
	soap_serializeheader(soap);
	soap_serialize___wsdd__Probe(soap, &soap_tmp___wsdd__Probe);
	if (soap_begin_count(soap))
		return soap->error;
	if ((soap->mode & SOAP_IO_LENGTH))
	{	if (soap_envelope_begin_out(soap)
		 || soap_putheader(soap)
		 || soap_body_begin_out(soap)
		 || soap_put___wsdd__Probe(soap, &soap_tmp___wsdd__Probe, "-wsdd:Probe", "")
		 || soap_body_end_out(soap)
		 || soap_envelope_end_out(soap))
			 return soap->error;
	}
	if (soap_end_count(soap))
		return soap->error;
	if (soap_connect(soap, soap_endpoint, soap_action)
	 || soap_envelope_begin_out(soap)
	 || soap_putheader(soap)
	 || soap_body_begin_out(soap)
	 || soap_put___wsdd__Probe(soap, &soap_tmp___wsdd__Probe, "-wsdd:Probe", "")
	 || soap_body_end_out(soap)
	 || soap_envelope_end_out(soap)
	 || soap_end_send(soap))
		return soap_closesock(soap);
	return SOAP_OK;
}

五、功能实现

/************************************************************************
**函数:ONVIF_init_ProbeType
**功能:初始化探测设备的范围和类型
**参数:
        [in]  soap  - soap环境变量
        [out] probe - 填充要探测的设备范围和类型
**返回:
        0表明探测到,非0表明未探测到
**备注:
    1). 在本函数内部通过ONVIF_soap_malloc分配的内存,将在ONVIF_soap_delete中被释放
************************************************************************/
static void ONVIF_init_ProbeType(struct soap *soap, struct wsdd__ProbeType *probe)
{
    struct wsdd__ScopesType *scope = nullptr;                                      // 用于描述查找哪类的Web服务

            SOAP_ASSERT(nullptr != soap);
            SOAP_ASSERT(nullptr != probe);

    scope = (struct wsdd__ScopesType *)ONVIF_soap_malloc(soap, sizeof(struct wsdd__ScopesType));
    soap_default_wsdd__ScopesType(soap, scope);                                 // 设置寻找设备的范围
    scope->__item = (char*)ONVIF_soap_malloc(soap, strlen(SOAP_ITEM) + 1);
    strcpy(scope->__item, SOAP_ITEM);

    memset(probe, 0x00, sizeof(struct wsdd__ProbeType));
    soap_default_wsdd__ProbeType(soap, probe);
    probe->Scopes = scope;
    probe->Types  = (char*)ONVIF_soap_malloc(soap, strlen(SOAP_TYPES) + 1);     // 设置寻找设备的类型
    strcpy(probe->Types, SOAP_TYPES);
}

struct OnvifAvailableDevice
{
    int availableNum;
    std::string AvailableDeviceXAddr[256];
};

void ONVIF_DeviceDiscovery(struct OnvifAvailableDevice *availableDevice)
{
    int i;
    int result = 0;
    unsigned int count = 0;                                                     // 搜索到的设备个数
    struct soap *soap = nullptr;                                                   // soap环境变量
    struct wsdd__ProbeType      req;                                            // 用于发送Probe消息
    struct __wsdd__ProbeMatches rep;                                            // 用于接收Probe应答
    struct wsdd__ProbeMatchType *probeMatch;

            SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));

    ONVIF_init_header(soap);                                                    // 设置消息头描述
    ONVIF_init_ProbeType(soap, &req);                                           // 设置寻找的设备的范围和类型
    result = soap_send___wsdd__Probe(soap, SOAP_MCAST_ADDR, nullptr, &req);        // 向组播地址广播Probe消息

#if DEBUG
    std::cout << "--------------------ONVIF_DeviceDiscovery--------------------" << "\n";
#endif

    while (SOAP_OK == result)                                                   // 开始循环接收设备发送过来的消息
    {
        memset(&rep, 0x00, sizeof(rep));
        result = soap_recv___wsdd__ProbeMatches(soap, &rep);
        if (SOAP_OK == result) 
        {
            if (soap->error) 
            {
                soap_perror(soap, "ProbeMatches");
            } else 
            {                                                            // 成功接收到设备的应答消息
                //  printf("__sizeProbeMatch:%d\n",rep.wsdd__ProbeMatches->__sizeProbeMatch);

                if (nullptr != rep.wsdd__ProbeMatches) 
                {
                    count += rep.wsdd__ProbeMatches->__sizeProbeMatch;
                    for(i = 0; i < rep.wsdd__ProbeMatches->__sizeProbeMatch; i++) 
                    {
                        probeMatch = rep.wsdd__ProbeMatches->ProbeMatch + i;
                        availableDevice->AvailableDeviceXAddr[count-1] = probeMatch->XAddrs;
#if DEBUG
                        std::cout << count << "\t" << availableDevice->AvailableDeviceXAddr[count-1] << "\n";                         
#endif
                    }
                }
            }
        } 
        else if (soap->error) 
        {
            break;
        }
    }
    availableDevice->availableNum = count;


#if DEBUG
    SOAP_DBGLOG("\nDetect end! It has detected %d devices!\n", availableDevice->availableNum);
    std::cout << "-------------------------------------------------------------" << "\n";
#endif

    if (nullptr != soap) {
        ONVIF_soap_delete(soap);
    }

}

六、实现效果

--------------------ONVIF_DeviceDiscovery--------------------
1       http://192.168.1.132:2000/onvif/device_service
2       http://192.168.1.67:1936/onvif/device_service
3       http://192.168.1.102:2000/onvif/device_service
4       http://192.168.5.168:2000/onvif/device_service
5       http://192.168.1.71:2000/onvif/device_service
6       http://192.168.5.168:2000/onvif/device_service
7       http://192.168.1.60:2000/onvif/device_service
8       http://192.168.1.128:2000/onvif/device_service
9       http://192.168.1.107:2000/onvif/device_service
10      http://192.168.1.110:5000/onvif/device_service
11      http://192.168.1.98:5000/onvif/device_service
12      http://192.168.1.117:2000/onvif/device_service
13      http://192.168.1.30:2000/onvif/device_service
14      http://192.168.1.65:5000/onvif/device_service
15      http://192.168.5.168:2000/onvif/device_service
16      http://192.168.1.137:81/onvif/device_service
17      http://192.168.1.75:2000/onvif/device_service
18      http://192.168.1.85:2000/onvif/device_service
19      http://192.168.1.68:2000/onvif/device_service
20      http://192.168.1.40:2000/onvif/device_service
21      http://192.168.1.206:2000/onvif/device_service
22      http://192.168.1.126:81/onvif/device_service

Detect end! It has detected 22 devices!
-------------------------------------------------------------

现在我们就可以查看可以连接设备的IP地址了!!!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐