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 秒实验
- 在
portType里再加一个operation,看message要不要跟着加。 - 把
style从document改成rpc,看soapcpp2生成的函数名会不会变(本机没工具, 只能看 WSDL 本身的变化)。 - 把
service里的location改成http://10.0.0.1/onvif/device_service, 看解析结果里哪一处跟着变。
常见误区
- 以为
message数等于操作数。 一个操作要输入和输出两条消息, 所以消息数通常是操作数的两倍。 - 以为 WSDL 是从设备上抓的。 很多设备不支持
?wsdl, 真项目用的是 ONVIF 官网的规范包里的 WSDL。 - 以为
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 也可能失败」这件事。
- ☐ (本节完,下一节 06.01.05 SOAP简介)