ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux下用gSOAP生成ONVIF框架C代码:完整流程与避坑指南

Linux下用gSOAP生成ONVIF框架C代码:完整流程与避坑指南 简介面向Linux下从事ONVIF视频监控开发的C/C工程师可直接使用由gSOAP工具链生成的ONVIF框架C代码用于对接支持ONVIF协议的网络摄像机和视频设备。压缩包仅1.31MB共12个文件包含5个C源文件、5个头文件以及nsmap、makefile等辅助文件覆盖soap客户端调用、WSDL对应结构体声明和编译构建配置。已有1677人学习/下载。借助这套代码可省去手工编写SOAP/XML通信的繁琐步骤重点掌握soapcpp2生成接口、初始化gSOAP上下文、调用设备信息服务与获取RTSP码流地址的关键路径配合makefile与标准命名映射能较快搭建起可编译运行的ONVIF客户端demo为后续二次开发和业务整合提供直接参考。 接到安防项目的朋友应该都有同感ONVIF这几个字母一出现意味着设备要跟NVR、平台、测试工具做一堆交互查设备信息、拉媒体流地址、PTZ控制、事件订阅……协议规则本身不算复杂复杂的是它底层那套SOAP/XML通信方式。要在Linux环境下用纯C语言手写SOAP报文序列化和解析我头几天干下来直接想放弃。后来改用gSOAP跑通生成流程把一个几万行的WSDL转成C代码框架再往里面填业务逻辑效率完全不一样。这篇文章就把我在Linux上用gSOAP生成ONVIF框架C代码的完整流程、参数拆解和踩坑记录整理出来给准备接入ONVIF的兄弟们一个可以直接参考的路径。这不是一篇纯理论文章我会把从环境安装、规范文件准备、wsdl2h/soapcpp2两条命令的每个参数含义到生成代码怎么编译、怎么接入业务、怎么处理鉴权和多线程的实操细节都写清楚。适合两类人看一是刚接触ONVIF、想知道怎么下手的C语言开发者二是已经在做IPC或NVR固件、想快速给设备加ONVIF服务接口的嵌入式工程师。看完之后你可以直接照着命令复制在自己的工程里跑一遍。1. 为什么安防开发绕不开gSOAP先搞懂这套工具链解决的是什么问题1.1 ONVIF协议的本质是SOAP/XML手写真的会崩溃ONVIFOpen Network Video Interface Forum定义的是网络摄像机、NVR、平台之间的互操作接口。从底层看这些接口全部是基于Web Services的传输层是HTTP/HTTPS消息体是SOAP封装的XML。也就是说你要实现一个GetSystemDateAndTime接口本质上就是往设备发一个HTTP POST请求请求体是一个特定格式的XML信封设备解析后返回另一个XML信封。这本身不算难但ONVIF接口数量庞大一个完整设备涉及Device、Media、PTZ、Event、Imaging、Analytics等十几个模块每个模块下面又有几十个操作。每个操作都需要处理请求结构体的构造、XML命名空间管理、SOAP Fault错误码映射、WS-Security鉴权头。如果全部手写光是把结构体到XML的序列化/反序列化代码写完就是一个不小的体力活。更麻烦的是ONVIF规范版本升级后WSDL里会新增字段或调整类型手写代码就得跟着改一版。1.2 gSOAP在这个链条里的角色gSOAP是一个跨平台的C/C Web Services开发框架它做的事情很直接输入是描述接口的WSDL文件输出是可以直接编译的C或C代码。它内部负责SOAP/XML序列化、命名空间处理、HTTP传输、服务端分发这些脏活累活留给你的只有业务函数。选gSOAP而不是其他方案我当时的考量有三个。第一它对C支持得非常彻底很多IPC固件本身就是纯C的项目引入C编译器会污染现有的编译体系gSOAP的-c参数能生成纯C代码这一点几乎没有替代品。第二gSOAP与ONVIF的配合非常成熟ONVIF规范里各种复杂类型、数组、引用、多态在gSOAP里都有对应处理方式社区里能找到大量基于gSOAP的ONVIF开源实现。第三gSOAP自带WS-Security支持ONVIF鉴权需要的UsernameToken Digest摘要可以直接调用不用自己拼XML。做个简单对比就能看出差异方案上手成本C语言友好度ONVIF兼容性后续维护纯手写SOAP极高需要自研序列化层高需要自己验证规范升级时要大改gSOAP低命令生成后填业务高纯C生成成熟稳定重新生成即可libonvif等第三方封装中中有的需要C覆盖功能有限依赖社区更新如果你只是快速验证一个协议功能第三方封装库确实能省事。但要做到完整覆盖规范、长期维护、和固件深度集成gSOAP是更稳的选择。2. 环境准备与WSDL获取装对版本、拿对规范文件后面才不闹心2.1 Linux下安装gSOAP的两种方式和版本差异gSOAP在Linux下的安装方式有两种。第一种是直接用包管理器Debian/Ubuntu执行sudo apt-get install gsoap这种方式最快装完会在/usr/include/gsoap下放好头文件wsdl2h和soapcpp2两个工具也在PATH里。缺点是版本可能偏低我用过的Ubuntu 20.04仓库里是2.8.75左右用起来没有大问题但老版本在生成某些新规范文件时可能对多命名空间的支持不够好。第二种是从源码编译适合需要最新版本的情况wget https://sourceforge.net/projects/gsoap2/files/gsoap_2.8.124.zip unzip gsoap_2.8.124.zip cd gsoap-2.8 ./configure make sudo make install源码编译会在/usr/local下安装还需要确认gsoap前缀下的import目录位置后面soapcpp2的-I参数要指过去。我个人建议用源码方式因为新版本对OpenSSL 3.x的兼容性更好而且修复了不少老版本在DOM解析和IPv6上的问题。装完验证一下wsdl2h -h soapcpp2 -h能正常打印帮助信息就没问题。2.2 ONVIF规范文件的选择与组织ONVIF的WSDL文件要去官网规范下载页拿。需要注意ONVIF规范分不同Profile和版本常见的是Profile S、Profile T、Profile C、Profile G等。做摄像头接入最常用的是Profile S和Profile T需要下载的核心文件包括devicemgmt.wsdl设备管理必选media.wsdl媒体服务必选ptz.wsdl云台控制按需event.wsdl事件服务按需imaging.wsdl图像设置按需deviceio.wsdlI/O服务按需这些WSDL会依赖多个XSD文件比如common.xsd、ver10/schema/common.xsd下载时要把它引用的所有XSD放在同一目录下。最省事的方式是把ONVIF规范包整个拉下来不用手动一个个挑依赖。规范包下完之后建议建一个干净的目录单独放这些文件比如working/ ├── wsdl/ │ ├── devicemgmt.wsdl │ ├── media.wsdl │ ├── ptz.wsdl │ ├── event.wsdl │ ├── common.xsd │ └── ...其他依赖文件 ├── typemap.dat └── build/把工作目录分开是因为之后wsdl2h会把所有WSDL合成一个头文件中间会产生大量临时文件混在一起容易看花眼。2.3 typemap.dat不配好这个后面会编译到怀疑人生typemap.dat是wsdl2h的配置文件用来指定XML Schema类型与C/C类型之间的映射关系。不配置它直接跑wsdl2h生成的代码里xsd__dateTime、xsd__duration这些类型默认映射到C的std::string或者一串复杂的gSOAP类型在纯C环境下会非常难受编译时各种类型不匹配。我常用的typemap.dat核心内容如下# 基础类型映射 xsd__dateTime time_t xsd__date char* xsd__time char* xsd__duration char* xsd__base64Binary struct soap_dom_element* # 部分ONVIF类型需要强制转成char* tds__ReferenceToken char* tt__ReferenceToken char*这段配置的意思是把ONVIF里大量出现的引用令牌、时间类型直接映射成C语言的time_t或char*业务代码里用起来就顺手多了。xsd__base64Binary映射成DOM元素是因为部分接口需要返回原始的XML片段配合-DWITH_DOM编译宏使用。有些版本的gSOAP自带了typemap.dat模板路径在/usr/share/gsoap/typemap.dat或/usr/local/share/gsoap/typemap.dat。可以在官方文件基础上追加自己的映射避免从头写。3. 执行生成两条命令的完整参数拆解3.1 wsdl2h命令——把WSDL变成C头文件wsdl2h的任务是解析WSDL和XSD生成一个描述接口的C头文件。我在实际项目中使用的命令是cd working wsdl2h -c -s -t typemap.dat -o onvif.h \ wsdl/devicemgmt.wsdl \ wsdl/media.wsdl \ wsdl/ptz.wsdl \ wsdl/event.wsdl \ wsdl/imaging.wsdl逐个说明参数含义-c生成C代码而不是C代码。这是最关键的一个参数不写它默认生成C结构体里全是std::stringC工程直接没法用。-s不使用STL生成代码里不会依赖stlvector.h这类C头文件。-t typemap.dat指定类型映射文件。-o onvif.h指定输出头文件名。这里有个细节要注意把多个WSDL一次性传给wsdl2h比分别生成再手动合并要好得多。ONVIF各模块之间互相引用类型比如media.wsdl里会用到devicemgmt.wsdl里的ReferenceToken一次性传入会让wsdl2h自动处理跨文件依赖生成的命名空间映射也更干净。执行完成后onvif.h就是整个ONVIF接口的数据结构定义。可以打开它看看里面有所有服务的操作函数声明和请求/响应结构体不用改它只是后面soapcpp2的输入。3.2 soapcpp2命令——把头文件变成可编译的C代码soapcpp2是真正的代码生成器它读取onvif.h生成客户端和服务端的调用骨架。我的命令是soapcpp2 -c -x -L -I /usr/include/gsoap -I /usr/include/gsoap/import onvif.h参数拆解-c生成C代码要和wsdl2h保持一致。-x不生成示例XML文件。如果不加这个参数每个接口都会生成一个.xml示例文件一大坨基本用不上。-L不生成soapClientLib.c和soapServerLib.c这两个聚合文件。这两个文件会把所有生成代码include到一个文件里便于快速编译但不利于工程组织我习惯不用它。-I指定导入路径。onvif.h内部会#import stlvector.h之类的文件这些文件在gSOAP的import目录下不指定路径会报错找不到头文件。如果只需要客户端或服务端可以分别加-C或-S但ONVIF开发里设备端通常既要当服务端响应NVR请求也要当客户端主动上报事件所以我默认两者都生成。3.3 生成结果一览哪些文件是宝藏哪些文件可以直接丢跑完soapcpp2之后当前目录会出现一批文件我整理了一个清单文件作用处理方式soapStub.h所有数据结构体定义业务代码里includesoapH.h内部函数声明服务端分发函数在这里编译器自动用soapC.c类型序列化/反序列化核心代码编进工程不要改动soapClient.c客户端调用函数实现编进工程不要改动soapServer.c服务端接口分发实现编进工程不要改动onvif.nsmap命名空间映射表必须编进工程否则链接报错soapClientLib.c / soapServerLib.c聚合文件用了-L就不生成*.xml / *.req / *.res示例消息用了-x就不生成这些代码里我唯一会看的是soapStub.h和onvif.nsmap。前者要写业务代码时查结构体字段名后者在排查SOAP消息解析错误时用来看命名空间前缀映射关系。其余文件一律当黑盒处理千万别手工改否则重新生成时改动就丢了。4. 把生成代码接入工程编译选项、宏定义与链接库4.1 工程目录布局与源文件分组生成代码之后建议在工程里建一个gsoap目录专门放框架代码业务代码单独放。我的习惯结构是project/ ├── src/ │ ├── main.c │ ├── onvif_device_service.c │ └── onvif_client_util.c ├── gsoap/ │ ├── stdsoap2.h │ ├── stdsoap2.c │ ├── soapC.c │ ├── soapClient.c │ ├── soapServer.c │ ├── soapStub.h │ ├── soapH.h │ ├── onvif.nsmap │ ├── wsse.h │ ├── wsse.c │ ├── threads.h │ └── threads.c └── Makefilestdsoap2.h/stdsoap2.c是gSOAP运行时库在/usr/include/gsoap或源码包的gsoap目录下需要拷贝到工程里。wsse.h/wsse.c是WS-Security实现在gSOAP源码的samples/wsse目录下做鉴权必须用。4.2 关键编译宏WITH_DOM、WITH_OPENSSL、WITH_IPV6这块是编译能不能通过的关键。gSOAP的代码里有很多功能开关是通过宏控制的不同的宏影响不同的特性。我做ONVIF项目时用到的几个必开宏CC gcc CFLAGS -Wall -O2 CFLAGS -DWITH_DOM CFLAGS -DWITH_OPENSSL CFLAGS -DWITH_IPV6 CFLAGS -I./gsoap LDFLAGS -lssl -lcrypto -lpthreadWITH_DOM开启DOM解析支持。ONVIF的很多接口在出错时返回原始XML内容部分类型如xsd__anyType也需要DOM能力。不加这个宏遇到包含任意XML元素的结构体解析会出问题。WITH_OPENSSL开启OpenSSL支持WS-Security的摘要计算、HTTPS传输都依赖这个宏。不加的话链接时全是undefined reference to EVP_*这类错误。WITH_IPV6开启IPv6支持。ONVIF 2.0之后很多NVR自动发现和连接会走IPv6不开启的话设备用IPv6地址访问时服务端直接收不到请求。链接库方面-lssl -lcrypto -lpthread是必须的。即使你只用HTTP不用HTTPS只要用了WS-Security鉴权libcrypto就绕不开。-lpthread是因为gSOAP多线程模式下需要pthread支持。还有一个容易忽略的问题onvif.nsmap需要被编译到工程里。常见做法是在main.c或某个全局文件里加一行#include onvif.nsmap不这样做的话链接阶段会报undefined reference to namespaces。4.3 启动一个最小ONVIF服务的代码骨架编译宏配好之后写一个最简服务端入口。gSOAP的服务端模式是典型的socket循环绑定端口、接受连接、分发请求。#include stdsoap2.h #include soapH.h #include wsse.h int main(int argc, char** argv) { struct soap* soap soap_new(); soap-bind_flags SO_REUSEADDR; int port 8080; if (!soap_valid_socket(soap_bind(soap, NULL, port, 100))) { soap_print_fault(soap, stderr); return 1; } for (;;) { if (!soap_valid_socket(soap_accept(soap))) { soap_print_fault(soap, stderr); break; } soap_serve(soap); soap_destroy(soap); soap_end(soap); } soap_free(soap); return 0; }soap_serve(soap)会根据收到的SOAP Action查找soapH.h里声明的服务函数比如客户端调用了GetSystemDateAndTimegSOAP会去调用你在源码里实现的__tds__GetSystemDateAndTime函数。这个函数名是由命名空间的前缀和操作名拼出来的前缀可以在onvif.nsmap里查到设备管理模块通常是tds__开头媒体模块是trt__开头。int __tds__GetSystemDateAndTime(struct soap* soap, struct _tds__GetSystemDateAndTime* req, struct _tds__GetSystemDateAndTimeResponse* resp) { // 查一下系统时间填充resp结构 memset(resp, 0, sizeof(*resp)); // resp-UTCDateTime ... return SOAP_OK; }只要实现了这些接口函数重新编译链接后设备就能响应ONVIF请求了。5. 从Hello到GetSystemDateAndTime第一次鉴权请求怎么跑通5.1 先确认服务端能收到请求框架跑起来之后第一件事不是急着写业务而是确认请求能通。可以用ONVIF Device ManagerODM或SoapUI直接对设备发一个GetSystemDateAndTime请求。如果请求还没走到业务函数就返回错误多半是命名空间映射或者SOAP Action配置不对先查onvif.nsmap里tds前缀对应的命名空间URL是否和规范一致。这一步要特别注意ONVIF规范对SOAP Action的要求比较宽松很多客户端不填SOAP Action也能工作但SOAP body里根元素的命名空间必须严格匹配。gSOAP生成的代码已经处理了这些对应关系只要你自己不手动拼报文一般不会出错。5.2 启用WS-Security鉴权的方式ONVIF默认要求设备端做鉴权常见的是UsernameToken Digest。gSOAP里启用方式很直接先把wsse.c编进工程然后在服务端初始化时设置#include wsse.h // 服务端验证回调 int verify_user(struct soap* soap, const char* user, const char* pass) { if (strcmp(user, admin) 0 strcmp(pass, admin123) 0) { return SOAP_OK; } return SOAP_FAULT; } int main() { struct soap* soap soap_new(); soap-fauthdigest verify_user; // ...绑定、accept、serve }gSOAP收到带UsernameToken的请求时会先执行fauthdigest回调做用户名密码校验校验通过才分发到业务函数。这里我踩过一个坑回调函数返回SOAP_FAULT时如果不想暴露具体的用户名字段需要在回调里把soap-userid清空否则错误信息里会带回URI信息。客户端调用设备时也类似在发起请求前设置用户名密码struct soap* soap soap_new(); soap_wsse_add_UsernameTokenDigest(soap, admin, admin123);这个函数内部会从当前时间戳生成Nonce和Digest并自动加到SOAP Header里。很多第三方工具调试时最容易失败的地方就是设备时间不同步导致Digest不匹配所以ONVIF设备出厂前第一件事永远是校时。5.3 客户端完整调用流程示例以调用设备的GetSystemDateAndTime为例#include soapClient.h #include soapH.h #include wsse.h int get_device_time(const char* device_url) { struct soap* soap soap_new(); soap_set_mode(soap, SOAP_C_UTFSTRING); struct _tds__GetSystemDateAndTime req; memset(req, 0, sizeof(req)); struct _tds__GetSystemDateAndTimeResponse resp; memset(resp, 0, sizeof(resp)); soap_wsse_add_UsernameTokenDigest(soap, admin, admin123); int ret soap_call___tds__GetSystemDateAndTime(soap, device_url, NULL, req, resp); if (ret SOAP_OK) { // resp.UTCDateTime 里就是设备时间 printf(UTC: %04d-%02d-%02dT%02d:%02d:%02dZ\n, resp.UTCDateTime-DateTime-Date.Year, resp.UTCDateTime-DateTime-Date.Month, resp.UTCDateTime-DateTime-Date.Day, resp.UTCDateTime-DateTime-Time.Hour, resp.UTCDateTime-DateTime-Time.Minute, resp.UTCDateTime-DateTime-Time.Second); } else { soap_print_fault(soap, stderr); } soap_destroy(soap); soap_end(soap); soap_free(soap); return ret; }注意几个细节。第一所有生成的请求/响应结构体一定要用memset清零否则gSOAP序列化时会认为某些指针是野指针直接崩溃。第二soap_call___tds__GetSystemDateAndTime这个函数名里的tds__前缀必须和onvif.nsmap里一致不同版本规范可能不一样以生成的实际名字为准。第三soap_set_mode(soap, SOAP_C_UTFSTRING)要设ONVIF的XML默认UTF-8编码不设的话gSOAP可能按本地编码输出中文描述字段会乱码。6. 深度使用中的五个高频坑与我的处理方式6.1 多线程场景下的soap结构体隔离问题gSOAF的服务端默认是串行处理请求的accept一个连接处理完再accept下一个。如果你的设备需要同时响应多个客户端的请求这种串行模型会阻塞。我第一版直接把它放进线程里每个连接新建一个线程处理结果发现并发请求乱套了。原因在于gSOAP的struct soap不是线程安全的一个soap对象同时只能处理一个请求。正确做法是每个线程维护独立的soap实例。gSOAP提供了soap_copy接口可以在accept之后复制一个soap给工作线程主线程继续acceptstruct soap* worker_soap soap_copy(soap); // worker线程里使用worker_soapsoap_copy会复制监听所需的上下文但连接套接字会转移到副本上主线程的soap可以继续accept下一个连接。这是一个高频且隐蔽的坑不处理好在压力测试时必现崩溃。6.2 IPv6和双栈监听现在很多ONVIF测试工具默认用IPv6发现设备如果你的服务只绑了IPv4的0.0.0.0测试工具的Device Discovery第一步就失败。开启方式前面说了编译时加-DWITH_IPV6绑定时把host参数传[::]gSOAP会自动进入双栈模式同时监听IPv4和IPv6soap_bind(soap, [::], port, 100);这里有个细节双栈模式下如果系统禁用了IPv6soap_bind会直接失败。稳妥的做法是先用[::]尝试绑定失败再回退到NULL等价于IPv4任意地址。我在一个老平台上就遇到过内核没编IPv6的情况加个回退逻辑代码稳很多。6.3 soap_destroy、soap_end、soap_free的释放顺序gSOAP的内存管理有一套自己的生命周期。soap_malloc分配的内存、反序列化时自动生成的对象都挂在soap对象的内部链表上。soap_destroy负责释放所有用户自定义类型对象soap_end释放soap上下文里其他临时分配的内存soap_free释放soap对象本身。释放顺序错了会导致段错误。服务端循环里处理完一个请求后应该soap_destroy(soap); soap_end(soap);不要调用soap_free因为主循环还要继续用这个soap对象接收下一个连接。整个进程退出前才soap_free。在客户端场景一次调用结束后把三步都执行完。这是一个很小但很常见的崩溃源我见过好几个同事在这个上面查了半天。6.4 超时参数与长时间运行的内存增长ONVIF设备处于局域网内但客户端如果拔网线或者异常断开HTTP请求可能长时间挂起。gSOAP默认没有超时限制需要显式设置soap-recv_timeout 5; soap-send_timeout 5; soap-connect_timeout 5; soap-accept_timeout 3;单位是秒。recv_timeout和send_timeout分别控制收发超时这在处理异常客户端时很重要。还有一个隐藏问题长时间运行的服务端如果每次请求处理完没有调用soap_destroy和soap_end内存会持续增长进程跑个几天后RSS暴涨。正常循环里这两个调用必须成对出现一个都不能省。6.5 第三方库头文件冲突ONVIF工程里通常还集成别的协议栈比如RTSP、HTTP server、JSON库。gSOAP生成的soapStub.h里定义了大量的结构体命名以_tds__、_trt__等为前缀冲突概率不大。真正麻烦的是soapH.h里会includestdsoap2.h这个头文件定义了一些通用的宏和类型比如BOOL、UINT32如果工程里其他库也定义了同名类型编译会直接报重定义。我的处理方式是把gSOAP生成代码单独编译成一个静态库业务代码只通过接口头文件和它对接不直接includesoapH.h。这样既隔离了类型冲突也缩短了业务模块的重编译时间。如果非要在一个编译单元里混用注意第一行加#define WITH_NO_C_LOCALE可以避免部分locale相关的类型冲突这招在嵌入式环境里经常救急。6.6 WS-Discovery发现协议的最简接入思路最后提一下设备的自动发现。ONVIF规范里发现走的是WS-Discovery在UDP 3702端口上监听多播消息响应Probe请求返回设备地址。gSOAP里完整的WS-Discovery实现可以参考samples/wsdd示例但完整集成工作量不小。如果只是要让ONVIF Device Manager能发现设备有个更快的办法直接用gSOAP的wsdd库接口在初始化时调用soap_wsdd_probe相关函数或者在UDP套接字上接收Probe并回一个ProbeMatch。先用手工构造UDP响应的方式把设备地址返回业务跑通后再逐步换成正规的gSOAP wsdd实现。这个路径我走过比一上来就啃完整的WS-Discovery规范要快得多。我自己在实际项目里的体会是gSOAP这套生成流程一旦走通后续填业务函数就是一个没有技术风险的体力活。最耗时间的阶段就是环境配置、WSDL选择、typemap调整这个前置环节而这篇文章里记录的这些细节基本上就是把我当时踩过的坑提前给你趟平了。如果你手里的项目也卡在ONVIF接入这一步照着这个流程跑一遍应该能在两天内看到自己的设备被ONVIF客户端正常发现和访问。本文还有配套的精品资源点击获取
返回列表