一个 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 秒实验
- 把桩里的
action=删掉,看服务端还能不能找到操作(本机迷你服务端认元素名,会成功; 真设备会失败)。 - 在信封里加一个空的
Header,想想鉴权信息该放在哪(08.01.05 会填进去)。 - 把桩的返回值从
0改成1表示失败,检查调用方是不是按!= 0判断的。
常见误区
- 以为 VS 工程只要加两个生成文件。 还要加
stdsoap2.cpp, 它不在生成产物里,在 gSOAP 源码包里。 - 以为 Windows 上不用管链接库。 必须链
ws2_32.lib, 否则链接期报__imp__socket未定义。 - 以为
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 是什么、 以及一台设备从「被发现」到「出流」要走哪几步。
- ☐ (本章完,下一节 07.01.01 ONVIF背景简介)