📹 第八章 ONVIF框架代码案例应用
📄 一句话概括

soapcpp2 把 onvif.h 变成八个源文件,-2 那个参数是 ONVIF 的硬要求。 417 个操作里有 86 个重名, 靠命名空间前缀才不撞车——所以命名空间表不是装饰,它是函数名的一部分。

📝 本节和前面几节的关系

上一节产出的 onvif.h 只是「描述」,这一节把它变成「代码」。 这是第八章倒数第二个「纯命令」的步骤,跑完之后就能写业务代码了。 它也是整章最后一个「一次生成、后面全靠它」的动作。

一、四个参数里 -2 最关键

参数 作用 不加会怎样
-2 生成 SOAP 1.2 绑定 只有 1.1,ONVIF 不认
-c 生成 C 和 -c 生成的头文件不搭
-x 不生成 XML 注释 文件大一圈,内容不变
-I import 去哪找 #import 的头 找不到 wsse.h,直接报错

第一行是本节唯一「不改就跑不通」的参数:ONVIF 全用 SOAP 1.2, 加了 -2 才会生成 application/soap+xml 的绑定。 不加的表现很难查:客户端发出去的是 SOAP 1.1 信封(text/xml), 设备要么回 400,要么直接断开—— 而两边的 XML 都是合法的,抓包看结构也差不多,只是信封命名空间不同。 第四行那个「直接报错」反而是好事:它至少让你当场知道少了头文件, 比那些「生成了但运行期才错」的参数友好得多。

二、八个产物各是什么

文件 是什么 客户端要不要
soapStub.h 所有类型的声明,最大的一个 要
soapH.h 函数原型与全局声明 要
soapC.cpp 序列化与反序列化 要
soapClient.cpp 客户端调用函数 要
soapServer.cpp 服务端分发 不要
soapClientLib.cpp 客户端库入口 可选
soapServerLib.cpp 服务端库入口 不要
*.nsmap 命名空间表 要

第三列那个「可选」和「不要」的区别在于「入口」: soapClientLib.cpp 里是 soap_client_init() 这类便捷函数, 如果你自己写 soap_init() 就不需要它。 而 soapServer.cpp 是「不要」:它包含服务端的 soap_serve 分发逻辑, 客户端带上它不会报错,但会多一堆用不到的符号,链接出来的 exe 大几百 KB。 最大的那个是 soapStub.h:它要装下 1810 个 element 的结构体定义, 打开它会卡编辑器——所以别在它上面用「全局搜索」,用「只在 onvif.h 里搜」。

三、417 个操作、86 个重名

这是本节最值得算的一个数:

项 数 说明
总操作数 417 14 个 WSDL 全算上
去掉重名 331 —
重名个数 86 同一个名字在多个 service 里都有
重得最多 GetServiceCapabilities 13 个 service 里都有

第三行那个 86 听着像 bug,其实是设计:ONVIF 每个 service 都提供 GetServiceCapabilities, 名字一样、语义一样、参数不一样。 所以生成的函数名必须带前缀: soap_call___tds__GetServiceCapabilities 与 soap_call___trt__GetServiceCapabilities 是两个函数。 前缀里那两个下划线是 gSOAP 的约定: 一个下划线分隔 soap_call 与命名空间,命名空间后面又是两个下划线接操作名。 这串东西不能手打,只能靠生成——打错一个下划线就是「未定义函数」。

四、生成完先编一遍

这一步很多人都跳过,然后带着「生成成功」的心态去写业务代码, 结果第一次编译就被一堆和业务无关的错误埋住。

做法 好处 代价
先单独编一次生成物 把「生成的问题」和「业务的问题」分开 两分钟
直接写业务代码 省两分钟 两类错误混在一起,排查翻倍

第一行是「拿两分钟换半小时」的典型: 生成物如果能单独编过,说明头文件、命名空间、wsse.h 都对, 后面业务代码报的错就一定是业务代码的错。 第二行的代价是隐性的:你会在 soapStub.h 的几万行里找自己那行代码的错误, 而编译器报的行号全在生成文件里。 所以我的习惯是「生成完先编一次空工程」,这一步花的时间,后面一定省回来。

五、动手 Demo:soapcpp2 生成那一堆文件

Demo 不跑 soapcpp2(本机没有),而是算清楚「它会生成多少函数」—— 用真 WSDL 数出操作数,再看重名情况。 这是理解「为什么函数名必须带前缀」的唯一硬证据。

# 14 个 WSDL 的操作名,看有多少重名
import collections
allops = [op for fn in SVC for op in _ops(fn)]
c = collections.Counter(allops)
dup = [k for k, v in c.items() if v > 1]
print(len(allops), len(c), len(dup))
print(c["GetServiceCapabilities"])   # 13

实跑输出:

--------------------------------------------------------------------
soapcpp2 生成那一堆文件
--------------------------------------------------------------------
**`wsdl2h` 产出的是「描述」,`soapcpp2` 才产出「代码」。**
**命令就一条**:

  `soapcpp2 -2 -c -x -I import onvif.h`

  参数            作用                         不加会怎样
  ----------------------------------------------------------------------------
  `-2`            **生成 SOAP 1.2 的绑定**     只有 1.1,ONVIF 不认
  `-c`            生成 C                       生成 C++,和 `-c` 的头文件不搭
  `-x`            **不生成 XML 注释**          文件大一圈,内容不变
  `-I import`     去哪找 `#import` 的头        找不到 `wsse.h`,直接报错

**第一行是 ONVIF 的硬要求**:ONVIF 全用 SOAP 1.2,
**不加 `-2` 生成的客户端会发 1.1 信封**,设备回 400 或直接断开。
**这个错很难看出来**,因为两边都是合法 XML,只是命名空间不一样。

**产物是固定的八个文件**,用途分两类:

  文件                    是什么                               客户端要不要
  --------------------------------------------------------------------------
  `soapStub.h`            **所有类型的声明(最大的一个)**     要
  `soapH.h`               函数原型与全局声明                   要
  `soapC.cpp`             **序列化与反序列化的实现**           要
  `soapClient.cpp`        客户端调用函数                       **要**
  `soapServer.cpp`        服务端分发函数                       **不要**
  `soapClientLib.cpp`     客户端库的入口                       可选
  `soapServerLib.cpp`     服务端库的入口                       不要
  `*.nsmap`               **命名空间对照表**                   要

**第三列是「只写客户端」时的取舍**:
**带了 `soapServer.cpp` 不报错但会多一堆链接符号**,
**体积大一点,功能一点不用**。

**然后是这一节最值得算的一个数:417 个操作会生成多少函数。**

**14 个 WSDL 一共 417 个操作,但去掉重名只剩 331 个**——重名 64 个。
**重得最多的是 `GetServiceCapabilities`**:13 个 service 里都有它。
**那生成出来的函数岂不是要撞车?不会**——
**`soapcpp2` 给函数名加了命名空间前缀**:

  `soap_call___tds__GetServiceCapabilities(...)`
  `soap_call___trt__GetServiceCapabilities(...)`

**`tds` 与 `trt` 那两个前缀就是从 `*.nsmap` 里来的。**
**所以「命名空间表」不是装饰,它是函数名的一部分**——
**少了哪一项,哪一族的函数就生成不出来。**

**顺带一个数**:每个操作生成「一个调用函数 + 一个服务函数 + 一个回调指针」,
**所以 417 个操作对应的函数量级是 417 到 1251 之间**。
**这也是 `soapStub.h` 动辄几万行的原因**。

这段代码只干一件事:证明「重名是真的」—— 417 个操作里 86 个重名,而 GetServiceCapabilities 有 13 个。 注意它统计的是「14 个 WSDL 全算上」,实际工程只喂一套 ver10 或 ver20, 所以真实的重复会少一些—— 但 GetServiceCapabilities 在 ver10 内部也重复,因为每个 service 都有它。 这也解释了一个常见现象: 在别人的代码里看到 soap_call___tds__GetServiceCapabilities 这种长名字, 不要以为是命名不规范——那是生成的,改不了。 最后那句「函数量级是 417 到 1251」是按「每操作 1 到 3 个函数」算的, 实际还包括序列化函数与命名空间函数,所以别把它当精确值,它只说明「量级」。

30 秒实验

  1. 把 _ops() 的返回按 service 分组打印,找出「只在 ver20 里才有」的操作。
  2. 数一下 GetServiceCapabilities 在 ver10 内部出现在几个 service 里。
  3. 查一下 gSOAP 文档里 soap_call_ 的命名规则,看那两个下划线怎么来的。

常见误区

  1. 不加 -2。 生成的客户端会发 SOAP 1.1 信封,设备回 400 或直接断开, 而两边 XML 都合法,极难查。
  2. 带 soapServer.cpp 编客户端。 不报错,但多一堆符号,链接出来的 exe 大几百 KB。
  3. 把 soap_call___tds__Get… 这种名字当成「命名不规范」。 那是生成的,前缀就是命名空间, 改不了也不该改。
自测三问(点开对答案) 1. `-2` 这个参数为什么不能漏? —— **ONVIF 全用 SOAP 1.2**,不加 `-2` 生成的是 1.1 绑定, **客户端发出去的信封命名空间不对,设备直接拒。** 2. 为什么 417 个操作里有 86 个重名却不撞车? —— **因为 `soapcpp2` 给函数名加了命名空间前缀**, **`tds` 与 `trt` 的 `GetServiceCapabilities` 是两个不同的函数。** 3. 生成完为什么要先单独编一次? —— **为了把「生成的问题」和「业务的问题」分开**。生成物编过了,后面报的错就一定是业务代码的错。

下一节讲命名空间表:十项,少一项就是 unbound prefix。

相关笔记