☰
gSOAP 2.8.105 实战:ONVIF 开发从 WSDL 到 SOAP 通信全流程
2026/10/15 17:59:25 网站建设 项目流程

简介:gSOAP 2.8.105 是一套面向 C/C++ 开发者的 SOAP/XML 协议实现工具包,常用于 ONVIF 协议对接、Web 服务端与客户端程序的快速构建。它通过自动生成代码的方式处理 SOAP 数据结构,让开发者无需为适配类库而大幅改动程序结构,适合从事网络摄像机、视频监控、设备互联等方向的中高级 C/C++ 工程师使用。压缩包为 zip 格式,整体约 31.21MB,内含 gSOAP 编译工具及相关源码文件,可用于生成 ONVIF 通信所需的桩代码与框架代码。目前已有 308 人学习下载,说明其在 ONVIF 开发场景中具有一定参考价值。借助该工具包,读者可快速搭建 SOAP 通信环境,理解 ONVIF 设备发现、媒体配置等接口的调用方式,并掌握从 WSDL 到 C/C++ 代码的生成流程,为实际项目中的协议对接与调试提供可复用的基础支撑。

1. 从一份 gsoap 压缩包说起:ONVIF 开发到底需要什么

如果你正在做网络摄像机的 ONVIF 协议对接,大概率绕不开 gSOAP 这个工具链。我手里这份gsoap_2.8.105.zip就是一套完整的 gSOAP 源码发行包,版本号 2.8.105 直接写在文件名里。它本身不是 ONVIF 的 SDK,但 ONVIF 的 WSDL 文件要靠它来生成 C/C++ 的 SOAP 通信框架代码。换句话说,你想让设备端或客户端跑通 ONVIF 的 Device、Media、PTZ 这些服务,第一步就是把 WSDL 喂给 gSOAP 的wsdl2h和soapcpp2,让它吐出可编译的桩代码和骨架代码。这份资源适合两类人:一是刚接手 ONVIF 对接、需要从零搭通信层的嵌入式或后端开发者;二是已经用过 gSOAP 但被版本兼容性和编译参数折腾过的老手,想找一个稳定版本重新梳理流程。下面我按实际拆包和跑通的顺序,把关键步骤和踩过的坑摊开讲。

2. 拆包与工具链就位:wsdl2h 和 soapcpp2 怎么跑起来

拿到gsoap_2.8.105.zip之后,别急着解压完就找 Makefile 一把梭。gSOAP 的目录结构在不同小版本间有细微差别,2.8.105 这套里,核心可执行文件在gsoap/bin/下,但默认可能是给当前平台编译好的,也可能是需要你自己先编一遍。我一般会先确认两件事:wsdl2h和soapcpp2能不能直接执行,以及typemap.dat文件在不在预期位置。

2.1 解压后的目录确认与可执行文件检查

在 Linux 环境下,解压后进入顶层目录,先看bin下面有什么:

unzip gsoap_2.8.105.zip -d gsoap_2.8.105 cd gsoap_2.8.105/gsoap ls bin/ # 常见输出:wsdl2h soapcpp2 soapcpp2_lex.l soapcpp2_yacc.y ...

如果bin/下只有源码没有可执行文件,或者执行./bin/wsdl2h -v报架构不匹配,那就需要先编译工具本身。gSOAP 自举编译不算复杂,但要注意它依赖 flex 和 bison:

# 在 gsoap 目录下 ./configure --prefix=$PWD/out make -j4 make install # 编译完成后,可执行文件在 out/bin/ 下 ./out/bin/wsdl2h -v ./out/bin/soapcpp2 -v

这里--prefix指定安装路径,我习惯装到当前目录的out里,避免污染系统路径。make -j4的并行数按你机器核数改,嵌入式交叉编译环境里通常用-j1更稳。编译成功后,wsdl2h负责把 WSDL 或 XSD 转成 gSOAP 自己的头文件,soapcpp2再把这个头文件转成 C++ 的客户端/服务端代码。两个工具缺一不可,版本必须来自同一个压缩包,混用不同版本的wsdl2h和soapcpp2是后面很多玄学编译错误的根源。

2.2 用 wsdl2h 拉取 ONVIF WSDL 并生成头文件

ONVIF 的 WSDL 文件可以从官方 schema 仓库获取,常见做法是本地保存一份devicemgmt.wsdl、media.wsdl、ptz.wsdl等,然后用wsdl2h合并生成一个统一的.h。我一般会建一个wsdl目录专门放这些文件,再建一个generated目录放输出。

mkdir -p wsdl generated # 假设已经把 ONVIF 的 wsdl 和 xsd 文件放到了 wsdl/ 下 cd generated ../out/bin/wsdl2h -o onvif.h -c -s -t ../gsoap/typemap.dat \ ../wsdl/devicemgmt.wsdl \ ../wsdl/media.wsdl \ ../wsdl/ptz.wsdl

参数逐个说:-o onvif.h指定输出头文件名;-c生成 C 风格代码,如果你用 C++ 可以去掉这个选项,但 ONVIF 场景下 C 风格更常见,因为很多嵌入式端只用 C;-s表示不使用 STL,同样是为了减少依赖;-t ../gsoap/typemap.dat指定类型映射文件,这个文件里定义了xsd:dateTime、xsd:duration等类型怎么映射到 C 的time_t或字符串,不指定的话生成出来的类型可能不是你想要的。执行完你会得到一个onvif.h,里面是 gSOAP 能识别的服务定义。如果wsdl2h报某个命名空间找不到,通常是 WSDL 里 import 的 xsd 文件路径不对,检查wsdl/下是否缺了onvif.xsd或common.xsd。

2.3 soapcpp2 生成桩代码与骨架代码

有了onvif.h,下一步就是让soapcpp2生成实际的通信代码:

../out/bin/soapcpp2 -c -x -I ../gsoap/import onvif.h # 生成文件包括: # soapC.c 序列化/反序列化 # soapClient.c 客户端桩 # soapServer.c 服务端骨架 # soapH.h 头文件 # soapStub.h 存根定义 # *.nsmap 命名空间映射

-c同样表示生成 C 代码;-x表示不生成 XML 解析器相关的额外文件,减小体积;-I ../gsoap/import告诉它去哪里找stlvector.h之类的导入文件。生成出来的soapClient.c和soapServer.c就是你要集成到项目里的核心文件。注意,soapC.c可能非常大,ONVIF 全量 WSDL 生成后几万行是正常的,编译时如果内存不够,可以按服务拆分 WSDL 分别生成,再在应用层做聚合。

3. 把生成的代码接进工程:编译、链接与 SOAP 运行时配置

代码生成只是第一步,真正让 ONVIF 通信跑起来,还要处理编译选项、链接库和运行时参数。这一章我按一个最小可用的客户端程序来走,服务端逻辑类似,只是调用的函数不同。

3.1 编译选项与依赖库的取舍

gSOAP 生成的 C 代码依赖几个东西:标准 C 库、数学库(如果用了浮点序列化)、以及可选的 SSL 库(ONVIF 很多设备要求 HTTPS)。编译时我一般会写一个简单的 Makefile 片段:

CC = gcc CFLAGS = -O2 -I./generated -I./gsoap -DWITH_NONAMESPACES LDFLAGS = -lm # 如果启用 SSL # CFLAGS += -DWITH_OPENSSL # LDFLAGS += -lssl -lcrypto OBJS = main.o soapC.o soapClient.o onvif_client: $(OBJS) $(CC) -o $@ $(OBJS) $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@

-DWITH_NONAMESPACES这个宏值得说一下。ONVIF 的 SOAP 消息里命名空间前缀有时候不固定,加上这个宏可以让 gSOAP 在解析时忽略前缀差异,减少“标签不匹配”的报错。但代价是如果你同时对接多个命名空间冲突的服务,可能会误解析。我一般在只做 ONVIF 单一协议时加上,混合协议时去掉。-lm是数学库,gSOAP 序列化float和double时会用到。SSL 部分看设备要求,如果设备只开放 HTTP 的 80 端口,可以先不编 SSL,等调通再补。

3.2 初始化 SOAP 环境与设置超时

生成的代码里,每个服务对应一个struct soap上下文。客户端程序里第一步是soap_init,然后设置超时和端点地址:

#include "soapH.h" #include "soapStub.h" #include "wsdd.nsmap" // 如果用到 WS-Discovery int main() { struct soap soap; soap_init(&soap); // 设置连接超时和发送/接收超时,单位秒 soap.connect_timeout = 5; soap.send_timeout = 5; soap.recv_timeout = 10; // 设置 ONVIF 设备地址 const char *endpoint = "http://192.168.1.100/onvif/device_service"; // 调用 Device 服务的 GetDeviceInformation _tds__GetDeviceInformation req; _tds__GetDeviceInformationResponse resp; memset(&req, 0, sizeof(req)); memset(&resp, 0, sizeof(resp)); if (soap_call___tds__GetDeviceInformation(&soap, endpoint, NULL, &req, &resp) == SOAP_OK) { printf("Manufacturer: %s\n", resp.Manufacturer); printf("Model: %s\n", resp.Model); printf("Firmware: %s\n", resp.FirmwareVersion); } else { soap_print_fault(&soap, stderr); } soap_destroy(&soap); soap_end(&soap); soap_done(&soap); return 0; }

soap_init初始化上下文;connect_timeout控制 TCP 连接阶段,send_timeout和recv_timeout控制数据收发阶段。ONVIF 设备响应有快有慢,recv_timeout设太短会在 PTZ 控制这种需要等待机械动作的场景下频繁超时,我一般给到 10 秒。soap_call___tds__GetDeviceInformation这个函数名是soapcpp2根据 WSDL 里的操作名生成的,前缀__tds__对应 Device 服务的命名空间缩写。调用失败时用soap_print_fault打印错误,它会输出 SOAP Fault 的详细信息,比只看返回值有用得多。

3.3 处理鉴权:WS-UsernameToken 的配置方式

大部分 ONVIF 设备在正式环境里要求鉴权,用的是 WS-UsernameToken 方式。gSOAP 本身不自动帮你算密码摘要,需要手动填充soap_wsse_username_token结构。常见做法是:

#include "wsseapi.h" // 需要把 gsoap 的 wsse 插件编进来 // 在 soap_init 之后 soap_register_plugin(&soap, soap_wsse); // 设置用户名和密码 if (soap_wsse_add_UsernameTokenDigest(&soap, NULL, "admin", "password123") != SOAP_OK) { soap_print_fault(&soap, stderr); return -1; }

soap_wsse_add_UsernameTokenDigest会自动生成 Nonce、Created 和 PasswordDigest,符合 ONVIF 的鉴权规范。注意wsseapi.c和wsseapi.h在 gSOAP 发行包的plugin/目录下,需要一起编进工程。如果设备用的是 HTTP Digest 而不是 WS-UsernameToken,那要在soap结构里设置soap.userid和soap.passwd,并启用SOAP_IO_CHUNK之类的选项,具体看设备手册。我遇到过的坑是:有些设备同时支持两种鉴权,但 WS-UsernameToken 的Created时间戳和设备的时钟偏差超过 5 分钟就会拒绝,所以设备端和客户端最好都开 NTP 同步。

4. 避坑与排查:ONVIF 对接中常见的五类翻车现场

这一章列的都是我在实际项目里踩过的坑,按“现象 → 原因 → 解决”写,你遇到类似报错时可以对照着查。

4.1 现象:soap_call 返回 SOAP_FAULT,错误信息是“Sender not Authorized”

原因:鉴权信息没带上,或者 WS-UsernameToken 的摘要算错了。常见于手动拼 SOAP Header 时漏了PasswordDigest,或者Created时间格式不对。另一个可能是设备要求 HTTPS,但你用 HTTP 发了请求。

解决:先确认soap_wsse_add_UsernameTokenDigest是否在每次调用前都执行了。如果用的是自己实现的摘要,检查Nonce是否做了 Base64 编码、Created是否符合 ISO8601。用soap_print_fault把完整 SOAP 消息打出来,对比设备文档里的鉴权示例。HTTPS 的话,在soap结构里设置soap.ssl = NULL并链接 OpenSSL,端点地址改成https://。

4.2 现象:编译时报“undefined reference tosoap_wsse_add_UsernameTokenDigest”

原因:只包含了wsseapi.h,但没有把wsseapi.c和smdevp.c编进工程。gSOAP 的插件是独立源文件,不会自动链接。

解决:在 Makefile 的OBJS里加上wsseapi.o和smdevp.o,这两个文件在gsoap/plugin/下。如果还报soap_wsse_verify_*相关的未定义,把wsseapi.c里用到的dom.c也加进来。交叉编译时注意这些插件文件也要用目标平台的编译器编。

4.3 现象:设备返回的 XML 里命名空间前缀和生成代码里的不一致,解析失败

原因:ONVIF 规范允许设备使用不同的命名空间前缀,比如tds:和ns1:都合法。gSOAP 默认按前缀匹配,前缀对不上就解析不了。

解决:编译时加-DWITH_NONAMESPACES,让 gSOAP 忽略前缀,只按标签名和命名空间 URI 匹配。如果项目里同时用了其他 SOAP 服务,不能全局加这个宏,那就用soap_set_namespaces手动设置命名空间映射表,把设备实际用的前缀映射到正确的 URI。

4.4 现象:PTZ 控制调用成功但设备不动,或者动一下就停

原因:PTZ 的ContinuousMove需要配合Stop使用,如果只发ContinuousMove不发Stop,有些设备会一直转,有些则因为超时自动停。另外,PTZSpeed参数里的PanTilt和Zoom速度值范围是 -1 到 1,设成 0 可能被设备忽略。

解决:确认ContinuousMove之后在预期时间发Stop,或者改用RelativeMove做定量移动。速度值先设 0.5 试,别一上来给 1.0。如果设备支持GetStatus,调用一下看当前 PTZ 状态,确认命令是否真的被接受。

4.5 现象:程序运行一段时间后内存持续增长,最终被 OOM 杀掉

原因:gSOAP 的soap上下文在每次调用后会分配内存,如果只调soap_end不调soap_destroy,或者反过来,都会导致部分内存没释放。另外,如果每次调用都soap_init一个新上下文而不soap_done,也会泄漏。

解决:养成固定模式:每次调用前soap_init或复用同一个上下文,调用后先soap_destroy释放反序列化产生的对象,再soap_end释放临时缓冲区,最后如果不再使用就soap_done。复用上下文时,每次调用前调soap_end清理上一次的残留即可。用 Valgrind 跑一遍,重点看soap_malloc的分配和释放是否配对。

5. 进阶技巧:用 WS-Discovery 自动发现设备与批量管理

前面讲的都是已知设备 IP 的情况下直接调用。实际项目里更常见的是设备在局域网里,IP 不固定,这时候需要 WS-Discovery 来发现。gSOAP 发行包里带了wsdd相关的示例和插件,但默认不生成 ONVIF 的发现代码,需要手动处理。

5.1 编译 WS-Discovery 插件并发送 Probe 消息

WS-Discovery 的核心是往组播地址239.255.255.250:3702发Probe消息,设备收到后单播回复ProbeMatch。gSOAP 的wsddapi.c提供了soap_wsdd_probe函数,但需要你先用soapcpp2生成wsdd.h对应的代码。常见做法是从 gSOAP 的samples/wsdd目录里拿现成的wsdd.h和wsddC.c,或者用wsdl2h处理wsdd.wsdl。

# 在 gsoap/samples/wsdd 下通常有现成文件 cp ../../samples/wsdd/wsdd.h ./generated/ cp ../../samples/wsdd/wsddC.c ./generated/ # 编译时加入 wsddapi.o

发送 Probe 的代码片段:

#include "wsddapi.h" struct soap soap; soap_init(&soap); soap_register_plugin(&soap, soap_wsdd); // 设置组播超时和回调 soap.wsdd__ProbeMatches = NULL; if (soap_wsdd_probe(&soap, NULL, NULL, NULL, NULL) != SOAP_OK) { soap_print_fault(&soap, stderr); } // 处理回复 soap_wsdd_listen(&soap, 5); // 监听 5 秒

soap_wsdd_probe的参数里可以指定Types过滤,比如只发现 NetworkVideoTransmitter 类型的设备。soap_wsdd_listen的超时时间根据网络规模调整,设备多的时候给 5 到 10 秒。收到的ProbeMatch里包含设备的XAddrs,那就是后续 ONVIF 调用的端点地址。

5.2 批量管理时的连接复用与并发控制

发现一批设备后,如果逐个soap_init再调用,开销不小。我一般会维护一个soap上下文池,每个设备复用一个上下文,调用前soap_end清理,调用后不soap_done。并发方面,gSOAP 本身不是线程安全的,多线程调用同一个soap上下文会出问题。常见做法是每个线程独立soap_init,或者用互斥锁串行化对同一上下文的访问。如果设备数量在几十台以内,单线程轮询加非阻塞 socket 也够用;上百台的话,建议按设备分组,每组一个线程,线程内串行。

5.3 一个实用的调试习惯

我调试 ONVIF 时习惯在soap结构里打开消息日志:

soap_set_mode(&soap, SOAP_C_UTFSTRING); soap.send_timeout = 5; soap.recv_timeout = 10; // 打印完整 SOAP 消息到文件 soap_set_logging(&soap, "onvif_debug.log");

soap_set_logging会把每次请求和响应的完整 XML 写进日志文件。设备返回的 SOAP Fault 里经常有厂商自定义的错误码,光看soap_print_fault不够,翻日志里的原始 XML 才能定位。从那以后我每次对接新设备,第一件事就是开日志,跑一遍 GetDeviceInformation 和 GetCapabilities,把设备的实际响应存下来当基线。后面任何调用出问题,先和基线对比,能省掉大量猜测。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询