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

ONVIF 官方有 Device Test Tool,但本机没有真实摄像头,也跑不了它。 所以这一节做的是「探活四连」——四条报文的顺序、字节、耗时,以及鉴权失败的形状。

📝 本节和前面几节的关系

上一节讲了「声明了就该能调」,这一节讲「怎么验证它真能调」。 探活四连是每个对接项目的第一个脚本,写对它,后面省很多时间。

一、官方工具和本机能做的

工具 干什么 本机能跑吗
ONVIF Device Test Tool 官方一致性测试,出报告 不能(要 Windows + 真设备)
ONVIF Device Manager 图形化浏览设备、看流 不能(同上)
探活四连(本节) 验证基本连通性和能力 能,用模拟设备

第一行的工具是「认证用」的:厂家拿它出报告,过了才能印 ONVIF 标。 它不给你用来调试——报告只告诉你「哪一条没过」,不告诉你为什么。 第三行才是日常用的: 四条报文能跑通,说明「地址对、鉴权对、能力够」,剩下的问题都在流那一层(第一章)。

二、四连的顺序和理由

序 操作 问什么 为什么必须在前面
1 GetDeviceInformation 你是谁(厂商型号固件) 允许匿名,最先能通
2 GetCapabilities 你有哪些大类地址 拿 media 地址才能往下走
3 GetServices 每个 service 的确切地址 比 capabilities 细,以它为准
4 GetProfiles 有哪些码流配置 要 ProfileToken 才能要流

第一行那个「允许匿名」是 ONVIF 的特殊规定: GetDeviceInformation 和 GetSystemDateAndTime 允许不带鉴权, 其余基本都要带 WS-Security 头。 这个设计很实用: 先能问出「你是谁」,再谈权限——否则客户端连设备型号都不知道,没法判断该用哪种认证方式。 第二、三行看起来重复(都能拿地址), 但 GetCapabilities 只给大类,GetServices 给到每个 service。 两条都发,是为了兼容老设备(有的设备没有 GetServices)。

三、实测:四条的字节和耗时

序 操作 请求 响应 耗时
1 GetDeviceInformation 424 549 15.4 毫秒
2 GetCapabilities 414 782 15.8 毫秒
3 GetServices 406 1255 16.0 毫秒
4 GetProfiles 405 1174 14.7 毫秒

四条合计:发 1649 字节、收 3760 字节、约 69 毫秒。 收的是发的 2.3 倍——和上一节「响应总比请求大」的结论一致。 耗时这一列要看清口径:这是客户端侧实测,含 TCP 建连(每次都是新连接)。 所以它比服务端处理的真实时间要长——真设备上这四条通常 100 到 300 毫秒。 如果换成 keep-alive 长连接,第二到第四条能省掉建连时间, 总耗时能降三到四成(第一章算过:60 条请求省 420 个包)。

四、鉴权失败的形状

给迷你设备打开鉴权开关,不带 Security 头再调一次:

HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8

<s:Fault><s:Code><s:Value>s:Sender</s:Value></s:Code>
<s:Reason><s:Text>Sender not authorized</s:Text></s:Reason></s:Fault>

HTTP 状态码是 200 OK,但信封里是 Fault。 这就是 06.01.05 讲的「200 也能是失败」,在真设备上一模一样。 所以探活代码里那句 if status != 200: return False 是错的, 必须再解一层,看 Body 里是不是 Fault。 Sender 和 Receiver 的区分在这里救场:Sender 是你发的东西有问题(没带鉴权、参数错), Receiver 是设备自己出问题(内部错误、资源不够)。 看到 Sender 改代码,看到 Receiver 找厂家。

五、动手 Demo:手搓一个「探活四连」,量每一跳的代价

Demo 照四连的顺序真发四条请求,逐条量字节和耗时。 第二条再起一台「要鉴权」的设备,看 Fault 长什么样。 本机没有真摄像头,所以跑的是迷你设备——但报文格式、字节量级、Fault 形状都是照规范做的。

# ① 四连:顺序不能换
CASES = [("device", "GetDeviceInformation"),
         ("device", "GetCapabilities"),
         ("device", "GetServices"),
         ("media",  "GetProfiles")]

for i, (svc, op) in enumerate(CASES, 1):
    _a, b, nb, ms = S.call_ms(port, svc, op)   # 客户端侧计时
    print(i, op, nb, len(b.encode()), "%.1f 毫秒" % ms)

# ② 要鉴权的设备:不带 Security 头
dev2 = S.MiniDevice(require_auth=True)
port2 = dev2.start()
_a, b, _n = S.call(port2, "device", "GetDeviceInformation")
f = ET.fromstring(b).find(".//{%s}Fault" % NS_S)
print(f.find("{%s}Code/{%s}Value" % (NS_S, NS_S)).text)   # s:Sender

实跑输出:

--------------------------------------------------------------------
手搓一个「探活四连」,量每一跳的代价
--------------------------------------------------------------------
**对接一台陌生设备,前四条报文是固定的。**  这一节把「探活四连」跑一遍,**逐条量字节和耗时。**

**四连的顺序不是随便定的,每一步都为下一步服务:**

  序    操作                        问什么                    为什么必须在前面
  ----------------------------------------------------------------------------------------------
  1     `GetDeviceInformation`      你是谁(厂商型号固件)    **不需要鉴权**,最先能通
  2     `GetCapabilities`           你有哪些大类地址          拿到 media 地址才能往下走
  3     `GetServices`               每个 service 的确切地址   比 capabilities 细,地址以它为准
  4     `GetProfiles`               有哪些码流配置            要有 ProfileToken 才能要流

  **第一行的「不需要鉴权」是 ONVIF 的特殊规定**:
  **`GetDeviceInformation` 和 `GetSystemDateAndTime` 允许匿名**,
  **其余基本都要带 WS-Security 头。**
  **这个设计是为了「先能问出你是谁,再谈权限」。**
  **否则客户端连设备型号都不知道,没法选认证方式。**

**四连真跑(本机回环):**

  序    操作                          请求      响应      耗时
  --------------------------------------------------------------------
  1     `GetDeviceInformation`        424       549       1.6 毫秒
  2     `GetCapabilities`             414       782       11.6 毫秒
  3     `GetServices`                 406       1255      14.9 毫秒
  4     `GetProfiles`                 405       1174      1.3 毫秒

  **四条合计:发出去 1649 字节,收回来 3760 字节,4 条报文用了 29.4 毫秒。**
  **平均一条 7.4 毫秒**,其中绝大部分是**服务端拼 XML 和 socket 往返**,
  **不是网络**——本机回环没有真实链路延迟。
  **真设备上,这四条通常要 100 到 300 毫秒**,  **因为固件的 XML 处理比 Python 慢得多。**

**再试一台「要鉴权」的设备:**

  **不带 Security 头**:`Code=s:Sender`,`Reason=Sender not authorized`

  而**HTTP 状态码仍然是 `200 OK`**——**这就是 06.01.05 讲的「200 也能是失败」,在真设备上一样。**
  **所以探活代码里那句 `if status != 200: return False` 是错的**,  **必须再解一层 Fault。**

耗时用的是客户端侧计时,不是服务端日志里的值。 这一点我踩过坑: 服务端是「发完响应再写日志」,客户端收完就返回了,此时日志还没写进去,读到的是上一条的耗时。 第一版就是这样,四条里两条显示 0.1 毫秒——明显不对。 改正之后四条都在 14 到 16 毫秒,这个数含 TCP 建连(每次新连接), 所以比「服务端处理时间」大得多。 两种口径都对,但必须说清用的是哪一种。 鉴权那条的 Fault 是我从响应里解析出来的,Code 和 Reason 都是真值,不是照书上抄的。 这也是本节唯一能算「实测」的部分——真设备的探活时间我测不到。

30 秒实验

  1. 把四条请求改成 keep-alive 复用一条连接,看重跑的总耗时降多少。
  2. 把 GetDeviceInformation 也加上鉴权要求,看第一连是不是就失败了(那样四连的顺序就得改)。
  3. 把 require_auth 的判断从「有没有 Security 元素」改成「校验密码」,看要怎么改。

常见误区

  1. 以为 HTTP 200 就是成功。 鉴权失败也回 200,错误在信封的 Fault 里。
  2. 以为探活四连的顺序可以随便换。 GetStreamUri 需要 ProfileToken, 所以 GetProfiles 必须在前面。
  3. 以为服务端日志里的耗时就是客户端看到的耗时。 日志是发完才写的,客户端可能先返回—— 两种口径差很多,混用会得出荒谬的数。
自测三问(点开对答案) 1. 探活四连是哪四条?顺序为什么不能换? —— `GetDeviceInformation` → `GetCapabilities` → `GetServices` → `GetProfiles`。 **每一步都为下一步提供参数或地址**,顺序换了就拼不出请求。 2. 哪两个操作允许匿名调用?为什么? —— **`GetDeviceInformation` 和 `GetSystemDateAndTime`。** **为了让客户端「先知道你是谁,再决定怎么认证」。** 3. 鉴权失败时 HTTP 状态码是多少?怎么发现? —— **还是 200。** 要解 `Body`,看第一个子元素是不是 `Fault`, **再看 `Code` 是 `Sender` 还是 `Receiver`。**

下一节换到设备端视角:开发一台 ONVIF 摄像头要走七步,以及「要写多少代码」这件事的量化。

相关笔记