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

wsdl2h 只干一件事:WSDL/XSD 里的类型翻成 C/C++ 的 struct 和函数原型。 它的产物顶部有几行 //gsoap 注释,那不是废话,是给 soapcpp2 看的编译指令。

📝 本节和前面几节的关系

上一节讲第二步 soapcpp2,这一节回到第一步 wsdl2h。 //gsoap 指令在这个文件里出现,看起来像注释,实际决定生成的代码长什么样—— 这是 gSOAP 最容易被忽略的设计。

一、类型映射:XSD → C

XSD 类型 C 类型 要注意什么
xs:string char * 要自己管内存,gSOAP 有专用分配函数
xs:int int 32 位,和平台无关
xs:dateTime time_t gSOAP 有专用解析器,别自己写
xs:boolean bool true/1 都认
xs:float float 注意精度,ONVIF 里用得少

第一行是 gSOAP 项目最经典的内存泄漏源:char * 是裸指针,谁分配、谁释放没有类型系统保证。 gSOAP 的对策是「内存池」:soap 上下文里挂着一个链表, soap_new_string 分配的东西在 soap_destroy 时统一回收。 所以别用 malloc,一律用 soap_* 系列函数。 第三行同理:xs:dateTime 是 ISO 8601 字符串(2024-05-01T12:00:00Z), 自己解析会漏掉时区前缀 Z 和小数秒,用 soap_s2dateTime 一行搞定。

二、实测:这份 WSDL 抽出 6 个带类型的元素

迷你 wsdl2h 把 types 段里所有 xs:element 连名字带类型抓出来:

元素名 XSD 类型 翻成 C
Manufacturer xs:string char *
Model xs:string char *
FirmwareVersion xs:string char *
SerialNumber xs:string char *
HardwareId xs:string char *
UTCDateTime xs:dateTime time_t

六个里有五个是 char *——这就是 ONVIF 结构体满屏指针的原因:设备信息几乎全是字符串。 唯一那个 time_t 出现在 GetSystemDateAndTime 的响应里, 而它恰好是最容易解析错的一个(时区、本地时间、UTC 三个字段容易搞混)。

三、//gsoap 指令:看起来像注释的配置

生成的 onvif.h 顶部通常长这样:

//gsoap ns service name:    Device
//gsoap ns service style:   document
//gsoap ns service encoding: literal
//gsoap ns schema namespace: urn:Device

四行全是注释,但 soapcpp2 会读它们。

指令 决定什么 改错的后果
service name 生成的服务类名/文件名 生成的文件名全变,编译脚本要跟着改
service style document 还是 rpc 和 ONVIF 规范不符,设备不认
service encoding literal 还是 encoded ONVIF 要 literal
schema namespace 生成的 ns__ 指向哪个 URI SOAPAction 对不上,全部 400

第四行是最致命的一行:它决定生成的代码里 ns 映射到哪个 URI, 而 ONVIF 要求 http://www.onvif.org/ver10/device/wsdl。 写错了,客户端发出去的 SOAPAction 就是错的,设备全部拒绝。 08.01.07 那一节叫「关联自己的命名空间」,讲的就是改这几行。

四、三个常用参数

参数 作用 什么时候用
-o out.h 指定输出文件 总是要
-t typemap.dat 指定类型映射文件 要自定义类型时(ONVIF 必改)
-s 不生成 STL 容器 纯 C 工程

第二行在 ONVIF 项目里是必选项:typemap.dat 里写着「哪个 XSD 类型映射到哪个 C 类型」, ONVIF 的 WSDL 引用了大量 xsd:duration、xsd:hexBinary, 不配 typemap 生成出来一堆 _xsd__ 开头的怪名字。 08.01.03 专门有一节讲改 typemap.dat,说明这确实是绕不过去的一步。 第三行在嵌入式里很重要:-s 生成的代码不用 STL,但代价是字符串处理全得用 C 的方式, 代码会啰嗦不少。

五、动手 Demo:wsdl2h 就是把 WSDL 翻成 C 头文件

同样用迷你版:从 WSDL 里抽元素和类型,按映射表翻成 C 声明, 再拼出 gSOAP 风格的 //gsoap 指令头。 跑的都是真解析(ElementTree 读真文件),只有「生成 gSOAP 代码」这一步是模仿的。

XS2C = {"xs:string": "char *", "xs:int": "int",
        "xs:dateTime": "time_t", "xs:boolean": "bool"}

# ① 遍历 types 段,把每个 xs:element 的名字和类型抓出来
for el in root.iter("{http://www.w3.org/2001/XMLSchema}element"):
    t = el.get("type")
    if t:
        print(el.get("name"), t, XS2C.get(t, "?"))

# ② 拼 gSOAP 风格的指令头 + 结构体
print("//gsoap ns service name:  Device")
print("//gsoap ns schema namespace: %s" % NS_T)
print("struct ns__GetDeviceInformationResponse {")
for name, t in fields:
    print("    %s %s;" % (XS2C[t], name))
print("};")

实跑输出:

--------------------------------------------------------------------
wsdl2h 就是把 WSDL 翻成 C 头文件
--------------------------------------------------------------------
**`wsdl2h` 干的事很单一:WSDL/XSD 里的类型 → C/C++ 的 struct 和函数原型。**
  这一节用迷你版走一遍,看它怎么翻。

**WSDL 里的类型 → C 类型,对照如下:**

  XSD 类型        C 类型          说明
  ----------------------------------------------------------
  `xs:string`     `char *`        要自己管内存
  `xs:int`        `int`           32 位
  `xs:dateTime`   `time_t`        gSOAP 有专用解析
  `xs:boolean`    `bool`          `true`/`1` 都认

**实测:从这份 WSDL 抽出所有元素和它们的类型:**

  元素名                    XSD 类型            翻成 C
  --------------------------------------------------------------
  Manufacturer              xs:string           char *
  Model                     xs:string           char *
  FirmwareVersion           xs:string           char *
  SerialNumber              xs:string           char *
  HardwareId                xs:string           char *
  UTCDateTime               xs:dateTime         time_t

  **一共 6 个带类型的元素**,翻成 C 之后就是 6 个字段。
  **注意 `xs:string` 翻成 `char *` 是个陷阱**:**要自己 malloc、自己 free**,
  gSOAP 用 `soap_new_string` 帮你管,**但忘了还是会漏。**

**迷你 wsdl2h 的输出(结构照 gSOAP 风格):**

  `//gsoap ns service name:    Device`
  `//gsoap ns service style:   document`
  `struct ns__GetDeviceInformationResponse {`
      char * Manufacturer;
      char * Model;
      char * FirmwareVersion;
      `…`
  `};`

  **顶部那两行 `//gsoap` 注释不是废话**:它们是给 `soapcpp2` 看的编译指令,
  **决定生成代码的服务名、风格、命名空间前缀。**
  **改这三行,生成的代码就换一套命名**——这是 gSOAP 的「宏语言」。

**三个常用参数:**

  参数            作用                      什么时候用
  ------------------------------------------------------------------
  `-o out.h`      指定输出文件              总是要
  `-t x.h`        指定类型映射文件          要自定义类型时
  `-s`            不生成 STL 容器           纯 C 工程

  **第三个参数在嵌入式项目里很重要**:ONVIF 设备端经常是纯 C,**带 STL 的代码根本编不过去。**

XS2C.get(t, "?") 里的 ? 很重要:没见过的类型要显式暴露出来, 别默认当成 char *——那会把类型错误藏到运行期。 输出里那 6 个元素是 types 段里所有带 type 属性的 xs:element, 不含 GetDeviceInformation 这种「带内联 complexType」的顶层元素(它们自己没有 type 属性)。 所以「6 个带类型的元素」这句话是有口径的:指的是叶子字段,不是顶层请求/响应元素。 汇总的时候把口径说清楚,比数字本身更重要。

30 秒实验

  1. 给 XS2C 加一条 "xs:long": "long long",看新类型能不能翻出来。
  2. 把 schema namespace 改成 urn:Device,然后想一下客户端发出去的 SOAPAction 会变成什么。
  3. 把 -s(不生成 STL)的效果手工模拟一遍:把 std::string 换成 char *,数要改几处。

常见误区

  1. 以为 //gsoap 是注释、可以删。 soapcpp2 会读它,删了等于改了生成配置。
  2. 以为 xs:string 翻成 char * 之后可以 malloc。 要用 soap_new_string 这类池分配, 否则 soap_destroy 收不回去,必漏。
  3. 以为 typemap 是可选的。 ONVIF 的 WSDL 引用了大量特殊类型, 不改 typemap.dat 生成出来一堆没法用的名字。
自测三问(点开对答案) 1. `xs:string` 为什么翻成 `char *` 是个陷阱? —— 因为**要自己管内存**。 gSOAP 用内存池(`soap_new_string`),**用 `malloc` 分配的东西它收不回去,就漏了。** 2. `//gsoap ns schema namespace` 写错会怎样? —— **生成的代码里 `ns` 指向错误的 URI**,客户端发出去的 `SOAPAction` 跟着错, **设备全部返回错误**。 3. `wsdl2h` 和 `soapcpp2` 的分工是什么? —— `wsdl2h`:**WSDL → 带指令的 C 头文件**(决定「有什么」)。 `soapcpp2`:**头文件 → 四个 .cpp/.h**(决定「怎么实现」)。

下一节开始动手装工具:CentOS 上编 gSOAP 要装哪六样东西,以及「编过了但用不了」是怎么回事。

相关笔记