首页 / 资讯中心 / 文章详情

用gSOAP在Linux下生成ONVIF框架C代码实战指南

用gSOAP在Linux下生成ONVIF框架C代码实战指南 ★ FEATURED ARTICLE
简介在Linux环境下使用gSOAP生成ONVIF框架C代码是开发网络视频设备客户端、实现跨厂商互操作的常用技术路线核心是借助gSOAP编译器解析设备WSDL自动生成可直接调用的C语言服务接口避免手工构造SOAP/XML请求的繁琐。这套资源面向需要快速接入ONVIF设备如获取设备型号、固件版本、RTSP码流地址等的C/C工程师也适合学习Web服务代码生成流程的嵌入式或物联网开发者对安防监控系统集成、IoT平台对接等场景尤为实用。压缩包共12个文件以C源文件、头文件为主体附有命名空间映射文件与makefile构建脚本整体大小约1.31MB目录结构紧凑清晰便于对照源码理解各模块作用。目前已有1687人浏览学习。包内提供的soapClient.c、stdsoap2.c、soapC.c、onvif_dump.c等生成代码与示例完整呈现了gSOAP上下文初始化、soap_call_ns__GetDeviceInformation等接口调用、响应结构解析、错误捕获及RTSP地址获取的核心逻辑结合头文件与构建脚本可快速编译出一套可运行的ONVIF客户端框架为二次开发和协议调试提供清晰可复用的参考模板。1. 这个标题在解决什么问题从一台 IPC 的互通说起拿到一台海康或者大华之外的杂牌网络摄像机你想在 Linux 主机上直接拉视频、控云台、读设备信息最不愿碰的事就是各家私有的 SDK 和私有协议。ONVIF 标准把设备管理、媒体流、云台、事件这些接口统一成了 Web Service而 gSOAP 是 Linux 下把 ONVIF 的 WSDL 转成 C 代码最成熟的一套工具链。标题里的“生成 ONVIF 框架 C 代码”本质就是让一台 Linux 设备或网关具备 ONVIF 客户端/服务端能力把标准协议变成你能直接调用的 C 函数。这篇文章写给想在嵌入式 Linux、工业网关或自有平台上快速打通 ONVIF 接入的开发者目标很具体从拿到 WSDL 到跑通一个真实设备请求全程不靠 C只用纯 C。2. 为什么是 gSOAP搞清楚生成器在做什么再动手2.1 从 WSDL 到 C 代码gSOAP 生成链路里那两步各干了什么gSOAP 最核心的两个可执行程序是wsdl2h和soapcpp2。它们分工非常明确wsdl2h读取 WSDL 和 XSD 定义把它们解析成 gSOAP 自己的一套接口描述头文件也就是一个.h文件soapcpp2再读这个.h文件生成真正的 C/C 序列化代码、客户端调用 stub 和服务端骨架。我一般把wsdl2h的产物叫做“半成品头文件”因为里面全是_tds__GetDeviceInformation这种结构体和函数声明还不能直接编译。soapcpp2的输出才是你工程里真正要用到的.c文件负责把结构体序列化成 SOAP XML、把 XML 解析回结构体、以及执行实际的 HTTP 传输。为什么 ONVIF 场景里大家都选 gSOAP 而不是手动写 SOAP因为 ONVIF 的 WSDL 是一个很深的依赖树每个服务都 import 公共类型定义手工拼 XML 请求报文不仅工作量巨大还容易在命名空间、数组类型、Base64 字段上翻车。gSOAP 把这一层全自动化了你要做的只是把生成的代码接进工程。gSOAP 默认输出 C 代码类和std::string都给你安排上。但嵌入式 Linux 上的很多 IPC、NVR、边缘网关工程是用纯 C 维护的项目标题也明确写了“C 代码”。所以后面所有命令我都加了-c参数让生成器输出纯 C 结构体和char*类型避免引入 C 运行时。2.2 依赖与版本先确认环境里少了什么在 Linux 上用 gSOAP第一步是确认工具链在不在。Debian/Ubuntu 系发行版直接装gsoap包就能同时拿到wsdl2h和soapcpp2RedHat 系对应的是gsoap和gsoap-devel。源码方式也很常见把 gSOAP 源码包解压后进gsoap目录执行标准的 configure、make、make install 三步。需要注意的一个点是不同的 gSOAP 小版本对 WSDL 的解析行为有差异尤其在对 ONVIF 某些奇怪类型的处理上。我自己踩过一次同一份 WSDL一个版本能顺利生成另一个版本在某个复杂类型上报错退出。所以实际项目里我建议固定一个版本整个团队统一而不是谁方便谁升级。源码包里自带的gsoap/wsdl/typemap.dat和后面要用的stdsoap2.c也要跟着工具版本走混用不同版本的运行时文件是最典型的黑匣子问题来源。另外ONVIF 设备接入通常要用到 WS-Security 做认证也就是用户名密码的摘要签名。这一步依赖 OpenSSL。你不一定在所有编译阶段都用它但链接最终程序时大概率要-lssl -lcrypto提前装好能省不少事。2.3 拿到和整理 ONVIF 的 WSDL文件比 URL 更可靠做 ONVIF 开发WSDL 文件从哪来常见做法是从 ONVIF 官网的规范下载页拿那里有完整的核心规范 WSDL 包按设备管理、媒体、云台、事件等分类。拿到后不要直接对着 URL 让wsdl2h去远程抓原因很简单WSDL 之间互相 import一旦某个远程地址不可达生成过程就中断而且你无法复现同一个结果。我习惯把所有 WSDL 和 XSD 文件放进同一个目录用本地文件路径作为输入。ONVIF 服务端 WSDL 的依赖关系大致是设备管理devicemgmt是最基础的媒体media会引用设备管理的公共类型事件event又引用媒体里的部分定义。所以整理目录时建议把整个 WSDL 文件包完整解压到一个目录一个都别删。你只需要设备管理能力也要把其他 WSDL 放在旁边因为解析器可能随时按 import 关系去读它们。mkdir -p ~/onvif/wsdls cd ~/onvif/wsdls # 将下载的 ONVIF WSDL 压缩包解压到当前目录 unzip onvif-wsdl.zip # 检查目录内 .wsdl 与 .xsd 文件是否齐全 ls -la *.wsdl *.xsd这段命令没有做复杂的事但目录结构一定要先理清。wsdl2h在解析一个 WSDL 时如果遇到 import 的另一个文件它会按相对路径去同级目录找。把全部文件放在同一级目录是让解析器最省心的做法也是后续一切生成工作的前提。3. 用 wsdl2h 生成头文件命令选项与 typemap 一起讲3.1 一条最小命令与每个参数的含义确认 WSDL 文件齐了之后进入生成头文件这一步。这里是最容易出“生成结果和预期不符”的地方但命令本身并不复杂。我通常这样写cd ~/onvif/wsdls wsdl2h -c -t typemap.dat -o onvif.h \ devicemgmt.wsdl media.wsdl ptz.wsdl event.wsdl参数说明如下-c指定输出 C 代码风格的头文件。没有这个参数生成的是 C 头文件里面会出现std::string、类定义等 C 工程无法直接编译的内容。-c是纯 C 项目的命根子。-o onvif.h指定输出头文件名。这个名字会在下一步传给soapcpp2建议用能表达业务含义的名字别叫a.h。-t typemap.dat指定类型映射文件。它解决的是 XML Schema 内置类型和 C 语言类型之间的对应问题后面单独讲。最后一个参数是 WSDL 文件列表。谁在前谁在后有一定讲究一般把最基础、被其他 WSDL import 的放在前面。但从实际经验看wsdl2h对 import 依赖的处理比较智能只要文件都在同一目录顺序影响不大。真遇到顺序导致的解析报错调整一下文件顺序再试即可。生成完成后用ls -l onvif.h确认文件大小。一个只有几 KB 的onvif.h基本可以断定是解析失败了正常的 ONVIF 全量头文件应该有上百 KB里面包含大量结构体定义。如果文件过小多半是 typemap 没生效或者 WSDL 没被正确读入。grep -n GetDeviceInformation onvif.h | head -n 10这条 grep 用来验证设备管理接口是否进了头文件。GetDeviceInformation是 ONVIF 最基础的设备信息查询接口如果它都不在说明devicemgmt.wsdl没解析成功。反过来如果你看到它出现在头文件里说明 WSDL 解析链路基本通了可以往下走。3.2 typemap.dat 该不该用、怎么改很多第一次接触 gSOAP 的开发者会忽略 typemap结果生成出来的结构体里全是char*或一些无法直接使用的中间类型然后开始怀疑工具坏了。实际上这是类型映射没做对。gSOAP 源码包的gsoap/wsdl/typemap.dat提供了一份默认映射但 ONVIF 有自己的类型习惯很多时候要根据设备端实际返回的数据调整。一个能用于多数 ONVIF 项目的 typemap 片段大致长这样xsd__dateTime time_t xsd__duration char* xsd__anyURI char* xsd__hexBinary struct xsd__hexBinary { unsigned char *__ptr; int __size; }解释一下这些映射的意义xsd__dateTime是 XML Schema 的日期时间类型ONVIF 的系统时间接口返回的就是它。如果不映射成time_t生成的头文件里会是一个内部结构体xsd__dateTime你又得自己写转换函数。把xsd__anyURI直接映射为char*也是同理这些字段对大多数应用来说就是字符串。xsd__hexBinary比较特殊它对应证书、指纹这类二进制数据映射成一个带指针和长度的结构体才方便使用。写 typemap 时注意每行等号左边是 XML Schema 类型名右边是 C 类型中间的空格随意。行首用#注释。如果你的工程里用了自定义结构体typemap 也可以引入#include myheader.h让生成的头文件包含你的类型定义。这点在做视频分析网关时特别有用你可以让 ONVIF 的某个扩展字段直接对应业务结构体。3.3 生成的头文件怎么看你才不会觉得是黑匣子打开onvif.h第一眼会看到一堆_tds__、_trt__、_tptz__前缀的结构体。前缀就是命名空间缩写tds是设备服务、trt是媒体服务、tptz是云台服务。在 ONVIF 里这叫服务前缀每个 WSDL 的 targetNamespace 不同wsdl2h会自动生成简写。你要找的接口函数在头文件的末尾附近名字形如soap_call___tds__GetDeviceInformation注意是三个下划线这部分是 gSOAP 函数的固定命名规则soap_call_加__加服务前缀加__加方法名。你用 grep 搜索soap_call_就能拉出全部可调用的客户端函数清单。那么服务地址在哪个结构体上设置请求响应结构体长什么样这些信息也都在头文件里。拿GetDeviceInformation来说输入结构体是_tds__GetDeviceInformation它只有一个DeviceToken字段可能为空响应结构体是_tds__GetDeviceInformationResponse里面的Manufacturer、Model、FirmwareVersion等字段就是你要拿的设备信息。第一次生成后不要急着写代码先花十分钟通读这几个关键结构体后面调用的心智负担会小很多。4. 用 soapcpp2 生成骨架代码并接入工程从生成到能跑4.1 运行 soapcpp2纯 C 与客户端裁剪头文件确认无误后下一步是生成真正的 C 代码。这一步是标题里“框架 C 代码”的核心产物。命令如下mkdir -p ~/onvif/src cd ~/onvif/wsdls soapcpp2 -c -C -p onvif -d ~/onvif/src -x onvif.h逐个参数拆开说-c和wsdl2h的-c对应生成纯 C 代码而不是 C。注意这里区分soapcpp2 -c和soapcpp2 -C两个参数小写c控制语言大写C控制生成客户端。大写S则是只生成服务端。-C只生成客户端框架。如果你只是要对接第三方 ONVIF 设备用-C足够还能减少一半的编译工作量。如果要让 Linux 主机本身成为一个 ONVIF 服务端那就不加-C或者用-S。-p onvif生成的代码文件前缀。默认生成的是soapC.c、soapClient.c、soapH.h这种加了前缀后变成onvifC.c、onvifClient.c、onvifH.h方便多套协议共存。-d ~/onvif/src输出目录。所有生成的文件都放这里保持工作目录干净。-x不生成示例代码文件。示例代码看着有用但实际工程里基本是垃圾还容易被人误编进去。执行后ls ~/onvif/src你会看到这些文件文件作用onvifStub.h数据结构定义头文件包含所有请求响应结构体onvifH.h主头文件你写业务代码时主要 include 它onvifC.c序列化代码负责结构体与 XML 互转onvifClient.c客户端 stub 函数定义onvif.nsmap命名空间映射表必须被工程引用onvifServer.c服务端骨架客户端模式用不上一个常见的错误是只把onvifClient.c加入工程却漏了onvifC.c和onvif.nsmap。onvifC.c里是所有结构的序列化逻辑没有它程序链接时会报一堆 undefined referenceonvif.nsmap是命名空间表没有它运行时所有请求都会因为“namespace mismatch”失败。这三个文件是绑在一起的缺一不可。4.2 把骨架代码接进你的工程一个最小 Makefile生成完代码下一步是编译。很多人这里开始犯难直接gcc main.c onvifClient.c onvifC.c能过吗能但往往漏了 gSOAP 自身的运行时stdsoap2.c。这个文件在 gSOAP 源码包的gsoap/目录下是所有 soap 函数的地基。一个最小可用的 Makefile 如下GSOAP_DIR /path/to/gsoap/gsoap CFLAGS -I. -I$(GSOAP_DIR) -g -Wall LDLIBS -lssl -lcrypto TARGET onvif_test OBJS main.o onvifClient.o onvifC.o stdsoap2.o $(TARGET): $(OBJS) $(CC) $(OBJS) -o $ $(LDLIBS) main.o: main.c onvifH.h $(CC) $(CFLAGS) -c main.c onvifClient.o: onvifClient.c onvifH.h $(CC) $(CFLAGS) -c onvifClient.c onvifC.o: onvifC.c onvifH.h $(CC) $(CFLAGS) -c onvifC.c stdsoap2.o: $(GSOAP_DIR)/stdsoap2.c $(CC) $(CFLAGS) -c $(GSOAP_DIR)/stdsoap2.c clean: rm -f $(OBJS) $(TARGET)这个 Makefile 有几个必须说的点第一stdsoap2.c直接编进工程而不是链接动态库libgsoap.so。这在纯 C 工程里是更稳的做法省去库版本不匹配的问题。同一个 gSOAP 版本生成的代码和stdsoap2.c必须配套用系统仓库装完 gsoap 后又去源码包拿生成代码是我见过最常见的翻车组合。第二-lssl -lcrypto放在链接阶段。即使你现在还没写认证代码也建议先加上因为 ONVIF 设备往往要求 WS-Security 用户名令牌后面集成的确定性更高。第三onvifH.h作为主头文件出现。你写main.c时需要同时 includeonvifH.h和onvif.nsmap后者要在某个.c文件里被 include 一次命名空间表才会进入程序的链接单元。省略这一步程序运行时会报命名空间相关的错误这是 gSOAP 最经典的一个“看起来没有错但就是跑不通”的坑。4.3 第一次调用设备发现与 GetDeviceInformation 的写法骨架代码进工程后马上验证一下能不能真的调通设备。拿最常见的 GetDeviceInformation 举例#include onvifH.h #include onvif.nsmap int main(void) { struct soap *soap; struct _tds__GetDeviceInformation req; struct _tds__GetDeviceInformationResponse resp; int ret; soap soap_new(); if (soap NULL) return -1; soap-recv_timeout 5; soap-send_timeout 5; soap-connect_timeout 5; soap-endpoint http://192.168.1.64/onvif/device_service; memset(req, 0, sizeof(req)); ret soap_call___tds__GetDeviceInformation(soap, soap-endpoint, NULL, req, resp); if (ret ! SOAP_OK) { fprintf(stderr, call failed: %s\n, soap_fault_string(soap)); soap_end(soap); soap_free(soap); return -1; } printf(Manufacturer: %s\n, resp.Manufacturer); printf(Model: %s\n, resp.Model); printf(FirmwareVersion: %s\n, resp.FirmwareVersion); soap_end(soap); soap_free(soap); return 0; }这段代码有几个关键点需要解释。soap_new()创建 soap 环境它管理所有内部缓冲区和连接状态。三个 timeout 参数是为了防止设备无响应时程序卡死嵌入式环境里网络请求一定不能无限等。soap-endpoint是设备 ONVIF 服务的完整 URL不同厂商的路径可能不一样常见的是/onvif/device_service也有用/onvif/device_service加端口号的情况具体看设备能力文档。soap_call___tds__GetDeviceInformation的函数名直接来自生成的头文件。你要养成一个习惯写的每个调用都回头文件里复制函数原型不要手敲因为前缀和参数顺序很容易记错。soap_fault_string在调用失败时会返回 SOAP Fault 的文本描述这是排错的第一入口。如果返回错误是认证类信息下一步就是接 WS-Security如果返回的是命名空间错误回头看onvif.nsmap有没有被正确 include。5. 生成与联调避坑现象、原因、解法各写各的5.1 WSDL 集合不全导致接口消失或 import 报错现象wsdl2h执行到一半报cannot find file ...wsdl或者生成成功但头文件很小grep 设备接口一个都搜不到。原因ONVIF 的 WSDL 之间存在大量 import 关系。比如media.wsdl会 importdevicemgmt.wsdl里的某些复杂类型event.wsdl又 importmedia的类型。你只把单个 WSDL 丢给wsdl2h解析器无法解析出完整的类型依赖树。解决把整个 ONVIF WSDL 包完整解压到同一个目录用通配符或者完整文件名列表作为输入。我一般写全四个核心 WSDL 文件名而不是用*.wsdl通配因为输入顺序也是一种调试手段全写出来更容易复现和排查。5.2 明明是-c生成代码里却冒出 C 语法现象wsdl2h加了-c生成的onvif.h里还是有std::string或者soapcpp2阶段报错提示找不到 C 标准头文件。原因-c参数只影响 wsdl2h 对语言风格的选择但 typemap.dat 如果显式把xsd__string映射成了std::string这个映射会覆盖语言默认行为。也就是说你抄来的 typemap 里有一行xsd__string std::string那不管加不加-c生成的头文件都会依赖 C STL。解决检查并清掉 typemap 里所有std::开头的映射。纯 C 模式下xsd__string直接用char*即可。同理soapcpp2的-c参数也必须在命令行里写出来不能指望从onvif.h里自动推断语言。5.3 请求一直报认证错误或“Expected tag Envelope”现象接口调用返回 SOAP Fault错误串是Expected tag Envelope或者设备返回 401 未认证。原因Expected tag Envelope这类错误九成是访问了错误的 URL设备回了一个 HTML 错误页或者普通 HTTP 内容解析器找不到 SOAP 信封。401 则说明 ONVIF 设备开启了 WS-Security 认证而你的客户端还没有带 UsernameToken。解决先用 curl 直接请求设备 ONVIF 地址看返回是不是一个合法的 XML 信封。如果是 HTML说明服务路径错了换/onvif/device_service之外的其他常见路径试试。认证问题则需要集成 WS-Security稍后在进阶章节里具体讲。这里先说一个排查习惯遇到 XML 解析类错误先抓报文不要猜原因。5.4 链接阶段 undefined reference 到 soap_* 符号现象编译正常链接报几十个 undefined reference全部指向soap_malloc、soap_call、soap_end这类函数。原因stdsoap2.o没有进到链接命令里。gSOAP 生成的onvifClient.c和onvifC.c只是业务层代码底层的序列化、套接字、内存管理全在stdsoap2.c中。很多人用系统包安装 gsoap 后找半天找不到这个文件因为它属于源码包的一部分系统包未必会安装到标准库路径。解决在 gSOAP 源码包里找到stdsoap2.c放进工程编译并链接。注意不要同时链接系统提供的libgsoap.so和源码里的stdsoap2.o会出现重复定义。二选一纯 C 工程建议源码编译可控性最好。5.5 ONVIF 版本差异响应结构体对不上或字段为空现象接口调用成功但响应结构体里关键字段全是空指针或访问响应里的联合体时程序崩溃。原因ONVIF 规范本身分多个版本不同设备对可选字段的填充习惯差异很大。比如GetProfiles返回的视频编码配置有些设备填充Encoding有些设备把这个字段留在扩展结构里gSOAP 生成的响应结构体里该字段是联合体加__union标记处理方式完全不同。另外有些设备实现了新版本规范但返回结构仍按老版本组织和 WSDL 解析出的结构对不上。解决遇到字段为空先用 SOAP 抓包或soap_response记录原始 XML 报文确认设备到底返回了什么。不要急着改代码。如果是联合体字段检查对应的__item或__union标志位再访问。整体上ONVIF 这类多版本协议调试时把原始 XML 留到手里比对着结构体瞎猜高效十倍。我见过太多人卡在“字段为什么是 NULL”上最后发现是设备压根没实现可选字段。6. 进阶只生成要用的接口并让认证一步到位6.1 按需裁剪多个 WSDL 的导入关系如果你只需要设备管理和媒体能力可以在 wsdl2h 命令里只列两个 WSDL但把整个 WSDL 包保留在目录中供 import 解析用。这样生成的 onvif.h 里只包含你关心的接口编译产物也小一圈。不要试图手动删改生成后的头文件那只会让后续重新生成时出现更难查的差异。6.2 认证集成在骨架代码里加入 UsernameTokenONVIF 设备通常要求 WS-Security 认证。常见做法是使用 gSOAP 源码包中 wsse 插件在调用任何接口前设置用户名和密码。代码形如#include wsse.h soap_wsse_set_UsernameToken(soap, admin, password); soap_wsse_set_WSUID(soap, Timestamp);注意 wsse.h 在 gSOAP 源码包的 plugin 目录下wsse.c 需要一并编译进工程。它内部依赖 OpenSSL 的摘要算法所以链接时-lssl -lcrypto不能省。加了这段之后每个发出的 SOAP 请求会自动携带安全的 UsernameToken设备不再返回 401。6.3 验证套路没有真实设备时用本地服务端自测如果你手上暂时没有支持 ONVIF 的设备可以用soapcpp2 -c -S生成服务端骨架在 Linux 上搭一个最小的模拟设备。服务端代码实现soap_serve回调把GetDeviceInformation的响应字段硬编码成测试值然后用生成的客户端去请求http://127.0.0.1:8080。这样可以在不依赖硬件的情况下验证整个生成代码链路是否正常。它不能模拟真实设备的全部行为但足以暴露命名空间、序列化、地址解析这类基础问题。我自己的习惯是每接到一个新平台的 ONVIF 项目先花半天时间把生成、编译、本地自测这套流程完整跑一遍再碰真实设备。这套流程跑顺了后面联调即使遇到问题也知道问题一定出在某台设备自身的行为差异上而不是出在工具链上。gSOAP 生成 ONVIF 框架代码这件事本身就是一次性的工程投入把这一步做扎实后续维护成本会低很多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站