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

Linux 下没有「exe 同目录」这个概念,所以 .nsmap 一般不落地成文件。 做法是把那张表编进程序,而表里有两个名字只差一个字符、意思完全不同。

📝 本节和前面几节的关系

08.01.07 讲的是 Windows 版:把 *.nsmap 拷到 exe 旁边。 这一节讲 Linux 版:同一个信息,换个放法, 顺带说清 SOAP-ENV 与 SOAP_ENV 这对容易搞混的名字。

一、两种放法

Linux 不是不能读文件,是「文件该放哪儿」没有标准答案:

做法 怎么写 适合
读文件 soap_load_namespaces 要能换配置
编进程序 Namespace 数组 固定部署

第二行更适合 Linux:可执行文件在 bin/,数据在 /usr/share、配置在 /etc, 一个 .nsmap 到底该去哪儿,不同发行版各有各的规矩。 编进程序就绕开了这个问题: 二进制拷过去就能跑,不用管配置文件在哪——这正好是 Linux 部署最烦的那件事。 代价是「改命名空间要重新编译」:不过命名空间是按规范定的,一般十年不变, 所以这个代价可以接受。

二、表长什么样

十个条目,一条一个命名空间:

struct Namespace namespaces[] = {
  {"SOAP-ENV", "http://www.w3.org/2003/05/soap-envelope"},
  {"xsd", "http://www.w3.org/2001/XMLSchema"},
  {"xsi", "http://www.w3.org/2001/XMLSchema-instance"},
  {"tds", "http://www.onvif.org/ver10/device/wsdl"},
  {"trt", "http://www.onvif.org/ver10/media/wsdl"},
  {"tptz", "http://www.onvif.org/ver20/ptz/wsdl"},
  {"tt", "http://www.onvif.org/ver10/schema"},
  {"wsdd", "http://schemas.xmlsoap.org/ws/2005/04/discovery"},
  {"wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing"},
  {"wsse", "http://docs.oasis-open.org/wss/2004/01/"
           "oasis-200401-wss-wssecurity-secext-1.0.xsd"},
  {NULL, NULL}
};

这十个条目覆盖了第八章用到的全部服务:设备管理、媒体、云台、类型定义、发现、寻址、鉴权。 最后那个 wsse 的 URI 太长,在 C 里用两个字符串字面量拼接——编译器会把它们接成一个, 这样源码里就不会有一行两百字符。

三、三条必须遵守的规矩

规矩 违反了会怎样
最后一项是 {NULL, NULL} 读到野内存
第一项是信封命名空间 查表顺序错
名字要和代码里一致 unbound prefix

第一行是硬约束:gSOAP 用「两个空指针」判断表结束, 数组长度它不认——漏了这一行,它会一直往后读,读到的是栈上或静态区里的随机字节。 表现是随机崩溃或者「找不到命名空间」, 而崩溃位置每次都不同——这种 bug 最难查,因为加一行打印就「好了」(内存布局变了)。 第三行是逻辑约束:表里的名字是「前缀」,代码里的也是「前缀」,两处必须逐字符相同, 包括大小写和连字符。

四、SOAP-ENV 与 SOAP_ENV 是两个东西

这一条是本节的真正坑,因为它看起来像是同一个名字的两种写法:

写法 出现在哪 是什么
SOAP-ENV XML 前缀 报文里的字符串
SOAP_ENV C 语言标识符 结构体名

第一行带连字符,是 XML 里的前缀,它出现在真正的报文里:<SOAP-ENV:Envelope> 这种。 第二行带下划线,是 C 的标识符, 因为 C 的变量名不能有连字符—— 于是 gSOAP 把它写成 SOAP_ENV__Header、SOAP_ENV__Envelope。 typemap.dat 里写的是后者(SOAP_ENV__Header),而 soapcpp2 生成的报文里用的是前者。 两个都存在、各管一处:把 SOAP-ENV 写进 typemap.dat 会解析失败, 把 SOAP_ENV 写进 namespaces[] 会 unbound prefix。

五、自己的命名空间怎么加

很多厂家的设备有私有扩展,这些扩展不在 ONVIF 标准命名空间里:

情形 要不要加 放哪
只用标准操作 不用加 —
用了厂家私有字段 要加 表末尾
自己定义的扩展 要加 表末尾

第二、三行都要加在表末尾,不要插在标准项中间——因为第一项固定是信封命名空间, 中间插入会让「第几项是什么」这种依赖顺序的逻辑错位。 加之前先确认前缀没被占用:标准里已经用了 tds、trt、tt、tptz, 再起一个同名的会把标准项顶掉,表现是「标准操作突然 unbound prefix」。 这也是「先对清单再动手」的道理:表里已有的十个前缀,就是你的可用前缀黑名单。

六、动手 Demo:Linux 下命名空间表怎么放

Demo 把那张表按 C 的写法打印出来,顺便验证三件事:条数、结尾项、第一项。 输出里那十行就是可以直接粘进 .c 的代码,不是伪代码。

ns = [("SOAP-ENV", "http://www.w3.org/2003/05/soap-envelope"),
      ("xsd", "http://www.w3.org/2001/XMLSchema"),
      ("xsi", "http://www.w3.org/2001/XMLSchema-instance"),
      ("tds", "http://www.onvif.org/ver10/device/wsdl"),
      ("trt", "http://www.onvif.org/ver10/media/wsdl"),
      ("tptz", "http://www.onvif.org/ver20/ptz/wsdl"),
      ("tt", "http://www.onvif.org/ver10/schema"),
      ("wsdd", "http://schemas.xmlsoap.org/ws/2005/04/discovery"),
      ("wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing"),
      ("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd")]

for k, v in ns:
    print('  { "%s", "%s" },' % (k, v))
print("共 %d 项,结尾补 {NULL, NULL}" % len(ns))

实跑输出:

--------------------------------------------------------------------
Linux 下命名空间表怎么放
--------------------------------------------------------------------
**Windows 版是把 `*.nsmap` 拷到 exe 同目录**,
**Linux 版更常见的做法是「编进程序里」**——
**因为 Linux 下没有「exe 同目录」这个概念**(可执行文件在 `bin/`,数据在 `/usr/share`)。

  做法             怎么写                                    适合
  ------------------------------------------------------------------------
  **读文件**       `soap_load_namespaces()`                  要能换配置
  **编进程序**     `struct Namespace namespaces[] = {…}`     **固定部署**

**第二行更适合 Linux**:**一个二进制文件拿过去就能跑**,
**不用管「配置文件放哪儿」**——
**这正好是 Linux 部署最烦的那件事。**

**本机把这张表按 C 的写法打出来(10 项)**:

  { "SOAP-ENV", "http://www.w3.org/2003/05/soap-envelope" },
  { "xsd", "http://www.w3.org/2001/XMLSchema" },
  { "xsi", "http://www.w3.org/2001/XMLSchema-instance" },
  { "tds", "http://www.onvif.org/ver10/device/wsdl" },
  { "trt", "http://www.onvif.org/ver10/media/wsdl" },
  { "tptz", "http://www.onvif.org/ver20/ptz/wsdl" },
  { "tt", "http://www.onvif.org/ver10/schema" },
  { "wsdd", "http://schemas.xmlsoap.org/ws/2005/04/discovery" },
  { "wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing" },
  { "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" },

**注意三点**:
**① 这个数组必须以 `{NULL, NULL}` 结尾**,
**因为 gSOAP 用「空指针」判断表结束**,漏了会读到野内存;
**② 第一项必须是 SOAP 信封的命名空间**,顺序不能乱;
**③ 表里的名字要和生成代码里用的前缀完全一致**——
**`SOAP-ENV` 带连字符,写成 `SOAP_ENV` 就对不上**(这两个在 gSOAP 里是不同的东西)。

**第三点是真事故**:`soapcpp2` 生成的行里用的是 `SOAP-ENV`,
**而 `typemap.dat` 里写的是 `SOAP_ENV`(下划线)**,
**两个都存在,各管一处**——**搞混了就是 unbound prefix。**

十行输出无一例外都以 { 开头、}, 结尾——这就是 C 的初始化列表语法, 所以这段输出可以整段粘进源码,不用改一个字符。 那句 print 的花括号与引号要数清:外层是单引号包字符串、内层双引号当字面量, 所以内层双引号不用转义——这正是选单引号当外层的理由。 最后一行那个「补 {NULL, NULL}」是提醒不是代码:Python 里没有 NULL, 这一行只能靠手写,所以要在输出里明说——这种「代码生成不了的部分」一定要标出来,否则会漏。 注意十个条目里 SOAP-ENV 带连字符、其余都是短前缀:这个不对称是有原因的, 因为 SOAP-ENV 是 W3C 定的 XML 前缀,不能改。

30 秒实验

  1. 把 {NULL, NULL} 那行删掉再编,看会不会崩(在虚拟机里试)。
  2. 把 SOAP-ENV 改成 SOAP_ENV,看报文里前缀变成什么。
  3. 把自己公司域名当命名空间加一项,看它会不会影响标准操作。

常见误区

  1. 以为 SOAP-ENV 和 SOAP_ENV 是同一个东西。 一个是 XML 前缀、一个是 C 标识符, 写错地方就是 unbound prefix。
  2. 表末尾不写 {NULL, NULL}。 gSOAP 靠空指针判结束,漏了会读到野内存, 崩的位置每次都不同。
  3. 把自己的命名空间插在表中间。 第一项必须是信封命名空间,中间插入会让依赖顺序的逻辑错位。
自测三问(点开对答案) 1. 为什么 Table 的第一项必须是信封命名空间? —— **因为 gSOAP 查表时按「第一项是 SOAP 信封」来定位报文最外层的解析规则。** 顺序一乱, 整个 XML 的解析起点就错了。 2. `{NULL, NULL}` 漏了为什么会「随机」崩? —— **因为没有结束标记,查表会一直往后读**,读到静态区里别的变量。 加一行打印就「好了」,是因为内存布局变了。 3. 加了厂家私有命名空间之后,为什么要先查前缀有没有被占用? —— **同名会把标准项顶掉**,表现是标准操作突然 unbound prefix。 表里已有的前缀就是可用前缀的黑名单。

下一节讲封装:把「拼信封、解 XML」这两件事关进一个类里。

相关笔记