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

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

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

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

在这里插入图片描述

如何获取WSDL文件

有两种方法:

  • 我们可以在这里, onvif官网下载需要的wsdl文件到本地
    • remotediscovery.wsdl :用于发现设备
    • devicemgmt.wsdl :用于获取设备参数
    • media.wsdl:用于获取视频流地址
    • ptz.wsdl:用于设备的PTZ控制
  • 也可以使用命令远程链接相关文件。

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

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

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

  • 在上面我的安装目录是:/home/oceanstar/workspace/cpp/Onvif/gsoap
  • 在编译成功后,在该路径下会生成如下目录
    在这里插入图片描述
  • 在bin目录里有如下两个工具
    • wsdl2h:将wsdl协议文件生成头文件 ./wsdl2h -h 查看帮助
    • soapcpp2:根据上面的头文件生成源文件C/C++代码

一个例子: 设备搜索

要访问一个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快速生成代码。

实践

方法1

代码编写

WS-Discovery的Ad hoc模式,使用多播(不使用gSOAP实现设备发现)

  • 这是客户端

  • 设备发现的多播地址为239.255.255.250,端口3702。

  • 从技术层面来说,通过单播、多播、广播三种方式都能探测到IPC,但多播最具实用性。单播得预先知道IPC的地址(那还搜索啥子嘛),没有实用性。多播是ONVIF规定的方式,能搜多到多播组内的所有IPC。广播能搜索到局域网内的所有IPC,但涉及广播风暴的问题,不推荐。

  • const char *probe变量的内容,即探测消息(Probe)的内容,是ONVIF Device Test Tool 15.06工具搜索IPC时通过Wireshark抓包工具抓包到的。
    在这里插入图片描述

  • 从实际执行结果来看,探测到的应答信息都是一堆SOAP协议数据包,一堆XML要自己解析,实用性极差,所以这种方式知道下就好,不要在项目使用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef WIN32
#include <winsock.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#endif

/* 从技术层面来说,通过单播、多播、广播三种方式都能探测到IPC,但多播最具实用性*/
#define COMM_TYPE_UNICAST         1                                             // 单播
#define COMM_TYPE_MULTICAST       2                                             // 多播
#define COMM_TYPE_BROADCAST       3                                             // 广播
#define COMM_TYPE                 COMM_TYPE_MULTICAST

/* 发送探测消息(Probe)的目标地址、端口号 */
#if COMM_TYPE == COMM_TYPE_UNICAST
#define CAST_ADDR "100.100.100.15"                                          // 单播地址,预先知道的IPC地址
#elif COMM_TYPE == COMM_TYPE_MULTICAST
#define CAST_ADDR "239.255.255.250"                                         // 多播地址,固定的239.255.255.250
#elif COMM_TYPE == COMM_TYPE_BROADCAST
#define CAST_ADDR "100.100.100.255"                                         // 广播地址
#endif

#define CAST_PORT 3702                                                          // 端口号

/* 以下几个宏是为了socket编程能够跨平台,这几个宏是从gsoap中拷贝来的 */
#ifndef SOAP_SOCKET
# ifdef WIN32
#  define SOAP_SOCKET SOCKET
#  define soap_closesocket(n) closesocket(n)
# else
#  define SOAP_SOCKET int
#  define soap_closesocket(n) close(n)
# endif
#endif

#if defined(_AIX) || defined(AIX)
# if defined(_AIX43)
#  define SOAP_SOCKLEN_T socklen_t
# else
#  define SOAP_SOCKLEN_T int
# endif
#elif defined(SOCKLEN_T)
# define SOAP_SOCKLEN_T SOCKLEN_T
#elif defined(__socklen_t_defined) || defined(_SOCKLEN_T) || defined(CYGWIN) || defined(FREEBSD) || defined(__FreeBSD__) || defined(OPENBSD) || defined(__QNX__) || defined(QNX) || defined(OS390) || defined(__ANDROID__) || defined(_XOPEN_SOURCE)
# define SOAP_SOCKLEN_T socklen_t
#elif defined(IRIX) || defined(WIN32) || defined(__APPLE__) || defined(SUN_OS) || defined(OPENSERVER) || defined(TRU64) || defined(VXWORKS) || defined(HP_UX)
# define SOAP_SOCKLEN_T int
#elif !defined(SOAP_SOCKLEN_T)
# define SOAP_SOCKLEN_T size_t
#endif

#ifdef WIN32
#define SLEEP(n)    Sleep(1000 * (n))
#else
#define SLEEP(n)    sleep((n))
#endif

/* 探测消息(Probe),这些内容是ONVIF Device Test Tool 15.06工具搜索IPC时的Probe消息,通过Wireshark抓包工具抓包到的 */
const char *probe = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Envelope xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\" xmlns=\"http://www.w3.org/2003/05/soap-envelope\"><Header><wsa:MessageID xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\">uuid:fc0bad56-5f5a-47f3-8ae2-c94a4e907d70</wsa:MessageID><wsa:To xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To><wsa:Action xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action></Header><Body><Probe xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\"><Types>dn:NetworkVideoTransmitter</Types><Scopes /></Probe></Body></Envelope>";

int main(int argc, char **argv)
{
    int ret;
    int optval;
    SOAP_SOCKET s;
    SOAP_SOCKLEN_T len;
    char recv_buff[4096] = {0};
    struct sockaddr_in multi_addr;
    struct sockaddr_in client_addr;

#ifdef WIN32
    WSADATA wsaData;
    if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 ) {                             // 初始化Windows Sockets DLL
        printf("Could not open Windows connection.\n");
        return 0;
    }
    if ( LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2 ) {
        printf("the version of WinSock DLL is not 2.2.\n");
        return 0;
    }
#endif

    s = socket(AF_INET, SOCK_DGRAM, 0);                                         // 建立数据报套接字
    if (s < 0) {
        perror("socket error");
        return -1;
    }

#if COMM_TYPE == COMM_TYPE_BROADCAST
    optval = 1;
    ret = setsockopt(s, SOL_SOCKET, SO_BROADCAST, (const char*)&optval, sizeof(int));
#endif

    multi_addr.sin_family = AF_INET;                                            // 搜索IPC:使用UDP向指定地址发送探测消息(Probe)
    multi_addr.sin_port = htons(CAST_PORT);
    multi_addr.sin_addr.s_addr = inet_addr(CAST_ADDR);
    ret = sendto(s, probe, strlen(probe), 0, (struct sockaddr*)&multi_addr, sizeof(multi_addr));
    if (ret < 0) {
        soap_closesocket(s);
        perror("sendto error");
        return -1;
    }
    printf("Send Probe message to [%s:%d]\n\n", CAST_ADDR, CAST_PORT);
    SLEEP(1);

    for (;;) {                                                                  // 接收IPC的应答消息(ProbeMatch)
        len = sizeof(client_addr);
        memset(recv_buff, 0, sizeof(recv_buff));
        memset(&client_addr, 0, sizeof(struct sockaddr));
        ret = recvfrom(s, recv_buff, sizeof(recv_buff) - 1, 0, (struct sockaddr*)&client_addr, &len);
        printf("===Recv ProbeMatch from [%s:%d]===\n%s\n\n",  inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), recv_buff);
        SLEEP(1);
    }
    soap_closesocket(s);

    return 0;
}

Linux编译: gcc search_c.c,然后运行(必须先关闭防火墙),效果如下。
在这里插入图片描述

抓包分析

抓包
1、打开终端,因此执行下面命令

tcpdump -i ens33  host 239.255.255.250 -nn -w discovery.pcap
# -i ens33表示抓取ens33网口的数据包
# host表示主机过滤,抓取对应IP的数据包
# -nn 表示不解析IP地址和端口号的名称
# -w ping.pcap:表示将抓取到的数据包保存到discovery.pcap文件中

2、运行上面的可执行文件

3、把 discovery.pcap 文件拖到电脑,再用 Wireshark 打开它。
在这里插入图片描述
分析
从上图可以看出,一共用了两种协议SSDPUDP

方法2

1、生成头文件

onvif代码都是靠工具生成的,我们得根据我们的要求生成头文件,onvif是根据模块来生成头文件,需要的模块多,生成的代码就庞大,相反则很少,我们接下来使用onvif官方提供的wsdl来生成代码

我们可以先建立一个目录,这里我们要实现的功能是设备发现,所以叫做onvif-discover。 该目录下有如下目录
在这里插入图片描述

  • bin:放置上面生成的两个工具,注意要加可执行权限 chmod +x wsdl2h soapcpp2
    在这里插入图片描述
  • gsoap:
    • 上面编译后的gsoap文件夹,其路径为 /home/oceanstar/workspace/cpp/Onvif/gsoap/share ,
    • 此外,我们还需要将gsoap源代码(/home/oceanstar/workspace/cpp/gsoap-2.8/gsoap)下的几个文件拷到我们的gsoap目录下,dom.c stdsoap2.c stdsoap2.h,stdsoap2.cpp
      • stdsoap2.c/cpp文件时运行时的c/c++库,带HTTP/SOAP解析器和运行时支持历程。通过这两个文件和上述的框架代码,就可以开发客户端/服务器端代码了
        在这里插入图片描述
        目录创建好了,我们就可以生成头文件了
        可以直接执行命令,我觉得编解脚本比较方便,脚本名字就叫onvif_head.sh 内容如下
#!/bin/bash
mkdir onvif_head
cd onvif_head
../bin/wsdl2h -o onvif.h   -t ../gsoap/WS/typemap.dat \
http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl

解释:

  • -o onvif.h:为生成onvif.h头文件
  • -t:指定使用的dat文件,就是当前目录的gosap/WS目录下
  • http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl 为 remotediscovery的wsdl文件,我们这里需要的功能不多,用不到很多的的wsdl文件,所以直接就官网链接,如果多了的话,就下载下来,因为官网速度太慢
    在这里插入图片描述

执行上面的脚本

onvif-discover]$ chmod 777 onvif_head.sh
onvif-discover]$ $ ./onvif_head.sh 
Saving onvif.h


**  The gSOAP WSDL/WADL/XSD processor for C and C++, wsdl2h release 2.8.117
**  Copyright (C) 2000-2021 Robert van Engelen, Genivia Inc.
**  All Rights Reserved. This product is provided "as is", without any warranty.
**  The wsdl2h tool and its generated software are released under the GPL.
**  ----------------------------------------------------------------------------
**  A commercial use license is available from Genivia Inc., contact@genivia.com
**  ----------------------------------------------------------------------------

Reading type definitions from type map "../gsoap/WS/typemap.dat"
Connecting to 'http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl' to retrieve WSDL/WADL or XSD... connected, receiving...
Done reading 'http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl'

Warning: 2 service bindings found, but collected as one service (use option -Nname to produce a separate service for each binding)

To finalize code generation, execute:
> soapcpp2 onvif.h
Or to generate C++ proxy and service classes:
> soapcpp2 -j onvif.h

然后ls可以看到生成了一个叫做onvif_head,里面生成了一个头文件onvif.h:

[  onvif-discover]$ ls
bin  gsoap  onvif_head  onvif_head.sh
[  onvif-discover]$ cd onvif_head/
[  onvif-discover/onvif_head]$ ls
onvif.h

生成源代码

接下来需要根据生成的头文件生成源文件,需要使用soapcpp2工具。

(1)编写脚本gen_code.sh

[ onvif-discover]$ ls
bin  gencode.sh  gsoap  onvif_head  onvif_head.sh
[ onvif-discover]$ cat gencode.sh
#!/bin/bash
DIR=soap
mkdir $DIR
cd $DIR
 ../bin/soapcpp2 -2 -x  ../onvif_head/onvif.h  -L -I ../gsoap/import -I ../gsoap/

解释一下

  • -x: 不生产xml文件
  • -2:生成SOAP 1.2版本代码
  • -L:不生成soapClientLib/soapServerLib文件,这里的代码比较简单,我们就使用这几个代码,也是为了简单方便,不过之后正式的工程为了规范我觉得还是使用比较好
  • -H:指定头文件目录

(2)执行脚本

onvif-discover]$ ls
bin  gencode.sh  gsoap  onvif_head  onvif_head.sh
onvif-discover]$ chmod 777 gencode.sh
onvif-discover]$ ./gencode.sh 

**  The gSOAP code generator for C and C++, soapcpp2 release 2.8.117
**  Copyright (C) 2000-2021, Robert van Engelen, Genivia Inc.
**  All Rights Reserved. This product is provided "as is", without any warranty.
**  The soapcpp2 tool and its generated software are released under the GPL.
**  ----------------------------------------------------------------------------
**  A commercial use license is available from Genivia Inc., contact@genivia.com
**  ----------------------------------------------------------------------------


soap12.h(54): *WARNING*: option -1 or -2 overrides SOAP-ENV namespace


soap12.h(55): *WARNING*: option -1 or -2 overrides SOAP-ENC namespace

Saving soapStub.h annotated copy of the source interface header file
Saving soapH.h serialization functions to #include in projects
Using wsdd service name: wsdd
Using wsdd service style: document
Using wsdd service encoding: literal
Using wsdd schema import namespace: http://schemas.xmlsoap.org/ws/2005/04/discovery
Saving wsdd.nsmap namespace mapping table
Using tdn service name: RemoteDiscoveryBinding
Using tdn service style: document
Using tdn service encoding: literal
Using tdn schema namespace: http://www.onvif.org/ver10/network/wsdl
Saving RemoteDiscoveryBinding.nsmap namespace mapping table
Saving soapClient.cpp client call stub functions
Saving soapServer.cpp server request dispatcher
Saving soapC.cpp serialization functions

Compilation successful (2 warnings)
$ ls
bin  gencode.sh  gsoap  onvif_head  onvif_head.sh  soap

可以看到生成了一个叫做soap的目录,进入进入目录,可以看到生成了各种文件:

onvif-discover]$ ls
bin  gencode.sh  gsoap  onvif_head  onvif_head.sh  soap
onvif-discover]$ cd soap/
$ ls  -l
总用量 632
-rw-rw-r--. 1 oceanstar oceanstar    901 1116 15:28 RemoteDiscoveryBinding.nsmap
-rw-rw-r--. 1 oceanstar oceanstar 284396 1116 15:28 soapC.cpp
-rw-rw-r--. 1 oceanstar oceanstar  22639 1116 15:28 soapClient.cpp
-rw-rw-r--. 1 oceanstar oceanstar 242157 1116 15:28 soapH.h
-rw-rw-r--. 1 oceanstar oceanstar  11539 1116 15:28 soapServer.cpp
-rw-rw-r--. 1 oceanstar oceanstar  66528 1116 15:28 soapStub.h
-rw-rw-r--. 1 oceanstar oceanstar    901 1116 15:28 wsdd.nsmap

  • 各种nsmap文件:命名空间,除了名字不一样,内容是一样的,里面的内容竟然是每一个xml文件里的Envelope字段内容。我们只需要留下一个就可以了,并将之改名为wsdd.nsmap
  • soapC.c:指定数据结构的序列化和反序列化
  • soapClient.c:客户端代码
  • soapH.h:主头文件,所有客户机和服务器源代码都要包括他
  • soapServer.c:服务器端代码
  • soapStub.h:从输入头文件(onvif.h)生成的经过修改且带命名空间前缀的头文件

如果不选择-x,还会生成

  • 各种xml文件:一个xml文件就是一个soap消息

编写代码

现在我们的需要的代码并不多,并且现在是实例代码,为了方便,我们将所有需要的代码放到同一个目录中。我们在刚才建立的onvif-discover目录下创建一个application目录,存放我们需要的所有代码。

(1)从刚刚生成的soap目录下拷贝所有的*wsdd.nsmap.c.h和onvif_head目录下的头文件onvif.h

onvif-discover/application]$ ls -l
总用量 652
-rw-rw-r--. 1 oceanstar oceanstar  24473 1116 15:23 onvif.h
-rw-rw-r--. 1 oceanstar oceanstar 284396 1116 15:28 soapC.cpp
-rw-rw-r--. 1 oceanstar oceanstar  22639 1116 15:28 soapClient.cpp
-rw-rw-r--. 1 oceanstar oceanstar 242157 1116 15:28 soapH.h
-rw-rw-r--. 1 oceanstar oceanstar  11539 1116 15:28 soapServer.cpp
-rw-rw-r--. 1 oceanstar oceanstar  66528 1116 15:28 soapStub.h
-rw-rw-r--. 1 oceanstar oceanstar    901 1116 15:28 wsdd.nsmap

(2)然后我们创建一个CMakeLists.txt,然后用CLion打开这个工程。
编写CMakeLists.txt

application]$ ls
CMakeLists.txt  soapC.cpp       soapH.h         soapStub.h
onvif.h         soapClient.cpp  soapServer.cpp  wsdd.nsmap

application]$ cat CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(onvif-discover)

set(CMAKE_CXX_STANDARD 11)

add_definitions("-Wall -g")
aux_source_directory(. SRC_LIST)
add_executable(onvif-discover  ${SRC_LIST} )

(3)在application创建一个build目录,进入build目录之后编译:

application]$ mkdir build
application]$ cd build/
application]$ cmake ..
application]$ make

发现报错
在这里插入图片描述
解决方法:将gsoap-2.8/gsoap/目录下的stdsoap2.cppstdsoap2.h复制到当前目录下

application]$ ls -l
总用量 1440
drwxrwxr-x. 3 oceanstar oceanstar     89 1116 15:53 build
-rw-rw-r--. 1 oceanstar oceanstar    196 1116 15:33 CMakeLists.txt
-rw-rw-r--. 1 oceanstar oceanstar  24473 1116 15:23 onvif.h
-rw-rw-r--. 1 oceanstar oceanstar 284396 1116 15:28 soapC.cpp
-rw-rw-r--. 1 oceanstar oceanstar  22639 1116 15:28 soapClient.cpp
-rw-rw-r--. 1 oceanstar oceanstar 242157 1116 15:28 soapH.h
-rw-rw-r--. 1 oceanstar oceanstar  11539 1116 15:28 soapServer.cpp
-rw-rw-r--. 1 oceanstar oceanstar  66528 1116 15:28 soapStub.h
-rw-r--r--. 1 oceanstar oceanstar 640612 820 03:45 stdsoap2.cpp
-rw-r--r--. 1 oceanstar oceanstar 156586 820 03:45 stdsoap2.h
-rw-rw-r--. 1 oceanstar oceanstar    901 1116 15:28 wsdd.nsmap

重新编译:

application]$ cd build/
build]$ cmake ..
build]$ make

在这里插入图片描述
这些未定义的引用就是我们需要实现的功能。

服务端

创建一个deivceserver.cpp,将刚刚所有未定义的实现:

#include "stdsoap2.h"
#include "soapStub.h"

/** Web service one-way operation 'SOAP_ENV__Fault' implementation, should return value of soap_send_empty_response() to send HTTP Accept acknowledgment, or return an error code, or return SOAP_OK to immediately return without sending an HTTP response message */
SOAP_FMAC5 int SOAP_FMAC6 SOAP_ENV__Fault(struct soap*soap, char *faultcode, char *faultstring,
                                          char *faultactor, struct SOAP_ENV__Detail *detail,
                                          struct SOAP_ENV__Code *SOAP_ENV__Code,
                                          struct SOAP_ENV__Reason *SOAP_ENV__Reason,
                                          char *SOAP_ENV__Node, char *SOAP_ENV__Role,
                                          struct SOAP_ENV__Detail *SOAP_ENV__Detail)
{
    printf("SOAP_ENV__Fault %s, %d\n", __FUNCTION__, __LINE__);

    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __wsdd__Hello(struct soap*, struct wsdd__HelloType *wsdd__Hello){
    printf("__wsdd__Hello %s, %d\n", __FUNCTION__, __LINE__);
    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __wsdd__Bye(struct soap*, struct wsdd__ByeType *wsdd__Bye){
    printf("__wsdd__Bye %s, %d\n", __FUNCTION__, __LINE__);
    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __wsdd__Probe(struct soap*, struct wsdd__ProbeType *wsdd__Probe){
    printf("__wsdd__Probe %s, %d\n", __FUNCTION__, __LINE__);
    								 
    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __wsdd__ProbeMatches(struct soap*, struct wsdd__ProbeMatchesType *wsdd__ProbeMatches){
    printf("__wsdd__ProbeMatches %s, %d\n", __FUNCTION__, __LINE__);
    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __wsdd__Resolve(struct soap*, struct wsdd__ResolveType *wsdd__Resolve){
    printf("__wsdd__Resolve %s, %d\n", __FUNCTION__, __LINE__);
    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __wsdd__ResolveMatches(struct soap*, struct wsdd__ResolveMatchesType *wsdd__ResolveMatches){
    printf("__wsdd__ResolveMatches %s, %d\n", __FUNCTION__, __LINE__);
    return 0;
}

SOAP_FMAC5 int SOAP_FMAC6 __tdn__Hello(struct soap*, struct wsdd__HelloType tdn__Hello, struct wsdd__ResolveType &tdn__HelloResponse)
{
    printf("__tdn__Hello %s, %d\n", __FUNCTION__, __LINE__);

    return 0;
}
/** Web service operation '__tdn__Bye' implementation, should return SOAP_OK or error code */
SOAP_FMAC5 int SOAP_FMAC6 __tdn__Bye(struct soap*, struct wsdd__ByeType tdn__Bye, struct wsdd__ResolveType &tdn__ByeResponse)
{
    printf("__tdn__Bye %s, %d\n", __FUNCTION__, __LINE__);

    return 0;
}
/** Web service operation '__tdn__Probe' implementation, should return SOAP_OK or error code */
SOAP_FMAC5 int SOAP_FMAC6 __tdn__Probe(struct soap*, struct wsdd__ProbeType tdn__Probe, struct wsdd__ProbeMatchesType &tdn__ProbeResponse)
{
    printf("__tdn__Bye %s, %d\n", __FUNCTION__, __LINE__);
    return 0;
}

然后重新编译:

cmake ..
make

在这里插入图片描述
两个错误:一个是/home/oceanstar/workspace/cpp/Onvif/onvif-discover/application/stdsoap2.cpp:12228:对‘namespaces’未定义的引用

解决方法是,在deivceserver.cpp引入头文件

#include "wsdd.nsmap"

错误2:(.text+0x20):对‘main’未定义的引用。 原因是没有实现main函数。解决方法,在在deivceserver.cpp编写main函数:

int main(int argc, char **argv){
    int m, s;
    
    struct ip_mreq mcast;

    struct soap probe_soap;
    soap_init2(&probe_soap, SOAP_IO_UDP|SOAP_IO_FLUSH, SOAP_IO_UDP|SOAP_IO_FLUSH); //指定创建UDP
    probe_soap.bind_flags = SO_REUSEADDR;  // 让端口释放后立即就可以被再次使用
    soap_set_namespaces(&probe_soap, namespaces);
    if(!soap_valid_socket(soap_bind(&probe_soap, NULL, 3702, 10)))
    {
        soap_print_fault(&probe_soap, stderr);
        exit(1);
    }


    mcast.imr_multiaddr.s_addr = inet_addr("239.255.255.250"); //多播组
    mcast.imr_interface.s_addr = htonl(INADDR_ANY); //加入的客户端主机IP,这里设置为INADDR_ANY表示所有ip
    if(setsockopt(probe_soap.master, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mcast, sizeof(mcast)) < 0)
    {
        printf("setsockopt error!\n");
        return 0;
    }

    //成功绑定后,开始鉴定
    for(;;)
    {
        if(!soap_valid_socket(soap_accept(&probe_soap)))
        {
            soap_print_fault(&probe_soap, stderr);
            exit(-1);
        }

        fprintf(stderr, "Socket connection successful:slave socket = %d \n", s);

        /* 连接成功后,便开始处理响应请求,下面的函数就是用来处理并响应请求
            封装了所有的处理与响应过程,在该函数中调用了本地所实现的相关web服务
            他所需要的所有请求信息都找soap结构体中
        */
        if (soap_serve(&probe_soap))
            soap_print_fault(&probe_soap, stderr);
        soap_destroy(&probe_soap);	//处理完后,撤销soap环境
        soap_end(&probe_soap);		//清楚所有资源,关闭套接字
    }

    soap_done(&probe_soap);
}

现在我们就已经成功的创建了一个server了。编译运行

在这里插入图片描述

客户端

实际上,在和其他Onvif对接的时候,我们扮演的更多是客户端的角色,摄像头厂商扮演的才是服务器端的角色。

所以在实际开发的时候,我们可以选择不生成服务端的框架,而只生成客户端的框架。

但是我们在上面选择了生成服务端的代码,因此,将deivceserver.cpp中的mian函数以及#include "wsdd.nsmap"注释掉,然后在该项目上重新创建一个deviceprobe.cpp编写客户端的代码

实现一

#include "soapStub.h"
#include "wsdd.nsmap"
#include "soapH.h"


//probe消息仿照 soap_wsdd_Probe 函数编写

int main(int argc, char *argv[])
{
    //soap环境变量
    struct soap *soap;

    //发送消息描述
    struct wsdd__ProbeType req;
    struct wsdd__ProbeType wsdd__Probe;

    struct __wsdd__ProbeMatches resp;

    //描述查找那类的Web消息
    struct wsdd__ScopesType sScope;

    //soap消息头消息
    struct SOAP_ENV__Header  header;

    //获得的设备信息个数
    int count = 0;

    //返回值
    int result = 0;

    //存放uuid 格式(8-4-4-4-12)
    char uuid_string[64];

    printf("%s: %d 000: \n", __FUNCTION__, __LINE__);
    sprintf(uuid_string, "464A4854-4656-5242-4530-110000000000");
    printf("uuid = %s \n", uuid_string);

    //soap初始化,申请空间
    soap = soap_new();
    if(soap == nullptr)
    {
        printf("malloc soap error \n");
        return -1;
    }


    soap_set_namespaces(soap, namespaces);  //设置命名空间,就是xml文件的头
    soap->recv_timeout = 5;  //超出5s没数据就推出,超时时间

    //将header设置为soap消息,头属性,暂且认为是soap和header绑定
    soap_default_SOAP_ENV__Header(soap, &header);
    header.wsa__MessageID = uuid_string;
    header.wsa__To = "urn:schemas-xmlsoap-org:ws:2005:04:discovery";
    header.wsa__Action = "http://schemas.xmllocal_soap.org/ws/2005/04/discovery/Probe";
    //设置soap头消息的ID
    soap->header = &header;

    /* 设置所需寻找设备的类型和范围,二者至少设置一个
        否则可能收到非ONVIF设备,出现异常
     */

    //设置soap消息的请求服务属性
    soap_default_wsdd__ScopesType(soap, &sScope);
    sScope.__item = "onvif://www.onvif.org";
    soap_default_wsdd__ProbeType(soap, &req);
    req.Scopes = &sScope;

    /* 设置所需设备的类型,ns1为命名空间前缀,在wsdd.nsmap 文件中
       {"tdn","http://www.onvif.org/ver10/network/wsdl"}的tdn,如果不是tdn,而是其它,
       例如ns1这里也要随之改为ns1
    */
    req.Types = "ns1:NetworkVideoTransmitter";

    //调用gSoap接口 向 239.255.255.250:3702 发送udp消息
    result = soap_send___wsdd__Probe(soap,  "soap.udp://239.255.255.250:3702/", nullptr, &req);

    if(result == -1)
    {
        printf("soap error: %d, %s, %s \n", soap->error, *soap_faultcode(soap), *soap_faultstring(soap));
        result = soap->error;
    }
    else
    {
        do{
            printf("%s: %d, begin receive probematch... \n", __FUNCTION__, __LINE__);
            printf("count = %d \n", count);

            //接收 ProbeMatches,成功返回0,错误返回-1
            result = soap_recv___wsdd__ProbeMatches(soap, &resp);
            printf(" --soap_recv___wsdd__ProbeMatches() result=%d \n",result);
            if(result == -1)
            {
                printf("Find %d devices!\n", count);
                break;
            }
            else
            {
                //读取服务器回应的Probematch消息
                printf("soap_recv___wsdd__Probe: __sizeProbeMatch = %d \n", resp.wsdd__ProbeMatches->__sizeProbeMatch);
                printf("Target EP Address : %s \n", resp.wsdd__ProbeMatches->ProbeMatch->wsa__EndpointReference.Address);
                printf("Target Type : %s \n", resp.wsdd__ProbeMatches->ProbeMatch->Types);
                printf("Target Service Address : %s \n", resp.wsdd__ProbeMatches->ProbeMatch->XAddrs);
                printf("Target Metadata Version: %d \n", resp.wsdd__ProbeMatches->ProbeMatch->MetadataVersion);
                printf("Target Scope Address : %s \n", resp.wsdd__ProbeMatches->ProbeMatch->Scopes->__item);
                count++;
            }
        }while(true);
    }
    //清除soap
    // clean up and remove deserialized data
    soap_end(soap);
    //detach and free runtime context
    soap_free(soap);

    return result;
}


编译运行如下
在这里插入图片描述

实现二

#include <assert.h>
#include "soapH.h"
#include "wsdd.nsmap"
#include "soapStub.h"


#define SOAP_ASSERT     assert
#define SOAP_DBGLOG     printf
#define SOAP_DBGERR     printf

#define SOAP_TO         "urn:schemas-xmlsoap-org:ws:2005:04:discovery"
#define SOAP_ACTION     "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe"

#define SOAP_MCAST_ADDR "soap.udp://239.255.255.250:3702"                       // onvif规定的组播地址

#define SOAP_ITEM       ""                                                      // 寻找的设备范围
#define SOAP_TYPES      "dn:NetworkVideoTransmitter"                            // 寻找的设备类型

#define SOAP_SOCK_TIMEOUT    (10)               // socket超时时间(单秒秒)

void soap_perror(struct soap *soap, const char *str)
{
    if (nullptr == str) {
        SOAP_DBGERR("[soap] error: %d, %s, %s\n", soap->error, *soap_faultcode(soap), *soap_faultstring(soap));
    } else {
        SOAP_DBGERR("[soap] %s error: %d, %s, %s\n", str, soap->error, *soap_faultcode(soap), *soap_faultstring(soap));
    }
}

void* ONVIF_soap_malloc(struct soap *soap, unsigned int n)
{
    void *p = nullptr;

    if (n > 0) {
        p = soap_malloc(soap, n);
        SOAP_ASSERT(nullptr != p);
        memset(p, 0x00 ,n);
    }
    return p;
}

struct soap *ONVIF_soap_new(int timeout)
{
    struct soap *soap = nullptr;                                                   // soap环境变量

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

    soap_set_namespaces(soap, namespaces);                                      // 设置soap的namespaces
    soap->recv_timeout    = timeout;                                            // 设置超时(超过指定时间没有数据就退出)
    soap->send_timeout    = timeout;
    soap->connect_timeout = timeout;

#if defined(__linux__) || defined(__linux)                                      // 参考https://www.genivia.com/dev.html#client-c的修改:
    soap->socket_flags = MSG_NOSIGNAL;                                          // To prevent connection reset errors
#endif

    soap_set_mode(soap, SOAP_C_UTFSTRING);                                      // 设置为UTF-8编码,否则叠加中文OSD会乱码

    return soap;
}

void ONVIF_soap_delete(struct soap *soap)
{
    soap_destroy(soap);                                                         // remove deserialized class instances (C++ only)
    soap_end(soap);                                                             // Clean up deserialized data (except class instances) and temporary data
    soap_done(soap);                                                            // Reset, close communications, and remove callbacks
    soap_free(soap);                                                            // Reset and deallocate the context created with soap_new or soap_copy
}

/************************************************************************
**函数:ONVIF_init_header
**功能:初始化soap描述消息头
**参数:
        [in] soap - soap环境变量
**返回:无
**备注:
    1). 在本函数内部通过ONVIF_soap_malloc分配的内存,将在ONVIF_soap_delete中被释放
************************************************************************/
void ONVIF_init_header(struct soap *soap)
{
    struct SOAP_ENV__Header *header = nullptr;

    SOAP_ASSERT(nullptr != soap);

    header = (struct SOAP_ENV__Header *)ONVIF_soap_malloc(soap, sizeof(struct SOAP_ENV__Header));
    soap_default_SOAP_ENV__Header(soap, header);
    header->wsa__MessageID = "shsuishsihsishsishsuisbjshsusisuisusi";
    header->wsa__To        = (char*)ONVIF_soap_malloc(soap, strlen(SOAP_TO) + 1);
    header->wsa__Action    = (char*)ONVIF_soap_malloc(soap, strlen(SOAP_ACTION) + 1);
    strcpy(header->wsa__To, SOAP_TO);
    strcpy(header->wsa__Action, SOAP_ACTION);
    soap->header = header;
}

/************************************************************************
**函数:ONVIF_init_ProbeType
**功能:初始化探测设备的范围和类型
**参数:
        [in]  soap  - soap环境变量
        [out] probe - 填充要探测的设备范围和类型
**返回:
        0表明探测到,非0表明未探测到
**备注:
    1). 在本函数内部通过ONVIF_soap_malloc分配的内存,将在ONVIF_soap_delete中被释放
************************************************************************/
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);
}

void ONVIF_DetectDevice(void (*cb)(char *DeviceXAddr))
{
    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消息
    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;
                        if (nullptr != cb) {
                            cb(probeMatch->XAddrs);                             // 使用设备服务地址执行函数回调
                        }
                    }
                }
            }
        } else if (soap->error) {
            break;
        }
    }

    SOAP_DBGLOG("\ndetect end! It has detected %d devices!\n", count);

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

}



int main(int argc, char **argv)
{
    ONVIF_DetectDevice(nullptr);
    return 0;
}

在这里插入图片描述

参考

其他文件

Logo

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

更多推荐