📹 第六章 SOAP及gSOAP实战
📄 一句话概括

一个 gSOAP 客户端桩只做三件事:拼信封、发 HTTP POST、解响应取值。 VS 里那些配置(加文件、配包含目录、链 ws2_32)本质上都是在为这三件事备料。

📝 本节和前面几节的关系

06.02.01 到 06.02.04 都在准备工具,这一节把工具产物真正跑起来。 本机没有 VS、没有 gSOAP,所以这一节用迷你 codegen 生成的桩去连本机的迷你服务端, 把「桩是怎么被调用的」跑通。

一、桩做的三件事

soap_call___ns1__GetDeviceInformation 这种函数看着吓人,拆开只有三步。

步 动作 gSOAP 生成代码里的对应物
① 拼 SOAP 信封(含 Header 鉴权) 序列化函数 soap_put___ns1__...
② 发 HTTP POST soap_send/soap_recv(内部用 socket)
③ 解响应、填结构体 反序列化函数 soap_get___ns1__...

这三步和 06.01.05 手写的那三步是同一件事—— 只不过手写时你自己拼字符串,gSOAP 时由生成的代码拼。 理解了这一点,gSOAP 就没有魔法了:它就是「把拼字符串这件事自动化」。 第②步里的 socket 在 Windows 上要靠 Winsock, 所以 VS 工程必须链 ws2_32.lib——这是本节第一个必踩的坑。

二、VS 工程要加哪些东西

| 项 | 加什么 | 漏了报什么 | |---|---|---|---| | 源文件 | soapC.cpp、soapClient.cpp | undefined symbol soap_... | | 头文件 | soapH.h、soapStub.h、stdsoap2.h | 编译期找不到声明 | | 包含目录 | gSOAP 的 include 目录 | soapH.h 里的 #import 失败 | | 链接库 | ws2_32.lib | unresolved external symbol __imp__socket |

第一行里的 stdsoap2.cpp 最容易漏:它不是 soapcpp2 生成的, 是 gSOAP 源码包里自带的运行时。少了它,编译期全过,链接期报几百个未定义符号。 第四行是 Windows 独有的:Linux 上 socket 在 libc 里、不用额外链, Windows 上必须显式链 Winsock 库。 报错里那个 __imp__socket 的 __imp__ 前缀就是「等着从 DLL 导入」的意思。

三、Content-Type 里的 action 参数

实测报文里那一行:

Content-Type: application/soap+xml; charset=utf-8;
              action="http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation"

这就是 SOAP 1.2 的做法(06.01.05 讲的版本差异): 它把 1.1 的 SOAPAction 头挪进了 Content-Type 的参数里。 但很多老设备只认 SOAPAction 头,所以实际代码常常两个都带: 在 soap 上下文里设 soap->action,同时让 gSOAP 生成 1.2 的 Content-Type。 这一条在联调时特别有用: 如果设备回「Action not supported」,先看它到底认哪个版本—— 抓一条自己的请求,看 action 是头还是参数。

四、一次完整调用的链路

把前面六节串起来,就是这一条链:

环节 谁提供 对应小节
知道有哪些接口、参数什么类型 WSDL 06.01.04
生成结构体和函数原型 wsdl2h 06.02.02
生成序列化和桩代码 soapcpp2 06.02.01
把调用装进信封 SOAP 06.01.05
送出去 HTTP over socket 06.01.02
取回值、检查 Fault 解析代码 06.01.05

六行里只有第三行需要工具,其余都是「理解」的活。 所以联调出问题时,工具通常不是嫌疑对象—— 先查 WSDL 版本、命名空间、鉴权头,这三样占故障的绝大多数。 这一节结束,第六章就完了。 下一章进 ONVIF 本身:它为什么会出现、Profile 是什么、 以及一台设备从「发现」到「出流」要走哪几步。

五、动手 Demo:用「生成出来的桩」走一遍完整调用

VS 那一套(新建工程、加四个生成文件、配包含目录)这里做不了,本机没有 VS。 但「生成的桩怎么被调用」这件事可以完整跑一遍。 我用 06.02.01 那套迷你 codegen 生成桩,再对着本机的迷你服务端发真请求。 所以这一节验证的是「桩的行为」,不是「VS 的配置」——配置部分只能照文档写,不能实测。

# 迷你 codegen 生成的桩,本质就是这三行
def soap_call_ns__GetDeviceInformation(soap, out):
    xml = (                       # ① 拼信封
        '<s:Envelope xmlns:s="%s"><s:Body>'
        '<tds:GetDeviceInformation/></s:Body></s:Envelope>') % NS_S
    r = _http_post(soap["port"], "/onvif/device_service",   # ② 发 POST
                   [("Content-Type",
                     "application/soap+xml; charset=utf-8; action=\"%s\"" % ACT)],
                   xml)
    root = ET.fromstring(r.split("\r\n\r\n", 1)[1])        # ③ 解响应
    body = root.find("{%s}Body" % NS_S)
    node = list(body)[0]
    if node.tag.endswith("Fault"):
        return -1
    for ch in node:
        out[ch.tag.split("}")[1]] = ch.text
    return 0                      # SOAP_OK

实跑输出:

--------------------------------------------------------------------
用「生成出来的桩」走一遍完整调用
--------------------------------------------------------------------
**VS 那一套(新建工程、加四个生成文件、配包含目录)这里做不了,本机没有 VS。**
  **但「生成的桩怎么被调用」这件事可以完整跑一遍**。
  **我用 D74 那套迷你 codegen 生成桩,再对着本机的迷你服务端发真请求。**

**桩函数做的三件事,就是客户端要做的全部:**

  步    动作                      对应代码
  --------------------------------------------------------------
  ①     拼 SOAP 信封              `<s:Envelope><s:Body>…`
  ②     发 HTTP POST              `POST /onvif/device_service`
  ③     解响应取值                `find("{…}Body")` 再往下走

  **gSOAP 的 `soap_call___ns1__GetDeviceInformation` 干的就是这三件事**,
  **只不过序列化是自动生成的,不用手拼字符串。**

**实际发出去的请求头:**

  POST /onvif/device_service HTTP/1.1
  Host: 127.0.0.1:49904
  Content-Type: application/soap+xml; charset=utf-8; action="http://www.on
  Content-Length: 191
  <?xml version="1.0"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap

  **注意 `Content-Type` 里那个 `action=`**:SOAP 1.2 用它代替了 1.1 的 `SOAPAction` 头。
  **很多老设备只认 `SOAPAction`,所以实际代码常常两个都带。**

**响应取到的字段:**

  字段                  值
  ------------------------------------------------
  Manufacturer          ACME
  Model                 IPC-2000
  FirmwareVersion       5.6.1

  **到这里,ONVIF 的一次调用已经走通了**:
  **WSDL 告诉你发什么,SOAP 把调用装进信封,HTTP 把它送出去。**
  **第六章到这里结束,下一章进 ONVIF 协议本身。**

注意桩函数返回的是 int(SOAP_OK 是 0),不是「成功/失败」的布尔值—— 这是照 gSOAP 的签名做的,用起来要写 if (ret != 0)。 第②步那行 Content-Type 里的 action= 是拼进去的, 这就是 SOAP 1.2 指定操作的方式(不是单独的 SOAPAction 头)。 实测请求头里能看到它,长度 191 字节。 第③步 list(body)[0] 取的是 Body 的第一个子元素, 在 SOAP 里它就是要调用的操作或它的响应——因为 Body 只允许一个子元素,这样取是合法的。 这一节里唯一「不是真的」的地方:这个桩是我手写的,不是 soapcpp2 生成的。 但它的三个动作和生成的桩完全一致,所以「怎么被调用」这个问题上的结论是可靠的。

30 秒实验

  1. 把桩里的 action= 删掉,看服务端还能不能找到操作(本机迷你服务端认元素名,会成功; 真设备会失败)。
  2. 在信封里加一个空的 Header,想想鉴权信息该放在哪(08.01.05 会填进去)。
  3. 把桩的返回值从 0 改成 1 表示失败,检查调用方是不是按 != 0 判断的。

常见误区

  1. 以为 VS 工程只要加两个生成文件。 还要加 stdsoap2.cpp, 它不在生成产物里,在 gSOAP 源码包里。
  2. 以为 Windows 上不用管链接库。 必须链 ws2_32.lib, 否则链接期报 __imp__socket 未定义。
  3. 以为 SOAP_OK 是 1。 它是 0,非 0 都是错误——和 POSIX 的习惯一致, 和「成功返回真」相反。
自测三问(点开对答案) 1. 一个 gSOAP 客户端桩做的三件事是什么? —— **拼 SOAP 信封、发 HTTP POST、解响应填结构体。** **和 06.01.05 手写的三步是同一件事。** 2. VS 工程里为什么必须加 `stdsoap2.cpp` 和 `ws2_32.lib`? —— `stdsoap2.cpp` 是 **gSOAP 运行时**(不在生成产物里); `ws2_32.lib` 是 **Winsock 库**,桩要靠它发 socket。 3. SOAP 1.2 用什么告诉服务端「调哪个操作」? —— **`Content-Type` 里的 `action=` 参数**,不是单独的 `SOAPAction` 头。 **但很多老设备只认头,所以实际代码常常两个都带。**

第六章完。下一章进 ONVIF 本身:它为什么出现、Profile 是什么、 以及一台设备从「被发现」到「出流」要走哪几步。

相关笔记