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

WSDL 是 XML 写的接口说明书:有哪些操作、参数什么类型、地址在哪。 五段依次回答五个问题,而 message 和 portType 不是一对一——一个操作要两条消息。

📝 本节和前面几节的关系

上一节讲 XML 的语法,这一节讲一份具体的 XML:WSDL。 没有 WSDL,soapcpp2 就无从生成代码(06.02.01),所以这一节是 gSOAP 的前置。

一、五段,依次回答五个问题

WSDL 的结构是「一层层引用」:service 指到 binding,binding 指到 portType, portType 指到 message,message 指到 types。

段 回答什么 被谁引用
types 数据类型长什么样 message
message 一次传输发什么 portType
portType 有哪些操作 binding
binding 怎么绑到协议上 service
service 地址在哪 (最外层,没人引用它)

倒着读最快:从 service 的地址出发, 顺着 binding → portType → message → types 一路问下去, 问到最后就得到了「这个字段是什么类型」。 顺着读则是「先定义类型,最后落到地址」,写 WSDL 的人是顺着写的,读 WSDL 的人应该倒着读。

二、message 和 operation 不是一对一

Demo 实测这份 WSDL 里:message 有 2 条,portType 只有 1 条。

标签 个数 为什么是这个数
types 1 一份 schema 装所有类型
message 2 一个操作要「输入」和「输出」两条消息
portType 1 只有一个操作
binding 1 一套协议绑定
service 1 一个服务地址

这个区分在生成代码时会变成实打实的东西: message 变成结构体,operation 变成函数(06.02.01 会看到)。 一个操作对应一个函数、两个结构体(请求一个、响应一个), 所以「函数数」和「结构体数」永远对不上——这不是工具的问题,是模型如此。

三、document 风格和 rpc 风格

binding 里那句 style="document" 决定了 Body 长什么样。

风格 Body 的样子 谁在用
document 直接就是业务元素,如 <tds:GetDeviceInformation/> ONVIF 用这个
rpc 套一层操作名,参数是子元素 早期的 .NET Web Service

两种风格的报文长得不一样,解析代码也不一样。 ONVIF 规范明确要求 document 风格,所以见到 rpc 风格的 ONVIF 报文, 基本可以判定是仿冒或者极老的固件。 这也是判断一个「ONVIF 摄像头」是不是真 ONVIF 的快速办法: 抓一条 GetDeviceInformation 的请求看看。

四、去哪里拿 WSDL

真实设备上,WSDL 一般挂在这些地址:

地址 拿到什么
http://ip/onvif/device_service?wsdl 设备管理(设备信息、时间、重启)
http://ip/onvif/media_service?wsdl 媒体配置(编码、分辨率)
http://ip/onvif/ptz_service?wsdl 云台控制

但很多设备不认 ?wsdl 这个后缀,要么返回 400,要么返回一个空信封。 这是 ONVIF 早期的历史遗留:规范只要求「能响应 SOAP 请求」,没强制要求支持 ?wsdl。 所以真正做项目时,WSDL 不是从设备上抓的,是从 ONVIF 官网下的(07.01.03 的规范包里就有)。 设备上那句 ?wsdl 只用来「验一下这个地址通不通」。

五、动手 Demo:WSDL 是一份「接口说明书」

Demo 用 ElementTree 把一份最小 WSDL 拆开:先数五段各有几个元素, 再把 portType 里的操作和 binding 里的 SOAPAction 对着抓出来。 这份 WSDL 是照 ONVIF 的 device_service 缩写的,只有 1 个操作。

root = ET.parse("_t35_dev.wsdl").getroot()
W = "{http://schemas.xmlsoap.org/wsdl/}"     # WSDL 自己的命名空间

for tag in ("types", "message", "portType", "binding", "service"):
    print(tag, len(root.findall(W + tag)))

# 操作名和它的 SOAPAction 分别住在两棵树里
ops = root.findall(W + "portType/" + W + "operation")
acts = root.findall(W + "binding/" + W + "operation/"
                     "{http://schemas.xmlsoap.org/wsdl/soap/}operation")
for op, ac in zip(ops, acts):
    print(op.get("name"), ac.get("soapAction"))

实跑输出:

--------------------------------------------------------------------
WSDL 是一份「接口说明书」
--------------------------------------------------------------------
**WSDL 就是 XML 写的接口定义:有哪些方法、参数什么类型、地址在哪。**
  这一节把一个最小 WSDL 拆成五段看。

**五段结构(按出现顺序):**

  段            回答什么                  本例里的内容
  ------------------------------------------------------------------------------------
  `types`       数据类型长什么样          xs:schema,定义了请求/响应元素
  `message`     一次传输发什么            Input/Output 两条
  `portType`    有哪些操作                DevicePort,一个操作
  `binding`     怎么绑到协议上            SOAP over HTTP,document 风格
  `service`     地址在哪                  http://192.168.1.64/onvif/device_service

**实测各段元素数:**

  标签              个数
  ----------------------------
  `types`           1
  `message`         2
  `portType`        1
  `binding`         1
  `service`         1

  **`message` 有 2 条、`portType` 只有 1 条**——**这说明「一条消息」和「一个操作」不是一对一**:
  **一个操作要引用输入、输出两条消息。**
  **这个区分很重要**:后面 soapcpp2 生成代码时,**消息变成结构体,操作变成函数**,
  两者不是一个东西。

**把操作和它的 SOAPAction 抓出来:**

  操作名                SOAPAction
  ------------------------------------------------------------------------------------
  GetDeviceInformation  http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation

  **SOAPAction 通常就是「命名空间 + 操作名」**,**所以它看着长,其实有规律。**
  **但也有厂家不按规矩写**,所以别自己拼,**从 WSDL 里读。**

**这份 WSDL 只描述了一个操作,真实的 ONVIF device_service 有几十个。**
  **在浏览器里打开设备的 `http://ip/onvif/device_service?wsdl` 就能拿到它。**
  **注意有的设备不认 `?wsdl` 这个后缀,要发一个空的 SOAP 请求才回**——这是 ONVIF 早期的历史遗留。

注意 findall 的路径写法:W + "portType/" + W + "operation"。 每一个标签名前面都要带命名空间,中间那个 / 不需要。 soap:operation 的命名空间和 operation 不一样(前者是 schemas.xmlsoap.org/wsdl/soap/), 所以那个嵌套的 soap:operation 得单独拼一遍 URI。 这是写 WSDL 解析代码最容易错的地方。 输出的 SOAPAction 就是「命名空间 + 操作名」拼起来的,看着很长,其实有规律。 但别自己拼——有的厂家不守规矩,从文件里读才稳。

30 秒实验

  1. 在 portType 里再加一个 operation,看 message 要不要跟着加。
  2. 把 style 从 document 改成 rpc,看 soapcpp2 生成的函数名会不会变(本机没工具, 只能看 WSDL 本身的变化)。
  3. 把 service 里的 location 改成 http://10.0.0.1/onvif/device_service, 看解析结果里哪一处跟着变。

常见误区

  1. 以为 message 数等于操作数。 一个操作要输入和输出两条消息, 所以消息数通常是操作数的两倍。
  2. 以为 WSDL 是从设备上抓的。 很多设备不支持 ?wsdl, 真项目用的是 ONVIF 官网的规范包里的 WSDL。
  3. 以为 soapAction 一定是「命名空间 + 操作名」。 大部分是, 但有厂家不守规矩,所以要读文件、别自己拼。
自测三问(点开对答案) 1. WSDL 的五段分别是什么? —— `types`(类型)、`message`(发什么)、`portType`(有哪些操作)、`binding`(怎么绑协议)、 `service`(地址在哪)。 2. 为什么这份 WSDL 里 `message` 是 2 条、`portType` 只有 1 条? —— 因为一个操作要引用**输入**和**输出**两条消息。 **「消息」是数据单元,「操作」是行为单元,两者数量不对应。** 3. `style="document"` 和 `style="rpc"` 的报文差在哪? —— document 的 Body 里**直接就是业务元素**;rpc 会**多套一层操作名**。**ONVIF 用 document。**

下一节把信封拆开:SOAP 的三段结构、Fault 错误,和「200 OK 也可能失败」这件事。

相关笔记