ONVIF开发必学:用gSOAP生成C框架代码的完整实战指南
简介面向Linux环境下从事网络视频设备接入的C/C开发者这份资源围绕gSOAP工具链生成ONVIF框架代码的完整流程整理出可直接使用的工程文件与生成结果适合需要快速理解ONVIF服务调用、RTSP码流获取等场景的工程师参考。包体共12个文件压缩后约1.31MB主要包含5个C源文件、5个头文件以及nsmap映射文件和makefile构建脚本覆盖SOAP客户端运行库、自动生成的桩代码与编译集成入口。已有1692人学习下载反映出该主题在安防与物联网开发群体中的实用价值。资源的价值在于提供了从WSDL解析、接口生成到主程序调用的闭环示例可重点学习soapcpp2的典型用法、设备信息查询和流地址获取等关键接口的实现方式同时借助makefile快速验证生成代码的编译与运行从而降低ONVIF协议栈的上手门槛。1. ONVIF开发绕不开gSOAP它把WSDL变成可调用的C函数凡是做过网络摄像机对接的基本都绕不开 ONVIF 和 gSOAP 这对组合。ONVIF 定义了一整套基于 SOAP 的 Web 服务接口gSOAP 则负责把这些 WSDL 描述翻译成可以直接调用的 C 结构体和 soap_call 函数。用 gSOAP 生成 ONVIF 框架 C 代码能省掉手写 XML 序列化、命名空间映射这批繁琐工作适合正在做 IPC 客户端、NVR 接入或协议测试工具的开发者和嵌入式工程师。我把整套流程拆成可复现的步骤连 WSDL 依赖怎么整理、typemap 怎么配、生成后怎么编译都说清楚新手按顺序操作能跑通老手可以直接跳到第 5 章看坑。2. 搭建Linux下的gSOAP工具链两个命令的版本匹配决定八成成败gSOAP 生成代码不是一条命令搞定而是 wsdl2h、soapcpp2 两个命令先后配合。前者吃 WSDL后者吃头文件各自版本如果和运行时库对不上生成的代码编译时会报出一堆跟宏定义有关的错。先把工具链装对、装统一后面才省心。2.1 安装gSOAP源码编译与包管理器选一条路Debian 系系统里直接装包是起步最快的sudo apt update sudo apt install -y gsoap wsdl2h --version soapcpp2 --version有些发行版把工具拆在gsoap和gsoap-dev两个包里只装前者时/usr/bin/wsdl2h不一定存在。装完先which wsdl2h确认路径再跑--version看工具是否可执行。如果系统仓库里 gSOAP 版本太老或者你接下来要交叉编译建议走源码编译sudo apt install -y flex bison libssl-dev ./configure --prefix/usr/local make -j4 sudo make installconfigure 阶段会检测 flex、bison 和 OpenSSL 开发头原因是 gSOAP 的 WS-Security 和 WS-Addressing 支持都依赖 OpenSSL。检测不过时先补对应依赖包再重跑 configure别跳过。注意/usr/bin/wsdl2h和/usr/local/bin/wsdl2h同时存在时PATH 决定谁先生效。我一般用源码编译完直接hash -r刷新缓存再确认版本避免生成代码和运行时库来自两套版本。工程里如果混用了系统 libgsoap 和源码生成的代码编译时常出现soapH.h里的宏与静态库不匹配这种问题定位起来非常费劲。2.2 wsdl2h与soapcpp2的分工两个阶段一条流水线WSDL 描述的是抽象接口C 语言没法直接消费。gSOAP 的做法分两步wsdl2h 把 WSDL 和 XSD 解析成一份 C/C 头文件里面是对应的结构体、枚举和服务方法soapcpp2 再读这份头文件生成真正可编译的源文件包括 XML 序列化、反序列化和客户端调用桩。理解这两个阶段的良机是看 soapcpp2 生成的函数签名int soap_call___tds__GetSystemDateAndTime( struct soap *soap, const char *endpoint, const char *action, struct _tds__GetSystemDateAndTime *in, struct _tds__GetSystemDateAndTimeResponse *out);这个签名贯穿后续所有调试。第一个参数struct soap *是 gSOAP 的运行时上下文承载端点地址、超时时间、认证信息和错误状态第二个参数是设备服务的 URL第三个是 SOAPActionONVIF 场景下很多设备会校验它最后两个是请求和响应结构体。设备返回 Fault 时错误信息挂在soap-fault里网络不通时返回值是SOAP_TCP_ERROR一类枚举。把这个签名字段弄明白后面看日志就有方向。2.3 自定义typemap.dat控制类型映射绕开字符串解析typemap.dat 是 wsdl2h 的类型映射文件作用是把 XML Schema 类型对应到 C 类型。gSOAP 自带一份完整的默认映射但 ONVIF 场景下几乎都要追加自己的规则。我习惯复制一份到项目里再改而不是直接动系统文件# 常用基础映射保持默认 xsd__int int xsd__string char* # 让 xs:dateTime 直接映射成 time_t省去字符串日期解析 xsd__dateTime time_t第 4 行这个映射在 ONVIF 里很实用。设备返回的时间字段如果不做映射默认是一段 ISO 8601 字符串取年份和秒数都要先解析映射成time_t后直接拿去gmtime、asctime即可。加了这条之后生成的响应结构体里日期字段类型会变成time_t写业务代码前先看soapStub.h确认别想当然地按字符串打印。typemap.dat 里也可以写#ignore来裁掉不需要的类型但 ONVIF 服务之间存在大量公共引用裁剪过头会导致 soapcpp2 阶段找不到结构体。我的经验是初期只加映射不做 ignore等生成跑通、确定哪些类型确实多余再删。3. 用wsdl2h解析ONVIF WSDL依赖整理与头文件检查wsdl2h 对 ONVIF 这种多层 import 的 WSDL 集合并不友好主文件喂进去之后它会顺着xsd:import和xsd:include递归找依赖。依赖目录不完整主 WSDL 再标准也生成不出来。3.1 准备WSDL目录完整依赖比主文件更重要先确定要哪些服务。日常对接设备下面三张表足够起步WSDL 文件服务内容常用操作devicemgmt.wsdl设备管理GetDeviceInformation、GetSystemDateAndTimemedia.wsdl媒体配置GetProfiles、GetStreamUriptz.wsdl云台控制ContinuousMove、AbsoluteMoveevent.wsdl 和 recording.wsdl 这类后续再加。event 依赖 wsnt、WS-BaseNotification 等一整套 Schema引入之后头文件体积大、生成时间长前期会让排查问题变得更复杂。把这些文件按目录整理好公共 Schema 放在common或对应版本目录下onvif_wsdl/ devicemgmt.wsdl media.wsdl ptz.wsdl common/ onvif.xsd ver10/ schema/ onvif.xsd拿到厂商或官网的 WSDL 包后第一个坑经常是schemaLocation写成在线 URL。离线环境跑 wsdl2h 会直接报module not found。我一般先扫描一遍grep -r schemaLocation\http onvif_wsdl/确认哪些 import 指向外部地址后再用 sed 把明显的 ONVIF 官方路径改成相对路径sed -i s#schemaLocationhttp://www.onvif.org/ver10/schema/onvif.xsd#schemaLocationcommon/onvif.xsd#g \ onvif_wsdl/devicemgmt.wsdl onvif_wsdl/media.wsdl不要把全部 http 开头都替换只处理确实会访问失败的 ONVIF 官方域名否则可能把不存在的路径引到错误位置。改完后用 grep 再确认一遍确保所有 import 都指向本地存在的文件。3.2 执行wsdl2hC模式参数与依赖目录生成头文件的命令按这个来wsdl2h -c -s -t ../typemap.dat \ -o onvif.h \ -I ../onvif_wsdl:/usr/local/share/gsoap/import \ ../onvif_wsdl/devicemgmt.wsdl \ ../onvif_wsdl/media.wsdl \ ../onvif_wsdl/ptz.wsdl逐个看参数含义-c生成 C 兼容头文件不是 C 默认风格-s不引入 STL。纯 C 环境必须加否则头文件里会出现std::string和stlvector.h引用交叉编译时经常找不到-t指定第 2.3 节写的 typemap.dat-o输出文件名-I依赖搜索路径冒号分隔。左边是 ONVIF WSDL 目录右边是 gSOAP 自带 import 目录里面放着 wsa.h、wsdd.h 等标准头文件最后列出的多个 WSDL 是入口文件公共onvif.xsd会被递归 import 自动带上。如果拿到的是 ONVIF 官方那个 remotedevicemgmt.wsdl 总入口也可以直接喂它它会递归把设备和各子服务的 WSDL 都带进来。缺点是一并引入 event、search 等一堆暂时用不到的服务生成的头文件很大。分离入口方式更适合逐步迭代。wsdl2h 报Cannot open file或module not found时九成是-I路径没覆盖到对应依赖先确认报错文件的实际位置再调整路径不要反复重试同一命令。3.3 检查生成的onvif.h命名空间前缀和关键结构体生成后先看一眼结构体grep _tds__GetDeviceInformation onvif.h | head -5ONVIF 的 WSDL 命名空间在 gSOAP 里会转成短前缀规则是下划线加服务缩写加方法名前缀对应服务设备上常见端点后缀tds设备管理/onvif/device_servicetrt媒体服务/onvif/media_servicetptz云台服务/onvif/ptz_servicestruct _tds__GetDeviceInformation里的tds就是设备服务命名空间http://www.onvif.org/ver10/device/wsdl的缩写。不同厂商设备端点后缀可能不同但命名空间前缀是 ONVIF 标准规定的不会变。还要确认SOAP_ENV__Fault相关定义在。没有它后续设备返回错误时你连解析 Fault 的能力都没有。C 模式下如果头文件里出现#include stlvector.h说明-s没生效或当前 wsdl2h 版本不吃这个参数回头检查版本这一步漏了后面编译会非常难受。4. 用soapcpp2生成C框架参数选择、生成物与最小调用wsdl2h 解决的是「类型长什么样」soapcpp2 解决的是「代码怎么跑」。这一步参数选错要么生成一堆用不上的服务端文件要么 ONVIF 强制要求的 SOAP 1.2 没开设备直接拒绝通信。4.1 生成前的决策C语言路线、目录与命名前缀生成之前先定三件事目标语言、客户端还是服务端、文件前缀。ONVIF 客户端在嵌入式 SDK 里常用纯 C原因是很多 BSP 的交叉编译环境对 C 支持不完整业务代码也未必愿意引入 C 运行时。纯 C 的代价是字符串全是char*内存要自己管结构体里嵌套指针数组也常见但在嵌入式里这就是常态。soapcpp2 加-c生成纯 C 代码不加默认走 C 路线。生成目录单独建一个mkdir -p gen cd gen文件前缀用-p onvif。默认的soapC.c、soapStub.h文件名太通用一旦工程里还有另一套 gSOAP 生成物重名文件直接互相覆盖。用-p onvif能把这些文件改成onvifC.c、onvifStub.h是成本最低的隔离手段。4.2 执行soapcpp2完整命令与参数拆解soapcpp2 -2 -c -C \ -p onvif \ -I ../gsoap/import:../onvif_wsdl \ -t ../typemap.dat \ -x \ ../onvif.h参数逐个说-2强制 SOAP 1.2。ONVIF 规范要求使用 SOAP 1.2不加这个参数默认生成的报文是 1.1设备会回version mismatch属于新手最容易犯的错-c生成 C 代码-C只生成客户端。做设备对接用这个能少生成一整套服务端桩代码-p onvif所有输出文件以 onvif 开头-I搜索路径。左边是 gSOAP 自带 import 目录放着 wsa.h、wsdd.h、wsse.h 这些扩展头右边是 ONVIF 的 WSDL 依赖目录-ttypemap.dat要和 wsdl2h 用同一份-x不生成示例 XML 报文省得目录里堆一堆*.req.xml、*.res.xml测试文件。soapcpp2 跑完会提醒头文件里哪些方法生成了、哪些没有。这个输出值得看一眼但也不是所有警告都要处理。真正要用的是后续代码编译时能否在onvifStub.h里找到对应函数声明。4.3 生成物清单与最小调用代码生成物各自的用处整理如下文件作用onvifStub.h数据结构、函数声明业务代码主要 include 它onvifH.hsoap 内部结构定义间接被引用onvifC.cXML 序列化与反序列化核心必须编入工程onvifClient.c客户端调用桩必须编入工程onvifClientLib.c客户端库封装按需使用onvif.nsmap命名空间映射表必须链接进来onvif.nsmap 是最容易被忽略的一个。它定义了类型和命名空间的对应表漏掉它运行时经常出现意料之外的 soap 错误。调用代码里通过 include 把它引进来。最小调用代码#include onvifH.h #include onvifStub.h #include onvif.nsmap int main(void) { struct soap *soap soap_new(); struct _tds__GetSystemDateAndTime req; struct _tds__GetSystemDateAndTimeResponse resp; memset(req, 0, sizeof(req)); memset(resp, 0, sizeof(resp)); int ret soap_call___tds__GetSystemDateAndTime( soap, http://192.168.1.64/onvif/device_service, NULL, req, resp); if (ret SOAP_OK resp.SystemDateAndTime resp.SystemDateAndTime-UTCDateTime) { struct _tds__DateTime *t resp.SystemDateAndTime-UTCDateTime; printf(UTC time: %04d-%02d-%02d %02d:%02d:%02d\n, t-Year, t-Month, t-Day, t-Hour, t-Minute, t-Second); } else { soap_print_fault(soap, stderr); } soap_free(soap); return 0; }soap_call___tds__GetSystemDateAndTime这个方法名有三个下划线是 gSOAP 远程调用的固定格式写错一个下划线编译都过不去。调用前先 grep 一份原型对照grep soap_call___tds__GetSystemDateAndTime onvifStub.hsoap_new()分配并初始化了运行时上下文结束用soap_free()释放。这个结构体不能被两个线程共用也不要手动 free否则后续重试会踩内存错误。5. 编译接入与避坑链接参数、连通性测试和五个常见问题生成代码不是终点编进你的 SDK 或测试工具才是。这一章给编译命令给第一轮验证思路然后把做 ONVIF 对接最容易翻车的五个点一次说透。5.1 编译命令源文件、运行时与openssl把生成物和示例程序编成一个可执行文件gcc -o onvif_demo \ demo.c \ onvifC.c onvifClient.c \ /usr/local/lib/libgsoap.a \ -I ../gsoap/import -I gen \ -lssl -lcrypto -lpthreadlibgsoap.a是 gSOAP 运行时静态库。不想依赖系统库直接把源码里的stdsoap2.c加进编译列表也可以效果等价。使用 WS-Security 时-lssl -lcrypto必加否则 wsseapi 相关的符号会链接失败。如果代码里用了设备发现和认证接口要把对应的扩展源文件也编进去gcc -o onvif_demo \ demo.c \ onvifC.c onvifClient.c \ wsddapi.c wsseapi.c \ /usr/local/lib/libgsoap.a \ -I ../gsoap/import -I gen \ -lssl -lcrypto -lpthread第一次联调时建议加上-DSOAP_DEBUG编译宏gSOAP 会把收发报文打到 stderr定位 XML 结构问题非常直观。跑通后再去掉这个宏减少日志开销。5.2 连通性测试先不认证打通一条无状态接口ONVIF 里GetSystemDateAndTime是不要求认证的少数接口适合当第一轮连通性测试。设备地址已知时先加超时保护soap-connect_timeout 5; soap-recv_timeout 5; int ret soap_call___tds__GetSystemDateAndTime( soap, http://192.168.1.64/onvif/device_service, NULL, req, resp);connect_timeout控制 TCP 建连超时recv_timeout控制等待响应超时。返回SOAP_TCP_ERROR时先确认 IP、端口和协议返回SOAP_OK但响应字段为空检查请求结构体是否 memset 过或者设备固件对空请求处理有差异。超时或 Fault 之后soap 结构体不能直接复用来发下一条请求。gSOAP 的内存模型是soap_destroy()释放反序列化出来的数据soap_end()释放临时分配然后才能重新初始化。图省事就直接soap_free()旧的、soap_new()一个新的简单有效。5.3 避坑ONVIF 加 gSOAP 常见问题1. 编译时 stlvector.h 找不到现象编译报fatal error: stlvector.h: No such file or directory或头文件里出现std::string。原因wsdl2h 生成头文件时没有加-s默认带上了 STL 依赖而纯 C 工程或交叉编译工具链里没有对应 STL 头文件。解决回到第 3.2 节在 wsdl2h 命令里补上-s重新生成soapcpp2 同步重跑。确认onvif.h里不再有#include stlvector.h再继续。2. 链接时报 soap_call 未定义现象编译通过链接报undefined reference to soap_call___tds__GetSystemDateAndTime之类。原因函数名写错了。soapcpp2 生成的客户端函数名是soap_call___加两个下划线再加命名空间前缀和方法名少一个下划线就是两个完全不同的符号。解决在onvifStub.h里 grep 函数原型严格复制完整签名不要手敲。3. 设备返回 Fault 或 HTTP 500现象调用 GetProfiles、GetStreamUri 这类接口时soap_call返回SOAP_FAULTsoap_print_fault打印出 Sender 错误或设备直接返回 500。原因这些接口要求 WS-Security 认证未携带 UsernameToken 就被设备拒绝。GetSystemDateAndTime能通不代表别的方法能通。解决按第 6.2 节补上soap_wsse_add_UsernameTokenDigest在每次soap_call之前设置认证信息。4. 设备提示 MessageID 缺失现象请求发出后设备回wsa5:MessageID is required或者连接被直接断开没有明确错误信息。原因ONVIF 使用 WS-Addressing消息头里必须带wsa5:MessageID。soapcpp2 生成代码不会自动填充这个字段需要在调用前手动生成。解决#include wsaapi.h soap-wsa_msgid soap_wsa_rand_uuid(soap);每条请求都需要一个新的 MessageID。复用同一个 ID部分设备也会拒绝。5. 交叉编译时 wsse 相关函数异常现象板子上跑起来调用认证接口时段错误或链接阶段报libssl相关符号缺失。原因编译机上 OpenSSL 头文件版本和板端 libssl 库版本不一致wsseapi.c 生成的代码调用签名对不上。解决交叉编译工具链要配套安装对应的 OpenSSL 开发包链接时确保-L指向板端库路径-lssl -lcrypto不要省略。6. 进阶验证设备发现加认证才是完整可用的ONVIF客户端6.1 WS-Discovery先找到设备再发起调用很多时候摄像头只在一个网段确切 IP 和 ONVIF 服务端点都不清楚。gSOAP 的 wsddapi.h 封装了 WS-Discovery可以直接用组播方式探测。#include wsddapi.h struct soap *soap soap_new(); soap-recv_timeout 3; int ret soap_wsdd_Probe( soap, soap_udp://239.255.255.250:3702, NULL, NULL, NULL, http://www.onvif.org/ver10/network/wsdl, 1);ONVIF 设备默认监听 239.255.255.250 的 3702 端口。soap_wsdd_Probe的具体参数以 wsddapi.h 原型为准最后一个timeout参数是探测等待秒数。结果里能拿到设备的XAddrs字段把它当soap_endpoint继续走后面的设备服务调用。排查发现问题时抓包是最直接的sudo tcpdump -i eth0 udp port 3702 -XX能看到自己发出的 Probe 报文和设备回应的 ProbeMatch。没回应就先查设备是否关了发现功能再查网卡是否加入了组播很多是二层环境问题而不是代码问题。6.2 WS-Security认证让受限接口通过发现设备之后没有认证设备管理、媒体、云台接口基本都调不通。gSOAP 的 wsseapi 提供了现成的 UsernameToken 封装#include wsseapi.h soap_wsse_add_UsernameTokenDigest( soap, auth, admin, password);第二个参数是wsu:Id传 NULL 也可以第三、四个参数是用户名和密码。Digest 方式比明文更安全不少设备也明确要求用 Digest明文方式会被直接拒绝。注意认证信息是绑定在 soap 结构体上的。每次soap_free后重新soap_new或者soap_end之后都需要重新设置 token。做发现时不需要认证但拿到XAddr后切换到设备服务调用前必须重新添加认证信息。我自己的习惯是每次生成代码后先跑一遍GetSystemDateAndTime再跑GetProfiles最后才碰 PTZ 运动控制。某项目里因一直忽略wsa_msgid被设备拒了大半天抓包才发现请求头里 MessageID 是空的。从那以后我每次调试 ONVIF 前都会先 tcpdump 看一眼请求头再进业务逻辑。希望帮到你。本文还有配套的精品资源点击获取