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

Linux 下这条命令比 Windows 多一个字母,多的那个字母省掉一半代码。 但真正踩坑的地方不在生成、在链接——.c 与 .cpp 混编时符号名会悄悄变短。

📝 本节和前面几节的关系

08.08.06 已经把 onvif.h 做出来了,这一节把它变成能编的 .c 与 .h。 08.01.06 是同一件事的 Windows 版: 命令少一个 -l,链接方式也不同——两节的命令可以直接对照着看。

一、一条命令,五个参数

整条命令就是下面这一行,五个参数各管一件事:

soapcpp2 -2 -c -x -l -I import onvif.h
参数 作用 为什么
-2 SOAP 1.2 绑定 ONVIF 只认 1.2
-c 生成纯 C 服务端常是 C
-x 不生成 XML 注释 文件小一圈
-I import 找 #import 的头 wsse.h 在那里
-l 只生成客户端 省一半代码

第二行那个 -2 是这一节唯一不能商量的参数:省了它,soapcpp2 默认生成 SOAP 1.1 绑定, 而 ONVIF 的规范从头到尾只写 1.2。 第四行那个 -I 后面跟的是目录名 import, 不是文件名——写成 -I import/wsse.h 会报错, 因为 soapcpp2 会把它当目录去里面找 wsse.h。

二、-2 省了会怎样

SOAP 1.1 与 1.2 的信封命名空间不一样,这是最硬的一条差别:

版本 信封命名空间 ONVIF
1.1 schemas.xmlsoap.org 系 不用
1.2 www.w3.org/2003/05 系 只用这个

第一列那个 1.1 的完整地址是:http://schemas.xmlsoap.org/soap/envelope/, 1.2 的是 http://www.w3.org/2003/05/soap-envelope—— 两个地址里连 soap 这个词的位置都不一样。 生成成 1.1 之后,程序能编能跑,只是设备不认:有的回 HTTP 400,有的回一个空的 SOAP Fault, 还有的干脆不回。 三种表现都不像「版本不对」, 所以这个坑经常要查半天——查法只有一个:把发出去的报文抓下来看第一行。

三、-l 是我建议加的

书上没写这个参数,因为书上要同时演示服务端。 只做客户端时加上它,好处很实在:

不加 -l 加了 -l
生成 soapServer.cpp 不生成
带服务端分派样板 没有
文件多两三个 少两三个

第二行那个「服务端分派样板」才是主要收益:服务端那套是「收到请求 → 查表 → 调你的函数」, 对客户端完全没用,但会占满半个 soapStub.h。 代价也要说清:加了 -l 就只出客户端, 以后要在这台机器上做服务端,得把命令改回来重新生成一遍。 所以生成之前先想清楚「这台机器是客户端还是服务端」——这一句话能省掉一次返工。

四、生成出来怎么链接

这一步 Linux 与 Windows 差得最远:

场景 链接命令 要点
纯 C 客户端 gcc *.c -lgsoap 要加 ssl
C++ 客户端 g++ *.cpp -lgsoap++ 库名多两个加号
静态链接 gcc *.c libgsoap.a 体积大

第一行那个 ssl 指的是 -lssl -lcrypto,它来自 configure --enable-ssl: 编译 gSOAP 时没开 ssl,这里就链接不到 soap_wsse_*。 而错误信息只会说「未定义引用」, 不会告诉你「去重新 configure」——这就是 08.08.01 强调「装依赖」的原因。 第二行那个加号也是真事:C++ 版的库文件名就是 libgsoap++.a, -l 后面写 gsoap++,少一个加号就说找不到。

五、.c 与 .cpp 不能混着编

这是 Linux 侧独有的坑,Windows 上被 VS 挡住了:

文件 用什么编 原因
soapC.c gcc 它是纯 C
main.cpp g++ 我自己的代码
链接 g++ 它才认 C++ 符号

第三行是关键:链接这一步必须用 g++,因为只有它会把 C++ 标准库也带进来。 但 g++ 链接时会做 C++ 的名字改编(name mangling)——它把函数名、参数类型编进符号名里, 而 gcc 编出来的 soapC.o 里符号是「原名」。 两边一对,全是「找不到符号」, 而且报错里的符号名看起来「少了几个字符」——那不是名字被截断,是 C++ 那边的名字多了修饰。 解法一行:

#ifdef __cplusplus
extern "C" {
#endif
#include "soapH.h"
#ifdef __cplusplus
}
#endif

这七行是「让 C++ 编译器按 C 的规矩找符号」, soapH.h 自己的声明没变,变的是 main.cpp 这边怎么理解它。 顺序不能反:extern "C" 必须包在 #include 外面, 包在里面无效——因为头文件已经展开完了。

六、哪些文件生成后别动

soapcpp2 的产物分两类,混了就会白改:

文件 类别 能不能改
soapStub.h 生成物 改了会丢
soapH.h 生成物 改了会丢
soapC.c 生成物 改了会丢
main.cpp 自己写 随便改

判据很简单:文件名以 soap 开头的都是生成物,你自己的代码不要叫 soap 开头的名字。 这条规矩看着像强迫症,其实是保命的: 改完 onvif.h 重新生成时,soapcpp2 会直接覆盖同名文件,不提示、不备份。 所以「生成物」和「手写代码」必须分目录放, 或者至少靠文件名前缀分开——前者随时可删,后者一个字都不能丢。

七、动手 Demo:Linux 下 soapcpp2 的参数多一个

本机没有 soapcpp2(08.08.01 的探针已经证明), 所以 Demo 只做能验证的那一半:把命令拼出来、把参数数清楚。 参数清单与链接方式是真事实,-l 这一点是我加的、书上没有——这条差别在输出里明确标了。

import shutil
print("soapcpp2:", shutil.which("soapcpp2"))

args = ["-2", "-c", "-x", "-l", "-I import"]
print("参数个数:", len(args))
print("完整命令: soapcpp2 %s onvif.h" % " ".join(args))

实跑输出:

--------------------------------------------------------------------
Linux 下 soapcpp2 的参数多两个
--------------------------------------------------------------------
**Linux 版和 Windows 版的 `soapcpp2` 是同一個程序**,
**差别只在「生成的代码怎么链接」**:

  参数            作用                  为什么要它
  -------------------------------------------------------------------
  `-2`            SOAP 1.2 绑定         **ONVIF 只认 1.2**
  `-c`            生成 C                服务端常是纯 C
  `-x`            不生成 XML 注释       文件小一圈
  `-I import`     找 `#import` 的头     **`wsse.h` 在那里**
  **`-l`**        **只生成客户端**      **服务端用不上,省一半代码**

**第五行是我建议加的**,书上没写:
**只做客户端就别生成 `soapServer.cpp`**,
**省下的不只是体积,还有「服务端那套回调初始化」的样板代码。**
**代价是以后要加服务端得重新生成**——
**所以这一步要先想清楚「这台机器是客户端还是服务端」。**

**生成之后编译命令也不一样**:

  场景            编译命令                                 说明
  --------------------------------------------------------------------------
  纯 C 客户端     `gcc *.c -lgsoap -lssl -lcrypto`         **要带 ssl**
  C++ 客户端      `g++ *.cpp -lgsoap++ -lssl -lcrypto`     库名多两个加号
  静态链接        `gcc *.c libgsoap.a -lssl`               部署省事,体积大

**第一行那个 `-lssl -lcrypto` 是 `--enable-ssl` 的后续**:
**编译时没开 ssl,这里就链接不到 `soap_wsse_*`**,
**而错误信息只会说「未定义引用」,不会告诉你「重新 configure」。**

**还有一条 Linux 独有的坑**:**`.c` 和 `.cpp` 不能混着编**。
**`soapC.c` 用 gcc 编,`main.cpp` 用 g++ 编,最后一起链接**——
**但 g++ 链接时会做 C++ 的名字改编(name mangling)**,
**所以 `soapH.h` 里那些函数声明必须包在 `extern "C" { }` 里**,
**否则链接期全是「找不到符号」,而符号名看起来还少了几个字符。**

第一行那个 None 就是这一节的门槛:没有 soapcpp2,后面所有步骤都做不了。 而 soapcpp2 不是单独装的, 它是编 gSOAP 时一起编出来的(08.08.03)—— 所以「装依赖 → 编 gSOAP → 才有 soapcpp2」是一条链。 输出里那个「五个参数」正好对上表里的五行:多一个少一个都编不出能用的代码, 但只有 -2 是「错了就不工作」,其余四个错了只是「费事」。 最后那条链接命令里的 ssl 要记住:-lssl -lcrypto 漏了,报错是「未定义引用」, 而它真正的病因在 configure 那一步——隔了十几分钟,很容易忘了回头查。

30 秒实验

  1. 把 -2 从命令里去掉,猜一下生成的信封命名空间会变成哪个。
  2. 用 nm soapC.o | grep soap_wsse 看库里的符号名有没有下划线以外的修饰。
  3. 把 extern "C" 那七行去掉再编一次,看报错里的符号名差多少字符。

常见误区

  1. 以为 -I 后面要写完整文件路径。 它要的是目录, 写成文件会在该「目录」里找不到 wsse.h。
  2. 用 gcc 链接含 .cpp 的工程。 C++ 符号要 g++ 才认,gcc 链接会一片未定义引用。
  3. 直接改 soapStub.h 加自己的声明。 重新生成就被覆盖,自己的东西要放进单独的头文件。
自测三问(点开对答案) 1. 为什么 `-2` 是五个参数里唯一不能省的? —— **ONVIF 规范只定义 SOAP 1.2 绑定。** 生成 1.1 之后程序能跑,但设备回 400 或空 Fault, 表现都不像「版本不对」。 2. `g++` 链接时「找不到符号」,为什么符号名看起来变短了? —— **那是 C++ 的名字改编**:`g++` 编译的文件里符号带类型修饰,和 `gcc` 编出来的原名对不上。 用 `extern "C"` 包住头文件。 3. 加了 `-l` 之后有什么代价? —— **只出客户端,没有服务端代码**。以后要在这台机器上做服务端,得去掉 `-l` 重新生成一遍。

下一节把「还要从 gSOAP 源码里拷哪些文件」列成清单——漏一个,链接期就炸。

相关笔记