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 秒实验
- 把
_ops()的返回按 service 分组打印,找出「只在 ver20 里才有」的操作。 - 数一下
GetServiceCapabilities在 ver10 内部出现在几个 service 里。 - 查一下 gSOAP 文档里
soap_call_的命名规则,看那两个下划线怎么来的。
常见误区
- 不加
-2。 生成的客户端会发 SOAP 1.1 信封,设备回 400 或直接断开, 而两边 XML 都合法,极难查。 - 带
soapServer.cpp编客户端。 不报错,但多一堆符号,链接出来的 exe 大几百 KB。 - 把
soap_call___tds__Get…这种名字当成「命名不规范」。 那是生成的,前缀就是命名空间, 改不了也不该改。
自测三问(点开对答案)
1. `-2` 这个参数为什么不能漏? —— **ONVIF 全用 SOAP 1.2**,不加 `-2` 生成的是 1.1 绑定, **客户端发出去的信封命名空间不对,设备直接拒。** 2. 为什么 417 个操作里有 86 个重名却不撞车? —— **因为 `soapcpp2` 给函数名加了命名空间前缀**, **`tds` 与 `trt` 的 `GetServiceCapabilities` 是两个不同的函数。** 3. 生成完为什么要先单独编一次? —— **为了把「生成的问题」和「业务的问题」分开**。生成物编过了,后面报的错就一定是业务代码的错。下一节讲命名空间表:十项,少一项就是 unbound prefix。
- ☐ (本节完,下一节 08.01.07 关联自己的命名空间)