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

soapcpp2 生成的只是一半,另一半在 gSOAP 源码目录里。 这一节把「还要拷哪些」列成七行清单,并且给出一个拿清单对目录的动作。

📝 本节和前面几节的关系

08.08.07 生成的是「协议相关」的代码,这一节拷的是「运行时」。 08.01.08 是 Windows 版:那时拷的是 stdsoap2 与 wsse,这一节多讲「哪些可以不拷」。

一、七行清单

这七行就是全部,前两行必拷、中间两行看要不要鉴权、后三行看场景:

文件 是什么 要不要
stdsoap2.c 运行时核心 必拷
stdsoap2.h 运行时头 必拷
wsse.h 鉴权结构体 要鉴权就拷
wsseapi.c 鉴权实现 要鉴权就拷
duration.c 时长类型转换 用到才拷
dom.c DOM 解析 不用就不拷
threads.c 多线程支持 非多线程不拷

前两行是绝对的:stdsoap2.c 里有 soap_new、soap_call、XML 读写全套, 少了它,生成的那些 soapClient.c 一个函数都链接不上。 第七行那个 threads.c 有个前提:它是由 configure --enable-threads 决定要不要编的, 没开线程的话,stdsoap2.c 里根本不会去调它。

二、最容易漏的是 wsseapi.c

漏它的人特别多,而且漏的过程很合理:

阶段 表现 难度
编译 找不到 wsse.h 好查
补上头文件 编译通过 —
链接 soap_wsse_* 未定义 难查

第三行那句报错只给函数名, 不说「你少拷了哪个文件」—— 而 soap_wsse_add_UsernameTokenDigest 这个名字,光看是猜不出它属于哪个 .c 的。 记住这条分界线就行:头文件错是编译期,实现错是链接期。 前者一眼就能看出缺什么,后者要反查。 反查的办法:grep -rl 函数名 gsoap源码目录,一条命令就能定位到那个 .c。

三、「用到才拷」的判据

duration.c 是最典型的例子,它的判据能说得很具体:

条件 结论
结构体里有 xsd__duration 必须拷
用到 MaximumRetentionTime 必须拷
只读厂家型号与流地址 可以不拷

第二行那个字段在 07.02.17 讲过:录像配置里的「最长保存时间」就是它, 类型是 xsd__duration,而它的序列化函数在 duration.c 里。 不拷的后果不是编译错:stdsoap2 用一个函数指针去调它,指针是空的时候编译完全正常, 运行到那个字段就崩。 这种「空指针才崩」的东西最难查, 因为它和「哪个操作」绑在一起—— 同一个程序,调 GetDeviceInformation 没事,调 GetRecordingConfigurations 才崩。

四、拿清单对目录

这个动作看着笨,但它比重编译一遍快得多:

情形 做法 耗时
自己的工程 心里有数 不用对
接手别人的 先对清单再看代码 省一半排查
跨平台搬 对清单 + 对换行 两件事

第二行那个顺序很重要: 先对清单、再看代码——因为「链接不过」的九成原因是缺文件,不是逻辑错。 反过来查会走很多弯路:先读两百行业务代码,最后发现只是少拷一个 .c, 这种事在接手项目时非常常见。 第三行那个「对换行」是 Linux 侧独有的:Windows 上编辑过的 .c 拿到 Linux 编, 如果文件里有中文注释、又混了 CRLF,某些编译器会对宏续行报莫名其妙的错。

五、别整目录拷

「省事」的做法是把整个 gSOAP 源码目录拷进来,这个做法会带来三个新问题:

拷了什么 后果
soapServer.c 多一套符号
autotest 等示例 重复 main
整个目录 编到很多无关文件

第二行那个「重复 main」是最常见的链接错误:示例程序里都有 main, 和你的 main.cpp 撞在一起,报错是「multiple definition of main」。 第一行那个「多一套符号」更隐蔽:soapServer.c 里有服务端的 soap_serve, 它和客户端那套不冲突,但会白占体积,而且如果哪天真的用了 -l,两边还会重复定义。 所以正确做法是「按清单拷单个文件」, 不是「按目录拷」——清单只有七行,对着拷一遍不到一分钟。

六、动手 Demo:还要复制哪些文件

Demo 做的是「拿清单对目录」这个动作,用一个假的工程目录演示怎么算出「还缺几个」。 输出里的数字是代码算出来的,不是手写的——所以它和清单永远是同步的。

need = ["stdsoap2.c", "stdsoap2.h", "wsse.h",
        "wsseapi.c", "duration.c", "dom.c", "threads.c"]
have = ["stdsoap2.c", "stdsoap2.h", "wsse.h", "duration.c"]

miss = [f for f in need if f not in have]
print("工程里现在有 %d 个:%s" % (len(have), "、".join(have)))
print("对照清单还缺 %d 个:%s" % (len(miss), "、".join(miss)))

实跑输出:

--------------------------------------------------------------------
还要复制哪些文件
--------------------------------------------------------------------
**`soapcpp2` 生成的只是一半,另一半在 gSOAP 源码里。**
**这一节就是把「哪些文件必须从 gSOAP 源码里拷出来」列清楚。**

  文件             是什么               要不要
  ----------------------------------------------------------------
  `stdsoap2.c`     **运行时核心**       **必拷**
  `stdsoap2.h`     运行时头文件         **必拷**
  `wsse.h`         **鉴权结构体**       要鉴权就必拷
  `wsseapi.c`      鉴权实现             要鉴权就必拷
  `duration.c`     时长类型转换         用到 `xsd__duration` 才拷
  `dom.c`          DOM 解析(可选)     不用就不用拷
  `threads.c`      多线程支持           非多线程可不拷

**第二行那个 `wsseapi.c` 最容易漏**:
**很多人只拷了 `wsse.h`(因为有编译错误提示「找不到头文件」),**
**结果头文件有了、结构体有了,**
**但 `soap_wsse_add_UsernameTokenDigest` 这个函数没有实现**,
**链接期才炸。**
**「头文件错是编译期,实现错是链接期」**——
**后者更难查,因为报错里只有函数名,没有「你少拷了哪个文件」。**

**第三行那句「用到才拷」也有讲究**:
**`duration.c` 提供的是 `xsd__duration` 与秒数互转**,
**ONVIF 里 `MaximumRetentionTime` 这种字段就是它**(07.02.17 讲过)。
**不拷它,编译能过、运行到那个字段就崩**——
**因为函数指针是空的。**

**我把这份清单整理成「核对用」的形式**:

假设工程里现在有 4 个:stdsoap2.c、stdsoap2.h、wsse.h、duration.c
对照清单还缺 3 个:wsseapi.c、dom.c、threads.c

**这个「拿清单对目录」的动作看着笨,但比重编译一遍快得多。**
**尤其是接手别人的工程**:**先对清单,再看代码**,
**能省掉一半「为什么链接不过」的排查。**

输出里「有 4 个、缺 3 个」正好加起来是清单的 7 行——这个自洽关系就是核对的意义: 数目对不上说明清单漏了或者目录多了。 这四行代码用的是列表推导,[f for f in need if f not in have] 一行就够, 不需要写循环——清单越短,这种写法越省事。 缺的那三个里有两个是鉴权相关的:wsseapi.c 和 dom.c, 而 dom.c 其实是 wsseapi.c 的依赖——因为鉴权头里那段 XML 是用 DOM 拼的。 所以「要鉴权就要拷三个文件」这条经验是有因果的: wsse.h 给结构体、wsseapi.c 给实现、dom.c 给它干活。

30 秒实验

  1. 把你项目里所有 soap 开头的文件列出来,看有几个是生成物。
  2. 用 grep -rl soap_wsse_add_UsernameTokenDigest 在 gSOAP 源码里找它在哪个 .c。
  3. 把 threads.c 从工程里删掉再编一次,看是不是真的不用它。

常见误区

  1. 只拷 wsse.h 不拷 wsseapi.c。 编译能过、链接才炸,而且要 grep 才查得出缺哪个文件。
  2. 把整个 gSOAP 源码目录拷进工程。 示例程序自带 main, 会和自己的撞成「multiple definition」。
  3. 以为 duration.c 不拷只是编不过。 它是函数指针形式调用的,编译正常、 运行到时长字段才崩。
自测三问(点开对答案) 1. 为什么「头文件错是编译期,实现错是链接期」这句话有用? —— **因为两阶段的报错信息量差很多**:编译期直接说「找不到头文件」,链接期只给函数名, 要自己 grep 反查属于哪个 `.c`。 2. 只读厂家型号和流地址的客户端,可以不拷哪几个文件? —— **可以不拷 `duration.c`、`dom.c`、`threads.c`。** 但一旦要读录像配置里的最长保存时间, `duration.c` 就必须补上。 3. 为什么「拿清单对目录」要排在「读代码」前面? —— **链接不过的九成原因是缺文件而不是逻辑错。** 先读两百行代码最后发现少拷一个 `.c`, 是最常见的白干。

下一节讲命名空间表——Linux 下它不是个文件,是程序里的一段数组。

相关笔记