☰
用gSOAP生成ONVIF C代码:从WSDL到服务端完整指南
2026/10/20 16:40:44 网站建设 项目流程

简介:面向Linux下基于ONVIF协议开发网络视频设备的C/C++工程师,这份gz压缩资源包提供了一套用gSOAP从WSDL生成ONVIF客户端框架C代码的完整实现,覆盖从协议解析、工具链使用到代码生成与集成的主要环节。包内共12个文件,包含5个C源码、5个头文件、1个nsmap映射与1个makefile,整体约1.31MB;C源码涵盖SOAP客户端调用、XML编解码、设备信息查询示例,头文件定义了Device、Media等ONVIF服务接口及gSOAP运行所需的数据结构,nsmap文件提供XML命名空间映射,makefile可快速完成编译链接。已有1692人学习该资源。通过该包可完整理解soapcpp2预处理流程,掌握客户端上下文初始化、设备信息获取、RTSP码流地址获取及错误处理等关键操作,代码结构清晰、注释明确,既可作为实际工程项目模板,也可作为学习gSOAP工具链、ONVIF协议与物联网视频监控开发的参考素材,适合具备一定C语言基础的开发者深入研究。

1. 用gSOAP生成ONVIF框架C代码:先信这条反直觉的路再动手

做安防设备的ONVIF对接,最扎手的往往不是业务逻辑,而是SOAP/XML那一层让人头大的报文组装。我见过不少团队第一版都是手拼XML字符串,结果客户端联调时一个字段对不上就要翻半天,改一处解析逻辑牵连一片。gSOAP的思路刚好反过来:它把WSDL里定义好的类型和方法直接翻译成C结构体和函数骨架,开发者只需要填业务实现,协议层的反复验证交给生成代码。这套工程讲的是Linux下用gSOAP把ONVIF的WSDL批量生成C框架代码的完整链路,覆盖环境安装、头文件生成、服务端移植、WS-Security鉴权接线,以及二次开发时怎么加自己的方法。适合准备接入ONVIF网关的嵌入式C开发者,也适合想弄明白gSOAP生成代码内部结构的后端同学。

2. 环境与工具链:gSOAP安装、wsdl2h和soapcpp2的分工

2.1 依赖检查与编译安装

gSOAP不是一条命令就能装完的,它是一套需要先编译进本机的代码生成工具。在我的经验里,安装前会先确认三样东西:gcc、openssl头文件、flex和bison。老一批gSOAP发行包用flex/bison生成解析器,新版本虽然自带XML解析,但编译时仍可能检测这两个工具,所以建议先把它们装上。openssl则是为后面ONVIF的UsernameToken鉴权准备的,不装的话wsse插件基本没法用。

# 以Debian系为例,不同发行版包名略有差异 sudo apt-get install -y build-essential libssl-dev flex bison # 解压源码包并编译 tar xzf gsoap*.tar.gz cd gsoap-*/ ./configure --enable-plugins --enable-ssl make -j"$(nproc)" sudo make install

这里两个configure开关要单独说。--enable-plugins决定gSOAP的import目录里会不会生成wsse.h、wsaddressing.h这些插件头文件;--enable-ssl则是让后面的服务端能用上soap_ssl_init以及WS-Security相关函数。如果你在configure阶段图省事不开ssl,后面手工加入wsse.c编译时会很痛苦,一堆soap_wsse_函数都依赖OpenSSL回调。

装完先验证工具是否进入PATH:

wsdl2h -h | head -n 20 soapcpp2 -h | head -n 20

能列出选项说明工具已经就位。装完还要留意import目录的位置,一般在/usr/local/share/gsoap/import。这个目录里面放着xsd.h、wsse.h这些公共定义,后续跑soapcpp2时,-I参数指向的就是这里。很多人第一次跑生成命令就翻车,查到最后是import目录没指对,工具找不到公共头文件,报错信息还很模糊。

2.2 wsdl2h与soapcpp2的职责划分

这两个工具名长得太像,我第一次把它们的职责彻底记反了,白白折腾了一天。现在一句话讲清楚:wsdl2h的输入是WSDL/XSD,输出是.h头文件;soapcpp2的输入是.h头文件,输出是.c源码。也就是说,wsdl2h负责把XML世界里的schema变成C语言能看懂的结构体声明,soapcpp2再按这些声明生成序列化代码和客户端/服务端骨架。ONVIF的WSDL是固定的,所以完整链路就是“WSDL → 头文件 → C代码”两条命令,中间那个头文件是唯一的交流契约。

工具输入主要输出常用参数
wsdl2h.wsdl / .xsdonvif.h-o、-t、-I、-P、-s
soapcpp2.hsoapC.c、soapServer.c、soapClient.c-c、-I、-x

wsdl2h最常用的参数组合是-o指定输出、-t指定typemap映射文件、-I指定包含目录。soapcpp2的-c是强制只生成纯C代码,不生成C++版本;-I告诉它去哪找公共头文件;-x是顺手不生成示例XML,减少目录噪音。实际生成时不能只用默认参数,因为ONVIF的xsd类型嵌套非常深,typemap不提前调整,生成的代码会很难下手。

还有一点容易绕进去:soapcpp2只吃头文件,不能直接吃WSDL。如果跳过wsdl2h,直接对wsdl文件跑soapcpp2,它会直接报解析错误。我理解的这套生成逻辑是:头文件相当于一个可读、可裁剪、可注释的中间产物,团队里谁接手都能先看结构,再决定要不要调整映射。这也是我后来喜欢gSOAP胜过其它代码生成器的原因——生成过程不是黑匣子,每一步都留了人工干预的接口。

3. 生成链路拆解:从WSDL到C结构体到服务端骨架

3.1 准备WSDL:目录结构是第一步

ONVIF的接口定义分散在多个wsdl和xsd文件里,设备管理、媒体、PTZ、事件各有入口。拿到标准包后,我的做法是先把整个wsdl目录平铺在一个目录下,不要在子目录里到处分散,这样wsdl2h解析时不需要补一堆相对路径。

mkdir onvif_wsdl && cd onvif_wsdl cp /path/to/ONVIF/*.wsdl /path/to/ONVIF/*.xsd . ls -1

想确认文件是否齐全,主要看两个:common.xsd和types.xsd是类型定义的底座,devicemgmt.wsdl是设备服务的主入口。如果目录里缺了common.xsd,wsdl2h会直接报无法加载外部schema的错误。ONVIF的wsdl依赖环环相扣,缺任何一个底层定义,整个解析都会断掉,所以目录齐整不是洁癖问题,而是生成能不能跑通的前提。

3.2 wsdl2h生成onvif.h

目录准备好之后,跑第一条生成命令,把设备管理、媒体、PTZ、事件四个wsdl一起喂给wsdl2h:

wsdl2h -o onvif.h -t typemap.dat -I . \ devicemgmt.wsdl media.wsdl ptz.wsdl event.wsdl

如果目标环境比较严格,只允许纯C,可以额外加-P和-s,前者去掉C++包装层,后者减少STL相关的vector/string映射。命令执行完,当前目录会出现一个体积不小的onvif.h。用grep快速验证一下结构体是否真的生成:

grep -n "struct _tds__GetDeviceInformation" onvif.h grep -n "tt__ReferenceToken" onvif.h

有结果说明WSDL已经被转换成C声明。这步常见的认知偏差在于:wsdl2h生成的不是可编译的完整工程,而是一份C语言语义的schema。它把每个操作包装成struct _tds__GetDeviceInformation和struct _tds__GetDeviceInformationResponse这样的请求/响应结构体,ONVIF里庞大的索引类型tt__ReferenceToken也在这里定义。后面写业务代码时,主要打交道的就是这些struct。

3.3 soapcpp2生成服务端源码

onvif.h拿到手,先别急着改业务,直接跑soapcpp2生成骨架:

soapcpp2 -c -I /usr/local/share/gsoap/import -x onvif.h

-c是纯C模式,-I必须指向gSOAP的import目录,-x不生成示例文件。执行完目录里会多出一批文件,每个文件都有自己的用途:

生成文件职责说明
soapStub.h请求/响应结构体定义,业务代码主要include它
soapH.h服务函数声明和序列化函数原型
soapC.c所有结构体的序列化与反序列化实现
soapClient.c客户端调用远端ONVIF服务的存根
soapServer.c服务端每个操作的回调入口
ns.nsmapnamespace前缀与URI的映射表

其中soapServer.c是设备端开发的重心。它会把wsdl2h里每个操作翻译成一个可覆写的函数,函数名形如soap_serve__DeviceBinding_GetDeviceInformation,具体命名跟namespace前缀有关系。我的做法是先不填任何业务,把生成的soapServer.c原封不动编译一次,确认空骨架能通过链接,再开始填实现。这样能把“生成问题”和“业务问题”彻底隔离开。

ns.nsmap也需要打开看一眼,它每一行对应一个XML命名空间前缀,比如tds映射到设备管理WSDL的命名空间URI。如果多个wsdl里有重复前缀,soapcpp2会生成带特殊区别符的命名,但后续对外交互还是要保证前缀能和ONVIF客户端认得的URI对上。

3.4 typemap.dat:低成本优化生成结果

typemap.dat是wsdl2h的文本配置,用来干预XSD类型到C类型的映射。处理ONVIF时我必改两类映射:第一,把ONVIF自定义的tt__ReferenceToken映射成char*,否则生成代码里会多一个指向struct的小盒子,访问起来很啰嗦;第二,xsd__dateTime默认会映射成struct tm,而ONVIF很多实现直接用字符串传递时间,映射成char*更省事。

# typemap.dat 片段 tt__ReferenceToken = | xsd__string | char* xsd__dateTime = | xsd__string | char*

改完重新跑一次wsdl2h,再跑soapcpp2,然后grep确认结构体成员确实变成char*。这一步能让后面设备端的序列化和反序列化负担小很多,是我觉得整个链路里性价比最高的一次修改。

4. 服务端落地:把生成代码编译成可鉴权的ONVIF设备服务

4.1 服务入口与soap_serve调度

生成的soapServer.c里最核心的对外函数是soap_serve。它一次只处理一个请求,内部做了soap_begin_recv、按body根元素分发到对应回调、回包封装的整套动作。要让设备真正常驻网络,还需要在main函数里自己写循环,用soap_bind和soap_accept接受连接,再调用soap_serve。

#include "soapH.h" #include "ns.nsmap" int main(void) { struct soap *soap = soap_new(); if (!soap) return -1; if (soap_bind(soap, NULL, 8080, 16) < 0) { soap_print_fault(soap, stderr); return -1; } for (;;) { if (soap_accept(soap) >= 0) { soap_serve(soap); soap_end(soap); } } soap_free(soap); return 0; }

soap_bind的四个参数分别是上下文、监听地址、端口和backlog。地址传NULL表示监听所有网卡,ONVIF设备经常要求在多个网卡上都可达,所以不建议写死单个IP。soap_serve是生成在soapServer.c里的全局函数,本身不是循环,所以别指望它自己跑起来。每次连接处理完如果不调soap_end,soap_malloc分配的内存池会只增不减,这在长期运行的设备上就是慢速内存泄漏。

4.2 让鉴权工作起来:WS-Security回调链

ONVIF要求设备接口带WS-Security UsernameToken,gSOAP的wsse插件正是为这个准备的。首先要在onvif.h头部加一行#import "wsse.h",然后重新跑soapcpp2,这样soap上下文里才会有校验回调相关的字段。接着在业务代码里实现一个鉴权函数,把它挂到soap->fvalidate上。

#include "wsse.h" static int on_validate(struct soap *soap) { if (soap_wsse_check_UsernameToken(soap, "admin", "123456", 300) != SOAP_OK) { /* 校验失败,返回SOAP_ERR,框架会生成认证失败的Fault */ return SOAP_ERR; } return SOAP_OK; } /* 在main函数初始化soap之后挂载 */ soap->fvalidate = on_validate;

这里soap_wsse_check_UsernameToken的四个参数分别是上下文、用户名、密码、允许的时间偏差。300表示允许客户端系统时间和设备最多差5分钟,防止报文重放。如果要做的是测试工具而不是嵌入式设备,客户端侧也可以用soap_wsse_add_UsernameToken生成请求头,两边的插件是同一套,不需要额外改协议。

4.3 工程化编译:Makefile与链接参数

生成的C代码和gSOAP运行时是分开编译的,需要一个Makefile把它们组织起来。典型结构如下:

CC = gcc CFLAGS = -I. -I/usr/local/share/gsoap/import \ -DWITH_DOM -DWITH_OPENSSL -D_GNU_SOURCE OBJS = main.o soapC.o soapServer.o stdsoap2.o dom.o wsse.o LIBS = -lssl -lcrypto onvif_server: $(OBJS) $(CC) -o $@ $^ $(LIBS) %.o: %.c $(CC) $(CFLAGS) -c -o $@ $<

stdsoap2.c是gSOAP运行时,必须和soapC.c一起编译;dom.o提供XML DOM能力,wsse.o提供鉴权插件,这两个文件都要从gSOAP源码目录拿过来。-DWITH_OPENSSL会开启SSL相关回调,别省掉这个宏,否则soap_wsse_函数里用到的OpenSSL符号会缺失。-D_GNU_SOURCE在glibc环境里也很关键,soapClient代码里某些系统调用在严格编译模式下会被隐藏,缺了它容易从warning演变成链接错误。

提示:主程序文件顶部include了ns.nsmap,所以这里不需要单独编译ns.nsmap,避免重复定义namespace数组。

5. 避坑与排查:gSOAP生成ONVIF代码最常见的四个坑

5.1 wsdl2h报错:提示无法加载common.xsd

现象:在全新目录下执行wsdl2h,命令跑到一半就退出,错误提示类似failed to load common.xsd,或者无法打开外部schema文件。

原因:WSDL文件里通过相对路径引用了common.xsd,而wsdl2h在当前工作目录找不到它。ONVIF的wsdl依赖环环相扣,缺一个底层xsd,整个解析就会断掉。这不是网络问题,是本地路径和搜索路径的问题。

解决:把同一个SDK里的所有.xsd和.wsdl放在同一个目录,执行时加上-I .。wsdl2h解析时用的是-I指定的搜索路径,跟当前shell的工作目录并不完全等价。我在实践里习惯先ls确认common.xsd已经在目录里,再跑命令,能省掉一轮无效排查。

5.2 链接时报undefined reference:soap_wsse开头函数

现象:代码写好了,链接阶段冒出一排soap_wsse_add_UsernameToken、soap_wsse_check_UsernameToken的undefined reference。

原因:最常见的是只编译了soapC.c和soapServer.c,没有把gSOAP源码目录里的wsse.c、dom.c一起加进工程,或者configure阶段没有开启ssl。wsse插件依赖OpenSSL,soap_wsse_*符号分散在wsse.c和stdsoap2.c的WITH_OPENSSL分支里,少任何一环都会缺符号。

解决:把wsse.c和dom.c加入源文件列表,同时确保编译宏里带了-DWITH_OPENSSL -DWITH_DOM,最后链接-lssl -lcrypto。如果这样还缺符号,就要检查onvif.h里是否成功import了wsse.h,没处理的话上下文里连fvalidate字段都没有,插件压根没被编译进骨架。

5.3 运行期Segfault:soap_end之后还在用soap_malloc的指针

现象:回调里用soap_malloc申请了一块内存,填充完字符串后函数返回,下一次请求处理时访问这块地址直接崩溃,用gdb看指针值也没变,就是内容没了。

原因:gSOAP的内存模型是上下文内存池,soap_malloc分配的内存会在soap_end时统一释放。服务端每处理完一次请求就在主循环末尾调soap_end,所以你在回调里留给全局变量的指针已经失效了。用惯了malloc/free的人在这里很容易踩坑。

解决:需要长期存活的数据用标准malloc分配,自己负责free;回调里要返回给客户端的数据则用soap_malloc,因为soap_serve回包前不会释放。我在代码里把这条规则写成了注释:回给soap_serve响应的用soap_malloc,存到业务层全局的用malloc。

5.4 调用设备接口全是SOAP_FAULT:namespace前缀对不上

现象:设备能被ONVIF探测到,但GetDeviceInformation返回SOAP_FAULT,调试工具里能看到返回报文的prefix前缀和请求里的SOAPAction不一致,或者命名空间URI缺了一段。

原因:多个WSDL一起生成时,gSOAP会按wsdl2h默认规则把每个wsdl映射成不同前缀,比如tds、trt、tptz。如果typemap.dat提前改过前缀映射,或者多个命名空间被强行合并成同一个前缀,生成的ns.nsmap里就会出现重复定义,服务端解包时按第一个匹配前缀解析内容,自然对不上。

解决:打开ns.nsmap,把重复前缀重命名成不冲突的标识,同时把修改同步到typemap.dat,然后从wsdl2h开始重新跑一遍生成命令。改完再编译,抓包对比SOAPAction和报文根节点是否一致。这个坑最隐蔽的地方在于错误信息不会直接说是前缀冲突,而是以SOAP_FAULT形式出现。

6. 二次开发:把默认服务改造成线程池和新业务接口

6.1 并发处理的正確姿势

默认main是串行处理,一个请求没处理完,后面的连接只能等。在IPC设备上勉强够用,但要做网关就吃力了。gSOAP推荐的做法是soap_copy复制上下文,每个线程持有独立soap拷贝,而不是共享同一个指针。

void *handle_request(void *arg) { struct soap *tsoap = soap_copy((struct soap *)arg); if (tsoap) { soap_serve(tsoap); soap_end(tsoap); soap_free(tsoap); } return NULL; }

soap_copy会复制协议状态,包括超时设置和namespace表,比重新soap_new再保留绑定信息要可靠得多。多个线程不能共用同一个soap指针调用soap_serve,那是崩溃源头,也是我最早踩过的最深坑。

6.2 新增自定义接口的纪律

ONVIF标准外的私有接口,不建议直接修改生成的soapServer.c再expect它不被覆盖。我习惯把自定义逻辑全部放到独立源文件里,在soap_serve分发之前,先用soap_peek_element识别自定义根节点名,匹配到就走自己的实现,匹配不到再交给gSOAP默认分发。下次重新生成时只覆盖soapServer.c,自定义文件完全不受影响。

这套流程走顺之后,ONVIF对接从一个玄学问题变成了一个机械问题。我最早把gSOAP生成代码当成不能碰的黑匣子,后来被覆盖生成坑过一次才明白:生成代码是脚手架,改它必须有纪律。从那以后,我每次拿到新版WSDL,都先在干净目录里跑完wsdl2h和soapcpp2,确认骨架能编译、鉴权能通过,再去填业务字段;新增方法一律独立成文件,绝不往soapServer.c里硬塞。希望帮到你。

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

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

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

立即咨询