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

*.nsmap 是「前缀 → URI」的表,本质就是 XML 的 xmlns。 ONVIF 需要十项,少一项就是 unbound prefix—— 而且「缺 nsmap」和「路径写错」的报错长得完全不一样。

📝 本节和前面几节的关系

上一节生成了八个文件,这一节讲其中一个最容易被忽略的:.nsmap。 它不是代码、不是配置,是一个「对照表」, 而 gSOAP 的每个函数名、每个序列化动作都依赖它。 这一节和 08.08.09 是同一件事的两个平台版本,Linux 下放法不同。

一、它到底解决什么问题

XML 里 tt:Width 这个写法,tt 只是个「缩写」, 真正的名字是「http://www.onvif.org/ver10/schema + Width」。 nsmap 干的就是「记住这个缩写对应哪个 URI」这件事。

角色 给谁看 少了会怎样
发请求时 设备 设备不知道 tt 是谁,回 Fault
收响应时 gSOAP 解析器 解析失败,抛 unbound prefix
生成函数名时 soapcpp2 那一族函数生成不出来

第三行是这一节最反直觉的一点:nsmap 不只影响运行期,还影响函数名。 因为函数名是 soap_call___tds__GetProfiles, 中间那个 tds 就来自这张表——表里没有 tds,这族函数就不存在。 所以「少一项」的后果分两层:生成期少一族函数,运行期解不了那一族的响应。

二、十项都要哪几个

前缀 URI 结尾 必须吗
SOAP-ENV /2003/05/soap-envelope 必须,且必须第一项
xsd/xsi XML Schema 必须
tds /ver10/device/wsdl 必须
trt /ver10/media/wsdl 取流就必须
tt /ver10/schema 必须,出现频率最高
wsdd /ws/2005/04/discovery 要发现就必须
wsse 鉴权那一段 要鉴权就必须

第一行那个「必须第一项」不是格式要求,是 gSOAP 的约定: 它按顺序取第一个作为「信封的命名空间」,放错了会把业务命名空间当成信封的,直接解析崩。 第五行那个 tt 是「最该检查的一项」: 它不像 tds、trt 那样「看着像主类型」,很容易在抄表时漏掉, 而它出现的次数最多——漏了它,几乎所有响应都解不了。 第六、七行是「按需」:只做取流的话 wsdd 与 wsse 可以先不放, 但只要加一行「自动发现」,wsdd 就变成必须的。

三、两种病,两种报错

这一节最实用的一段:把「缺 nsmap」和「路径写错」分开。

症状 真实原因 怎么确认
解析时报 unbound prefix 缺 nsmap 那一项 看报错里的前缀名
解析成功但字段全空 XPath/路径写错 打印整棵树看有没有那个节点
发出去设备回 Fault 请求里少了 xmlns 抓包看 Header

第一行和第二行的区别是「解析器到底能不能理解这个名字」: 前缀没定义,连名字都无法解释,一定抛异常; 前缀有定义但路径写错,解析器能找到树、只是没找到那个节点,返回 None。 所以「返回 None」和「抛异常」是两条完全不同的排查路线, 而很多人一看到「字段是空的」就去查 nsmap,方向就错了。 第三行是「发出去」那一侧:gSOAP 会用 nsmap 自动加 xmlns,表里没有的前缀它不会加, 于是请求里出现「用了 tt: 但没声明」的情况——设备会回 Sender 类的 Fault, 理由往往是「无法解析」。

四、那条最长的 URI 为什么要复制

wsse 那一项有 78 个字符:

http://docs.oasis-open.org/wss/2004/01/
oasis-200401-wss-wssecurity-secext-1.0.xsd

它长得离谱,而且里面有几个容易打错的片段:oasis-200401(不是 2004-01)、 secext(不是 sec-ext)、1.0(不是 1)。 这几个地方任何一个打错,结果都是一样的:unbound prefix 或 Fault。 所以这一条最该「复制粘贴」,不要手打。 更麻烦的是「打错了也看不出来」:78 个字符里错一个,肉眼几乎发现不了, 而排查时你会怀疑鉴权算错了、时钟不对、密码不对——唯独不会怀疑「URI 里少了个连字符」。 所以我的建议是:把这张表当成「常量表」写进代码,只从官方文档复制,永远不手敲。

五、动手 Demo:命名空间表少一项就 unbound

Demo 先用一段真报文试「有 xmlns:tt 与没有」的区别, 再把十项表打出来,看那条最长的 URI 有多长。 重点看两种错的报错差别。

# 同一个报文,去掉 xmlns:tt 再解析一次
def parse(x, drop=""):
    y = x if not drop else x.replace(
        ' xmlns:%s="http://www.onvif.org/ver10/schema"' % drop, "")
    try:
        r = ET.fromstring(y)
        return r.find(".//{http://www.onvif.org/ver10/schema}Width").text
    except ET.ParseError as e:
        return "ParseError: %s" % e

print(parse(xml))        # 1920
print(parse(xml, "tt"))  # unbound prefix

实跑输出:

--------------------------------------------------------------------
命名空间表少一项就 unbound
--------------------------------------------------------------------
**`*.nsmap` 是一张「前缀 → URI」的表,本质就是 XML 的 `xmlns`。**
**ONVIF 需要的项不多,但一个都不能少**:

  前缀           URI
  -----------------------------------------------------------
  `SOAP_ENV`     `http://www.w3.org/2003/05/soap-envelope`
  `tds`          `http://www.onvif.org/ver10/device/wsdl`
  `trt`          `http://www.onvif.org/ver10/media/wsdl`
  `tptz`         `http://www.onvif.org/ver20/ptz/wsdl`
  `tev`          `http://www.onvif.org/ver10/events/wsdl`
  `tim`          `http://www.onvif.org/ver20/imaging/wsdl`
  `tan`          `http://www.onvif.org/ver20/analytics/wsdl`
  `tt`           `http://www.onvif.org/ver10/schema`
  `wsdd`         `http://schemas.x…ws/2005/04/discovery`
  `wsse`         `http://docs.oasi…urity-secext-1.0.xsd`

**一共 10 项,其中最后两项不是 ONVIF 自己的**:
**`wsdd` 是设备发现,`wsse` 是鉴权。**
**两项都不是「可选项」**:**没有 `wsdd` 就发不出 Probe**,**没有 `wsse` 就填不了 Header。**
**`wsse` 那条在表里被我截短了,完整的是:**
`http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd`
**这一条特别长,抄错一个字符就是 unbound——所以它最该复制粘贴,不要手打。**

**用一段真报文试一次。**
**这是解析器看到的样子:**

带 `xmlns:tt` → 解析成功,Width=1920
去掉 `xmlns:tt` → **ParseError:unbound prefix: line 1, column 91**

**第二行那个报错是「前缀找不到」,不是「元素不存在」。**
**区别很重要**:
**元素不存在是「找到了但没内容」,前缀不存在是「连名字都无法解释」。**
**前者不报错(返回 None),后者一定抛异常。**
**所以「解析成功但字段全是空」和「解析直接失败」是两种病**,
**前一种是路径写错,后一种是 nsmap 缺项。**

两行输出的差别就是这一节的全部: 带 xmlns:tt 解析成功、拿到 1920;去掉它就抛 unbound prefix。 注意报错里那个「line 1, column 91」:它指的是「tt: 这个前缀第一次出现的位置」, 不是「缺少声明的位置」——所以列号对排查没什么帮助,要看的是前缀名。 后面那段「两种病」的总结是从这个实验里来的: 我把路径写错(比如找 Width 写成 Widht)会得到 None,不抛异常, 和这里的 ParseError 完全不同。 最后那个 78 字符的 URI 打印出来, 是为了让人直观看到「这条不能手打」——贴在屏幕上抄都容易错。

30 秒实验

  1. 把报文里的 tt:Width 改成 tt2:Width,看报错是 unbound 还是「找不到节点」。
  2. 在 parse() 里加一句 print(r.tag),看根元素的完整名字长什么样。
  3. 把 wsse 那条 URI 故意删掉一个连字符,看它会不会被 ET 接受。

常见误区

  1. 把 nsmap 当成「可有可无的配置」。 它还决定函数名—— 表里没有 tds,soap_call___tds__* 这族函数就不存在。
  2. 看到「字段是空的」就去查 nsmap。 缺 nsmap 一定抛异常, 字段空是路径写错,两条完全不同的排查路线。
  3. 手打那条 78 字符的 wsse URI。 错一个字符症状完全相同,而你会去怀疑鉴权算法、时钟、 密码。
自测三问(点开对答案) 1. nsmap 少一项会有哪两层后果? —— **生成期那一族函数生成不出来**,**运行期解不了那一族的响应**(unbound prefix)。 2. 「解析成功但字段是空的」是不是 nsmap 的问题? —— **不是。缺 nsmap 一定抛 `unbound prefix` 异常**; **返回空说明前缀有定义,是路径或元素名写错了。** 3. 为什么 `SOAP-ENV` 必须放在第一项? —— **gSOAP 按顺序取第一个当信封的命名空间**,放错了会把业务命名空间当信封的,解析直接崩。

下一节挑文件:八个产物里只拷五个,另外还要从 gSOAP 源码里拿三份。

相关笔记