soapcpp2 只吃「带 //gsoap 指令的头文件」,吐出四个文件:类型头、序列化实现、客户端桩、
服务端分发。
规律一句话:WSDL 的 message 变结构体,operation 变函数,
函数的第一个参数永远是 soap 上下文。
06.01.04 讲了 WSDL 的五段结构,这一节讲怎么把它变成能编译的 C/C++ 代码。
本机没有 gSOAP 工具链(没有 soapcpp2、wsdl2h,也没有 g++),
所以这一节跑的是我写的「迷你 soapcpp2」,产出形态照 gSOAP。
结构是真的,工具不是真的——这一点必须说清楚。
一、它在流水线的第二步
gSOAP 的完整流水线是两步工具 + 一步人工:
| 步 | 输入 | 工具 | 输出 |
|---|---|---|---|
| ① | onvif.wsdl |
wsdl2h |
onvif.h(带 //gsoap 指令) |
| ② | onvif.h |
soapcpp2 |
soapH.h 加三个 .cpp |
| ③ | 上面四个文件 | 人 | 填业务函数,编译 |
soapcpp2 不能直接吃 WSDL,这是新手最常搞错的一步。
WSDL 必须先被 wsdl2h 翻成头文件(06.02.02)。
所以看到「soapcpp2 报一堆语法错」,八成是直接把 .wsdl 喂给它了——它以为那是一份 C 头文件。
二、四种产物,谁用哪几个
| 文件 | 放什么 | 客户端 | 服务端 |
|---|---|---|---|
soapH.h |
类型定义 + 函数原型 + 序列化声明 | 要 | 要 |
soapC.cpp |
序列化/反序列化实现 | 要 | 要 |
soapClient.cpp |
客户端调用桩 | 要 | 不要 |
soapServer.cpp |
服务端分发骨架 | 不要 | 要 |
第一、二行是两边都要的,
因为序列化代码是共用的——客户端要用它把参数变成 XML,服务端要用它把 XML 变回参数。
后两行是分工:做客户端只要 soapClient.cpp,做服务端只要 soapServer.cpp。
两个都加进去也不会错,只是多编了几百 KB。
三、message 变结构体,operation 变函数
这是全篇最该记住的一句。迷你版从同一份 WSDL 生成的 soapH.h 片段是这样的:
struct ns__GetDeviceInformationResponse {
char *Manufacturer;
char *Model;
char *FirmwareVersion;
};
int ns__GetDeviceInformation(struct soap *soap,
struct ns__GetDeviceInformationResponse *out);
三段对照:
| WSDL 里 | 生成后 | 名字前面的 ns__ |
|---|---|---|
message |
xs:element 的复杂类型 |
struct |
portType 的 operation |
函数 | 命名空间前缀 |
binding 的 soapAction |
分发条件里的字符串 | 不带 |
第一个参数永远是 struct soap *soap,它是个「上下文」:
连接、缓冲区、错误状态、内存池全在里面。
所有 gSOAP 函数都靠它串起来——所以调用时永远要先有一个 soap 对象。
返回值是 int:SOAP_OK(0)表示成功,非 0 就是错误码。
所以判断成功要写 if (soap_call_xxx(...) != SOAP_OK),不能当 bool 用。
四、服务端就是一张分发表
迷你版生成的 soapServer.cpp 核心只有一行:
if (!strcmp(soap->action,
"http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation"))
return ns__GetDeviceInformation(soap, &req);
所谓「服务端」,在这一层就是「按 soap->action 把请求路由到你的函数」。
真实的 soapServer.cpp 比这复杂(要解 XML、填参数、序列化响应),但骨架就是这个 if。
这也解释了 06.01.02 里那个 SOAPAction 头的用途:它就是这张表的查询键。
头没带或带错,服务端就找不到该调哪个函数——
而 soap->action 在 SOAP 1.2 里是从 Content-Type 的 action 参数取的
(06.01.05 讲的两版差异,在这里落地)。
五、常用参数
| 参数 | 作用 | 什么时候用 |
|---|---|---|---|
| -c | 生成纯 C 代码(默认是 C++) | 嵌入式、纯 C 工程 |
| -C | 只生成客户端 | 只做客户端时 |
| -S | 只生成服务端 | 只做设备端时 |
| -x | 不生成 XML 示例注释 | 嫌文件太吵时,强烈建议 |
| -I路径 | 头文件搜索路径 | #import 的文件不在一起时 |
第一行在监控项目里很重要:IPC 固件经常是纯 C 写的(芯片厂商的 SDK 大多是 C),
默认生成的 C++ 代码根本编不进去。
第四行是老手的习惯:soapH.h 不加 -x 会有几千行 XML 示例注释,
IDE 里跳转会卡,grep 也慢。
参数以 soapcpp2 -h 的输出为准,不同版本(2.8.100 前后)有增删,别照抄网上的老文章。
六、动手 Demo:soapcpp2 到底生成了什么
本机没有 gSOAP 工具链,所以这一节不能真跑官方工具。 我写了一个「迷你 soapcpp2」:从同一份 WSDL 生成同样四种产物,结构照 gSOAP 的产物形态做, 这样能看清工具到底在干什么。 下文所有「实测」都是迷你版的输出,不是 gSOAP 的输出—— 字段名、命名风格可能有出入,但对应关系是真的。
# 迷你 soapcpp2 的核心:从 WSDL 抽出操作和消息,拼成 struct 和函数原型
ops = root.findall(W + "portType/" + W + "operation")
msgs = root.findall(W + "message")
# ① message -> struct
for m in msgs:
print("struct ns__%s { ... };" % m.get("name"))
# ② operation -> function,第一个参数固定是 soap
for op in ops:
name = op.get("name")
print("int ns__%s(struct soap *soap, ...);" % name)
print("soap_call_ns__%s" % name) # 客户端桩
# ③ 服务端分发表:一个 if 一条操作
for ac in actions:
print("if (!strcmp(soap->action, \"%s\")) return ...;" % ac)
实跑输出:
--------------------------------------------------------------------
soapcpp2 到底生成了什么
--------------------------------------------------------------------
**本机没有 gSOAP 工具链**(没有 `soapcpp2`、`wsdl2h`,也没有 g++),
**所以这一节不能真跑官方工具。**
**我做的事是:写一个「迷你 soapcpp2」,从同一份 WSDL 生成同样四种产物**,
**这样能看清工具到底在干什么**——结构是照 gSOAP 的产物形态做的。
**gSOAP 的标准产物是四个文件:**
文件 放什么 谁用
------------------------------------------------------------------------
`soapH.h` 类型定义 + 函数原型 + 序列化声明 两头都用
`soapC.cpp` 序列化/反序列化实现 两头都用
`soapClient.cpp` 客户端调用桩 客户端
`soapServer.cpp` 服务端分发骨架 服务端
**迷你版生成的 `soapH.h` 片段:**
`struct ns__GetDeviceInformationResponse { char *Manufacturer; … };`
`int ns__GetDeviceInformation(struct soap *soap, struct ns__GetDeviceInformationResponse *out);`
**规律很清楚**:**WSDL 的 message 变成结构体,portType 的 operation 变成函数**,
**函数第一个参数永远是 `soap` 上下文。**
**迷你版生成的客户端桩(对应 `soapClient.cpp`):**
生成的函数名 对应操作 参数个数
------------------------------------------------------------------------
`soap_call_ns__GetDeviceInformation` `GetDeviceInformation` 2
实测从 WSDL 抽出来 **1 个操作、10 个元素**,**生成的客户端桩就是 1 个函数**。
**迷你版生成的服务端分发(对应 `soapServer.cpp`):**
`if (!strcmp(soap->action, …)) return ns__GetDeviceInformation(soap, &req);`
**上面那个条件和 `soap->action` 比的就是 SOAPAction**,**匹配上了就调你的函数。**
**这就是「一个 if 一条操作」的分发表**——**所谓「服务端」在这里只是把请求路由到你的函数。**
**看懂这四种产物之后,用 gSOAP 就只剩三件事**:跑 `wsdl2h`(WSDL→头文件)、
跑 `soapcpp2`(头文件→代码)、填业务函数。
**剩下全是机械劳动。**
输出里那句 实测从 WSDL 抽出来 1 个操作、10 个元素:
10 个元素 = types 里 4 个顶层 xs:element 加上它们的 6 个字段,这个数不是操作数,别混。
「生成的客户端桩就是 1 个函数」这句话的重点在「1」:一个操作对应一个桩函数,不多不少。
如果生成出来发现函数特别多,通常是 WSDL 里包含了多个 service
(ONVIF 的完整 WSDL 会把 device/media/ptz 全塞进来)。
这段代码不调用 gSOAP,纯 Python 字符串拼接。
它证明的是「这件事本质上是文本转换」——理解了这一点,就不会把代码生成当成魔法。
30 秒实验
- 给 WSDL 再加一个
operation,看生成的函数和 if 各多几条(应该各多一条,且一一对应)。 - 把
ns__前缀改成tds__,看生成的四个文件里有多少处要跟着改。 - 把生成的
soapH.h里那个struct soap *soap参数删掉,想想为什么删不得(上下文没了)。
常见误区
- 以为
soapcpp2能直接吃 WSDL。 它吃的是头文件,WSDL 要先过wsdl2h。 - 以为返回值
0是失败。 gSOAP 里SOAP_OK就是0,非 0 才是错——和 POSIX 的习惯相反。 - 以为生成的代码不能改。 生成的四个文件不该手改(重新生成会覆盖), 业务代码要写在另外的文件里,由分发表调过去。
自测三问(点开对答案)
1. soapcpp2 的输入是什么?不是 WSDL 的话是什么? —— 输入是**带 `//gsoap` 指令的头文件**(`onvif.h`),**由 `wsdl2h` 从 WSDL 生成**。 2. WSDL 的 `message` 和 `operation` 分别变成什么? —— `message` 变 **`struct`**,`operation` 变 **函数**, **函数第一个参数固定是 `struct soap *soap`**。 3. 为什么服务端能靠 `soap->action` 找到对应的函数? —— 因为**客户端在报文里带了 `SOAPAction`(1.1)或 `Content-Type` 的 `action`(1.2)**, **生成的 `soapServer.cpp` 就是拿这个字符串去 `strcmp` 的。**下一节看第一步 wsdl2h:XSD 的类型怎么翻成 C 的类型,//gsoap 指令又是什么。
- ☐ (本节完,下一节 06.02.02 wsdl2h的用法)