📹 第六章 SOAP及gSOAP实战
📄 一句话概括

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 秒实验

  1. 给 WSDL 再加一个 operation,看生成的函数和 if 各多几条(应该各多一条,且一一对应)。
  2. 把 ns__ 前缀改成 tds__,看生成的四个文件里有多少处要跟着改。
  3. 把生成的 soapH.h 里那个 struct soap *soap 参数删掉,想想为什么删不得(上下文没了)。

常见误区

  1. 以为 soapcpp2 能直接吃 WSDL。 它吃的是头文件,WSDL 要先过 wsdl2h。
  2. 以为返回值 0 是失败。 gSOAP 里 SOAP_OK 就是 0,非 0 才是错——和 POSIX 的习惯相反。
  3. 以为生成的代码不能改。 生成的四个文件不该手改(重新生成会覆盖), 业务代码要写在另外的文件里,由分发表调过去。
自测三问(点开对答案) 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 指令又是什么。

相关笔记