📹 第七章 ONVIF协议原理解析
📄 一句话概括

tds、trt、tt、tptz、tev、timg——这些前缀全是缩写,而且都以 T 开头。 那个 T 是 Type:ONVIF 的规范文档就叫 types,前缀是同一套命名习惯。

📝 本节和前面几节的关系

上一节讲了概念之间的引用链, 这一节讲「这些概念在 XML 里长什么样」——也就是那些看着像乱码的前缀。 这一节的内容在第六章 XML 那一小节有底子,这里是把 ONVIF 约定的那几张表补上。

一、前缀是缩写,不是乱码

前缀 全称 命名空间尾段
tds Type Device Service ver10/device/wsdl
trt Type Real Time ver10/media/wsdl
tt Type Toolkit(公共类型) ver10/schema
tptz Type PTZ ver20/ptz/wsdl
tev Type Events ver10/events/wsdl
timg Type Imaging ver20/imaging/wsdl

第一列那个 T 是全表的共同点: ONVIF 的规范包里有一个 types.xsd,所有 service 都从这里引类型,所以所有前缀都带 T。 tt 是唯一一个只有两个字母的,因为它不是某个 service 的前缀,而是那份公共类型本体的前缀。 所以看到 tt: 就该反应过来「这是公共类型」,看到 trt: 才知道「这是媒体服务的」。

二、一份响应里的前缀分布

把 GetProfiles 的响应里所有带前缀的标签统计一遍,只有三种前缀:

前缀 出现次数 在重复什么
tt 21 参数:名字、token、分辨率、码率
s 2 SOAP 信封本身
trt 2 方法调用和返回的外壳

一共 25 个带前缀的标签,分布在 3 个前缀下,响应总长 1174 字节。 平均每个标签 47.0 字节——这个比例说明 XML 的开销主要在标签本身,不在值。 第一行占了 21 个,八成以上:这就是「一个 Profile 里套了一堆参数」的直接体现, 参数全是公共类型,所以全是 tt:。 而 trt: 只出现在最外层的 Profiles 上,因为只有「这个响应属于媒体服务」这一件事需要它。

三、外壳用服务的,内容用公共的

这是 ONVIF XML 的一条硬规律,看具体报文最清楚:

<trt:Profiles token="Profile_1">
  <tt:Name>main</tt:Name>
  <tt:VideoEncoderConfiguration token="VideoEncoder_1">
    <tt:Encoding>H264</tt:Encoding>
    <tt:Resolution><tt:Width>1920</tt:Width></tt:Resolution>
  </tt:VideoEncoderConfiguration>
</trt:Profiles>

trt: 只出现在第一行,往下一层全是 tt:。 规则是「外壳用服务自己的命名空间,内容用公共类型」—— 因为 Profiles 这个元素是媒体服务定义的,而 VideoEncoderConfiguration 是公共类型定义的。 这条规律在写解析代码时省很多事: 你不需要逐个元素查命名空间,只要记住「外面一层是服务的、里面都是 tt」。 但 find 的路径仍然要一段段换命名空间,用通配写法容易匹配到别层去。

四、前缀只是别名,不是身份

这是本节最该记住的一句:tds: 和 foo: 若能指向同一个 URI,它们在语义上完全等价。 XML 的真实身份是「命名空间 URI + 本地名」,也就是 Clark 记法写的那个东西:

写法 在代码里 说明
<tds:GetProfiles/> {URI}GetProfiles 给人看的
{URI}GetProfiles 同上 给代码用的真身份

所以换前缀不改语义——只要 xmlns: 声明跟着换。 反过来说,前缀换了但声明没换,就立刻报 unbound prefix。 我试过一个更笨的错法:用字符串替换把 s: 换成 soap:, 结果连 xmlns:s= 也被改成了 xmlnsoap:s=,tds: 也被改成了 tdsoap:—— 三处全坏,解析器一句话就说 unbound prefix。 这个教训的普适版是:改 XML 前缀不能用字符串替换,要按元素属性改。

五、动手 Demo:那几个前缀,其实是缩写表

Demo 把六组前缀和它们的全称、命名空间尾段对一遍,再把一份真实响应的前缀统计出来, 看哪个前缀占大头。 最后验一件事:换前缀不改语义——同一条请求换个前缀重发,设备照样认。

PFX = [("tds", "ver10/device/wsdl"), ("trt", "ver10/media/wsdl"),
       ("tt",  "ver10/schema"),      ("tptz", "ver20/ptz/wsdl"),
       ("tev", "ver10/events/wsdl"), ("timg", "ver20/imaging/wsdl")]
for p, tail in PFX:
    print(p, tail)

# 统计一份真响应里的前缀分布
_a, b, _n = S.call(port, "media", "GetProfiles")
cnt = {}
for m in re.finditer(r"<([a-z0-9]+):", b):
    cnt[m.group(1)] = cnt.get(m.group(1), 0) + 1
print(cnt, len(b.encode()))        # {tt:21, trt:2, s:2} 1174

# 换前缀不改语义:把 tds 改成 foo,声明一起换
xml = S.body_of("foo", "<foo:GetDeviceInformation/>")
print(xml.count("foo:"), "unbound" not in xml)

实跑输出:

--------------------------------------------------------------------
那几个前缀,其实是缩写表
--------------------------------------------------------------------
**`tds`、`trt`、`tt`、`tptz`…… 看着像乱码,其实每一个都有全称。**
  这一节把一份真实响应里出现过的前缀统计一遍,再看它们指向哪。

**ONVIF 规范里约定的前缀(按 service 分):**

  前缀      全称                              命名空间尾段
  ------------------------------------------------------------------------------------
  `tds`     **T**ype **D**evice **S**ervice   `ver10/device/wsdl`
  `trt`     **T**ype **R**eal **T**ime        `ver10/media/wsdl`
  `tt`      **T**ype **T**oolkit(公共类型)  `ver10/schema`
  `tptz`    **T**ype **PTZ**                  `ver20/ptz/wsdl`
  `tev`     **T**ype **Ev**ents               `ver10/events/wsdl`
  `timg`    **T**ype **Im**a**g**ing          `ver20/imaging/wsdl`

  **那个 `T` 是「Type」**——
  **ONVIF 的规范文档就叫 `types`,所有前缀都以 T 开头,是同一套命名习惯。**
  **`tt` 是唯一一个只有两个字母的**,因为它不是某个 service,
  **是所有 service 共用的类型定义**(分辨率、时间、编码参数都在这里)。
  **所以看到 `tt:` 就该知道「这是公共类型」,  看到 `trt:` 才知道「这是媒体服务的东西」。**

**把 `GetProfiles` 的响应里所有前缀统计一遍:**

  前缀      出现次数    大体上在重复什么
  ------------------------------------------------------------------
  `tt`      21          **参数:名字、Token、分辨率、码率**
  `s`       2           **SOAP 信封本身**
  `trt`     2           **方法调用和返回的外壳**

  **一共 25 个带前缀的标签**,分布在 3 个前缀下。
  **响应总长 1174 字节,平均每个标签 47.0 字节**——
  **这个比例说明 XML 的开销主要在标签本身,不在值。**

**一个具体例子,看前缀怎么嵌:**

  `<tt:VideoEncoderConfiguration token="VideoEncoder_1">`
  `  <tt:Encoding>H264</tt:Encoding>`
  `  <tt:Resolution><tt:Width>1920</tt:Width></tt:Resolution>`
  `</tt:VideoEncoderConfiguration>`

  **同一层里没有 `trt:`**——`trt` 只出现在最外层的 `Profiles` 上。
  **这就是「外壳用服务的命名空间,内容用公共类型的命名空间」**,
  **也是为什么解析时 `find` 的路径要一段段换命名空间**(06.01.04 踩过这个坑)。

统计用的是正则 <([a-z0-9]+):, 只数开标签,不数闭标签——所以 25 这个数是「标签对数」,不是「标签数」。 口径要说清,否则会和「响应里有多少个尖括号」对不上。 最后那段「换前缀」是本节的重点验证: 把 tds 改成 foo,声明也改成 foo,设备照样返回正确的响应。 这直接证明「前缀只是别名」,也解释了为什么解析代码里应该用 Clark 记法而不是前缀。 反例就在正文里:用字符串替换换前缀会同时改坏 xmlns: 声明和其他含相同字母的前缀, 这个错我犯过一次,报的是 unbound prefix,而真正的原因在替换脚本里,不在 XML 里。

30 秒实验

  1. 把 re.finditer 的正则改成也匹配闭标签,看计数变成多少。
  2. 把前缀换成 soap,同时把 s: 也换掉,看还能不能解析。
  3. 只换前缀不换 xmlns: 声明,看报的是什么错(应该是 unbound prefix)。

常见误区

  1. 以为前缀有语义。 前缀只是别名,真身份是命名空间 URI + 本地名,换前缀不改语义。
  2. 以为 tt 也是某个 service 的前缀。 它是公共类型的前缀,所有 service 共用, 所以出现次数最多。
  3. 用字符串替换改前缀。 会连 xmlns: 声明一起改坏,必须按元素和属性改。
自测三问(点开对答案) 1. `tds`、`trt`、`tt` 分别是什么的缩写? —— **Type Device Service、Type Real Time、Type Toolkit。** **`tt` 是公共类型**,不是某个 service。 2. 为什么所有前缀都以 T 开头? —— 因为 ONVIF 的类型定义集中在一个 `types.xsd` 里,**所有 service 都从它引类型**, 前缀沿用了同一套命名习惯。 3. 换前缀会不会改变报文语义? —— **不会,只要 `xmlns:` 声明一起换。** **语义由命名空间 URI 决定,不由前缀决定。**

下一节讲 Web 服务:ONVIF 里「一组地址 + 一张能力表」是怎么落成两样东西的。

相关笔记