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

八个产物里客户端只要五个,另外还得从 gSOAP 源码里拿三份。 而「放哪儿」比「拷什么」更容易错——*.nsmap 是运行期找的,不是编译期。

📝 本节和前面几节的关系

08.01 这一节的八篇走到最后一步了:代码生成完了,现在把它搬进自己的工程。 这一步是「从教程走向项目」的分界线—— 前面所有命令都是标准动作,从这里开始每个工程都不一样。

一、八个里挑五个

文件 来源 缺了会怎样
soapStub.h 生成 类型全未定义
soapH.h 生成 函数原型找不到
soapC.cpp 生成 链接期一片未定义符号
soapClient.cpp 生成 没有调用函数
*.nsmap 生成 运行期 unbound prefix

前三行是「编译期」的错,第四行是「链接期」,第五行是「运行期」。 三类错的味道完全不同: 编译期报「未见过的类型」,链接期报「未定义的引用」,运行期报「unbound prefix」或「Fault」。 所以「少拷了哪个文件」这个问题,其实可以从报错类型反推: 报错阶段越靠后,说明缺的东西越「不是头文件」。 第五行那个 *.nsmap 最特殊:它是唯一「运行期才读」的文件, 所以它放错位置的表现和「没拷」完全一样。

二、三份源码不能忘

文件 是什么 缺了会怎样
stdsoap2.c/h 运行时核心 soap_* 全找不到
wsse.h + wsseapi.c 鉴权结构 + 实现 编译期缺类型、链接期缺函数
duration.c 时长类型转换 运行到那个字段就崩

第二行那个「头文件 + 实现」是最容易只拷一半的: wsse.h 少了会有编译错误提示(找不到头文件),所以一定会补; 而 wsseapi.c 少了只是链接错误, 报的是 undefined reference to soap_wsse_add_UsernameTokenDigest—— 看得懂的人知道是缺文件,看不懂的人会去怀疑「gSOAP 没装好」。 第三行更隐蔽:duration.c 提供 xsd__duration 与秒数的互转, 不拷它编译能过(因为函数是声明过的),运行到那个字段才崩——因为函数指针是空的。 所以这三行的排查顺序应该是:先看编译错、再看链接错、最后才怀疑运行错。

三、放哪儿比拷什么更容易错

文件 放到 为什么
soapStub.h 等 和调用代码同级 #include "soapH.h" 能找着
stdsoap2.c 单独加进工程 别只加头文件
*.nsmap 和 exe 同目录 运行期读,不是工程目录

第三行是本节最实用的一条:在 VS 里点「运行」时,工作目录是 项目\Debug\, 不是项目根目录——所以 .nsmap 放在项目根目录,跑起来照样找不到。 表现是「编译没问题,一跑就说命名空间未定义」,而错误信息里不会告诉你「文件放错地方了」。 第二行那个「别只加头文件」也是真事故:stdsoap2.h 加了、stdsoap2.c 没加, 编译期一切正常,链接期几百个未定义符号——因为 gSOAP 的所有实现都在那个 .c 里。

四、用清单对目录

这一步没有技术含量,但「拿清单对目录」比重编译一遍快得多。 尤其是接手别人的工程:先对清单,再看代码,能省掉一半「为什么链接不过」的排查。

做法 花的时间 效果
对清单 一分钟 直接知道缺哪个
看编译报错 几分钟 知道缺「一类」
重跑生成 十几分钟 可能把别人的改动冲掉

第三行那个「冲掉改动」要特别小心: 生成的文件里如果被人手改过(很多人会去改 soapStub.h),重跑一次就全没了。 所以工程上有个约定:生成的文件不手改、不提交, 要改就在 onvif.h 或 typemap.dat 那一层改,然后重生成。 这条约定不遵守,早晚会出「昨天还好好的,今天就变了」的事故。

五、动手 Demo:哪些文件要拷进自己的工程

Demo 用「拿清单对目录」的办法,把「该有的」和「现在有的」比一遍。 这是本节唯一能自动化的一步,也是最值得自动化的一步。

# 该有的清单
need = ["stdsoap2.c", "stdsoap2.h", "wsse.h", "wsseapi.c",
        "duration.c"]
# 现在目录里有的
have = ["stdsoap2.c", "stdsoap2.h", "wsse.h", "duration.c"]
miss = [x for x in need if x not in have]
print(len(have), len(miss), miss)

实跑输出:

--------------------------------------------------------------------
哪些文件要拷进自己的工程
--------------------------------------------------------------------
**生成出来八个文件,但真正要拷进工程的是「五个加三份源码」。**
**先看清单**:

  要拷的               来源               缺了会怎样
  ------------------------------------------------------------------
  `soapStub.h`         生成               类型全未定义
  `soapH.h`            生成               函数原型找不到
  `soapC.cpp`          生成               **链接期一片未定义符号**
  `soapClient.cpp`     生成               没有调用函数
  `*.nsmap`            生成               **运行期 unbound prefix**
  `stdsoap2.c/h`       **gSOAP 源码**     `soap_*` 全找不到
  `wsse.h`             gSOAP 源码         鉴权结构体没有
  `duration.c`         gSOAP 源码         **时长类型转换缺实现**

**第三列分两类错**:前三行是「编译不过」,后两行是「编译过了运行才炸」。
**`*.nsmap` 那一行最典型**:编译期它只是个数组,
**运行期少一项,第一个带该前缀的元素就解析失败。**
**而失败的方式还分两种**:客户端发出去被拒、或者收回来解析不了。

**然后是「拷到哪」这件事,比拷什么更容易错**:

  文件                放到工程的                     说明
  -------------------------------------------------------------------------------
  `soapStub.h` 等     和调用代码同级                 `#include "soapH.h"` 能找着
  `stdsoap2.c`        **单独一个源文件加进工程**     **别只加头文件**
  `duration.c`        同上                           只在用到时长时才需要
  `*.nsmap`           **和 exe 同目录**              **不是工程目录**

**最后一行是新手最容易错的地方**:
**`.nsmap` 是运行期读的**(或者被编进 exe,取决于模式),
**放工程目录里、exe 在 `Debug\` 下跑,就找不着。**
**表现是「编译没问题,一跑就说命名空间未定义」**,
**而错误信息里不会告诉你「文件放错地方了」。**

这段代码只有三行有内容,但它是「工程化」的开始:把「靠记忆」换成「靠清单」。 输出里那个「还缺 1 个」就是 wsseapi.c——正好是上一节说的「最容易只拷一半」的那个。 注意清单里没有列 soapServer.cpp:这份清单是「客户端」视角, 写成服务端的话,清单会变成另一套。 所以清单本身要标明适用场景,否则用错版本反而添乱。 最后那句「拿清单对目录看着笨」是对的: 它确实笨,但它比「猜」可靠——而工程上「可靠」比「聪明」值钱。

30 秒实验

  1. 把清单改成服务端视角,加上 soapServer.cpp,看还缺几个。
  2. 在 have 里去掉 duration.c,看缺的个数变成几。
  3. 想一想:如果 .nsmap 放错了目录,清单能查出来吗?(答案:不能,它是运行期的)

常见误区

  1. 只拷 wsse.h 不拷 wsseapi.c。 头文件缺了有编译错误提示,实现缺了只有链接错误, 报错里看不出「少了个文件」。
  2. 把 *.nsmap 放在工程根目录。 VS 运行时工作目录是 项目\Debug\,放根目录照样找不到, 症状是「一跑就说命名空间未定义」。
  3. 手改生成的文件。 下次重跑生成就全没了,要改就在 onvif.h 或 typemap.dat 那一层改。
自测三问(点开对答案) 1. 少拷 `soapC.cpp` 会有什么表现? —— **链接期一片「未定义的引用」**。 它是序列化实现,所有 `soap_write_*`、`soap_read_*` 都在里面。 2. 为什么 `.nsmap` 放错目录和「没拷」表现一样? —— **因为它是运行期读的**,编译器根本不知道它存在,**所以放错了照样编过,跑起来才出问题。** 3. 为什么说生成的文件不能手改? —— **下次重跑生成会把改动冲掉**。要改就在 `onvif.h` 或 `typemap.dat` 那一层改,然后重新生成。

下一节进 08.02:代码有了,第一件事是「找到设备」。

相关笔记