📹 第八章 ONVIF框架代码案例应用
📄 一句话概括

设备档案是三次调用拼出来的:信息、能力、服务清单,三样各管一件事。 其中 GetCapabilities 最实用——它一次告诉你每个 service 的地址, 所以永远不要硬编码 /onvif/media_service。

📝 本节和前面几节的关系

上一节跑通了闭环,这一节把「取信息」这件事做完整。 很多人只调 GetDeviceInformation 就以为拿到了设备档案, 其实那只是「铭牌」,不是「能力」。

一、三次调用各答什么

操作 回答什么 典型用途
GetDeviceInformation 厂家/型号/固件/序列号 台账、工单
GetCapabilities 各 service 的地址 决定后面调哪些
GetServices 完整服务清单 + 版本 挑版本、查开关

第一行是「它是什么」, 第二、三行是「它能干什么」——做资产管理系统只需要第一行,做客户端必须三行都要。 第二行那个「地址」是最容易被浪费的信息: 很多人拿到之后就扔了,然后自己在代码里写死 /onvif/media_service—— 换个厂家路径变了,整个客户端就不能用。 正确做法是「先问地址,再去调」,这样同一份代码能适配所有厂家。

二、请求大小由信封决定

实测三次调用的请求字节是 424、414、406——几乎一样。 原因很简单:请求体都是空的(<tds:GetXxx/>),差别只在操作名那十几个字符。

组成部分 字节 占比
HTTP 头 约 150 35%
SOAP 信封与命名空间 约 240 56%
业务元素 十几到几十 不到 5%

第三行那个「不到 5%」在第七章讲过(412 字节里意图只占 27), 这里又验证了一次——所以「优化带宽」这个目标,在 ONVIF 里几乎不是优化业务元素能解决的。 真正能省的是「减少调用次数」和「复用 TCP 连接」,而不是「把 XML 写短一点」。 第二行那个 56% 是命名空间:每个请求都要带 5 个左右的 xmlns,这是 SOAP 的固有开销, 省不掉。

三、响应大小差别很大

实测响应是 549、782、1255——差别是三倍,因为内容量不同:

响应 字节 为什么
GetDeviceInformation 549 五个字段
GetCapabilities 782 三组能力,各带地址
GetServices 1255 每个服务一条,还有版本号

第三行最大是必然的:它要把 11 组 service 全列一遍, 每组还带 Namespace、XAddr、Version 三样。 所以「先调 GetServices 看有哪些服务」这个做法, 在设备多的时候是有代价的——一台设备 1.2 KB,一千台就是 1.2 MB,而且每次轮询都花这个钱。 工程上的折中:发现阶段调一次、存下来,轮询阶段不再调。

四、地址里可能藏着坑

GetCapabilities 返回的地址有三种陷阱:

形态 问题 怎么处理
端口与 device 不同 套用 device 的端口就连不上 按返回值用
主机名而不是 IP DNS 解析不了就失败 能解析才用,否则回退
https 开头 客户端没配 SSL 就失败 检查是否编进 SSL

第一行是真事故:有的厂家把 media 放在 8080、device 放在 80, 而很多人写代码时只记了 device 的端口,于是 GetProfiles 一直超时。 第二行更隐蔽:有的设备返回 http://IPC-2000/onvif/media_service(用主机名), 而你的机器解析不了这个名字——表现是「发现成功、能力取到、但后续全超时」。 第三行在「设备开了 HTTPS 但客户端没编 SSL」时出现,而症状是「连接被重置」, 看起来像网络问题。

五、动手 Demo:设备档案是三次调用拼出来的

Demo 连调三次,量请求与响应字节,再把 GetCapabilities 里的地址抽出来。 重点看三个地址的端口是不是一样的——这一条能省掉很多排查。

# 三次调用,量请求与响应
for op in ("GetDeviceInformation", "GetCapabilities", "GetServices"):
    t0 = time.perf_counter()
    _a, b, n = S.call(port, "device", op)
    ms = (time.perf_counter() - t0) * 1000
    print(op, n, len(b.encode()), "%.2f ms" % ms)

# 从 Capabilities 里抽每个 service 的 XAddr
for el in ET.fromstring(bodies["GetCapabilities"]).iter():
    addr = el.find(".//{%s}XAddr" % S.NS_TT)
    if addr is not None:
        print(el.tag.split("}")[1], addr.text)

实跑输出:

--------------------------------------------------------------------
设备档案是三次调用拼出来的
--------------------------------------------------------------------
**`GetDeviceInformation` 只给你五个字段**,
**想知道「这台设备能干什么」,还得问另外两个**:

  操作                       回答什么                     典型用途
  ---------------------------------------------------------------------------
  `GetDeviceInformation`     厂家/型号/固件/序列号     **台账**
  `GetCapabilities`          **各 service 的地址**        **决定后面调哪些**
  `GetServices`              **完整服务清单 + 版本**      挑版本、查开关

**第二行最实用**:**它一次告诉你媒体、云台、事件各自的服务地址**,
**不用猜路径、不用硬编码 `/onvif/media_service`。**
**很多现场问题就出在硬编码**:换个厂家路径就变了,
**正确做法是「先问地址,再去调」。**

**三次调用实测**:

  操作                       请求字节     响应字节     耗时
  ---------------------------------------------------------------
  `GetDeviceInformation`     424          549          1.11 毫秒
  `GetCapabilities`          414          782          0.85 毫秒
  `GetServices`              406          1255         0.68 毫秒

**第二列三行几乎一样**,因为请求体都是空的(`<tds:GetXxx/>`),
**差别只在操作名**——**请求大小由「信封」决定,不由「操作」决定。**
**第三列就不同了**:`GetServices` 的响应最大,因为它把每个服务都列一遍。
**第四列别当真**:同一台机器上三次调用抖到十倍是常事,因为每个请求都新建 TCP 连接,
还要等线程调度。

**`GetCapabilities` 里真正要抄下来的就这几行地址**:

  Device         → http://127.0.0.1:52707/onvif/device_service
  Media          → http://127.0.0.1:52707/onvif/media_service
  PTZ            → http://127.0.0.1:52707/onvif/ptz_service

**注意这些地址的端口与路径**:
**它们可能和 `device_service` 不同**(有的厂家分开端口),
**所以「把 device 的地址套给 media 用」这种写法迟早出事。**
**正确姿势**:**发现 → 取 capabilities → 从里面拿每个 service 的地址 → 再调。**

第一段的输出里第三列才是重点:请求字节 424、414、406, 几乎一样——因为它们都是「空请求体」,差别只在操作名。 这个现象可以用来判断「请求大小是否合理」: 如果你写出来的请求有 800 字节,说明多带了不该带的元素。 第二段那个 el.find 要带命名空间({...}XAddr), 这是 ElementTree 的硬要求——直接写 "XAddr" 一个都找不到。 输出里那三个地址的端口是一样的(本机模拟器都指向同一个服务),但真实设备上不一定—— 所以别把这个结果当成「端口总是一样」的证据。 这一节的表格里写「端口可能不同」是有现场依据的,不是推测。

30 秒实验

  1. 把 GetServices 的响应按行拆开,数一下一共列了几个服务。
  2. 在 GetCapabilities 里找 Media 之外的地址,看还有哪几个。
  3. 把三次调用的顺序换一下(先 GetServices),看耗时有没有变化。

常见误区

  1. 只调 GetDeviceInformation 就以为拿到了设备档案。 那只是铭牌, 「能干什么」要 GetCapabilities 和 GetServices。
  2. 把 device 的地址套给 media 用。 有的厂家两个 service 端口不同,套用会一直超时。
  3. 靠「把 XML 写短」来优化带宽。 业务元素不到请求的 5%, 真正能省的是「减少调用」和「复用连接」。
自测三问(点开对答案) 1. `GetCapabilities` 最实用的地方是什么? —— **它一次告诉你每个 service 的地址**,**所以不用猜路径、不用硬编码 `/onvif/media_service`。** 2. 为什么三次请求字节几乎一样? —— **因为请求体都是空的**,差别只在操作名那十几个字符。大小由信封决定,不由操作决定。 3. 「发现成功、能力取到、后续全超时」可能是为什么? —— **地址里用的是主机名,而本机解析不了**;**或者地址是 `https` 而客户端没编 SSL。**

下一节讲鉴权:四种情况、一个反直觉的「失败也回 200」。

相关笔记