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

HTTP 只有四段:请求行、头、空行、体。空行是硬分隔符,砍掉它服务端就一直等。 这一节用 socket 手写一条请求,把 SOAP 底下那层看清楚。

📝 本节和前面几节的关系

上一节看到 JSON-RPC 的请求里有 POST /rpc HTTP/1.1 这样的行,那就是 HTTP 的请求行。 SOAP 是「HTTP 的体」,所以 HTTP 的头写错了,体再对也没用。

一、四段结构,一个空行分开

HTTP 是纯文本协议,一条请求就是几行 ASCII,没有任何二进制头部。

段 长什么样 作用
请求行 POST /onvif/device_service HTTP/1.1 方法 + 路径 + 版本
头 Content-Type: application/soap+xml 逐行的 键: 值
空行 \r\n 硬分隔符,告诉对方「头完了」
体 SOAP 信封原文 长度由 Content-Length 指定

第三行是全篇最容易忽略的一行。 它不是「排版用的空行」,是语法的一部分——少了它,服务端会把后面的体当成头继续读, 一直读到超时。 行尾必须是 \r\n(CRLF),不是 \n。 很多服务端宽容到能认 \n,但标准要求 CRLF,自己拼报文时别省。

二、为什么非要有 Content-Length

TCP 是字节流,没有「包边界」。发 191 个字节,对端可能收到 100 + 91, 也可能等下一次一起收到 200。 所以 HTTP 必须自己告诉对方「体有多长」,两种办法:

办法 写法 适用
定长 Content-Length: 191 体在发之前就知道多大
分块 Transfer-Encoding: chunked 体是边算边发的

SOAP 请求体是先在内存里拼好的,所以永远用第一种。 这也是为什么 SOAP 报文的大小在发送前就能算出来——必须能算,不然没法定 Content-Length。 反过来,如果服务端返回的是 chunked,客户端就得一直读到最后一块空块, 不能靠「读一次」判断结束。

三、SOAP 要用的三个头

头 作用 写错的后果
Content-Type 体是什么格式 SOAP 1.2 要 application/soap+xml
Content-Length 体有多少字节 少了服务端一直等,直到超时
SOAPAction 调哪个操作(1.1 的写法) 服务端不知道调谁

第二行是新手最常见的坑:很多人用高级库习惯了,一旦换成手拼报文就忘了算长度,然后卡死。 第三行是版本差异:SOAP 1.1 用 SOAPAction 头, SOAP 1.2 把它挪进了 Content-Type 的 action 参数里(Demo 78 的实测报文能看到)。 ONVIF 规范用的是 SOAP 1.2,但很多设备两个都认, 所以实际代码常常两个都带——带了不亏,少一个可能就 400。

四、状态码:哪些在监控里真会碰到

| 码 | 含义 | 在 ONVIF 里意味着什么 | |---|---|---|---| | 200 | 成功 | 注意:报文里可能藏着 Fault(06.01.05) | | 400 | 请求格式错 | 头写错、XML 不合法 | | 401 | 未认证 | 没带 WS-Security 头,或密码错 | | 403 | 禁止 | 账号权限不够(比如只读账号要 PTZ) | | 404 | 路径不对 | 服务地址写错了 | | 500 | 服务端内部错 | 设备固件的问题,不是你的错 |

第一行的提醒是本课反复强调的:200 OK 不等于调用成功。 SOAP 把业务错误装在信封里返回,HTTP 层照样 200。 第三行是排查现场最常遇到的:401 就是没做鉴权,不是地址错、不是网络不通。 看到 401 先去查 WS-Security 头 (08.01.05 讲怎么加)。

五、动手 Demo:手写一条 HTTP 请求,看它的骨架

不用 requests、不用 http.client,直接 socket.sendall 一段字符串。 这样能看见协议本身长什么样——平时被库藏起来的部分,全在这里。

req = ["POST /onvif/device_service HTTP/1.1",
       "Host: 127.0.0.1:%d" % port,
       "Content-Type: application/soap+xml; charset=utf-8",
       "SOAPAction: http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation",
       "Content-Length: 2",
       "",          # 这一行就是那个「硬分隔符」
       "{}"]
s = socket.create_connection(("127.0.0.1", port), timeout=3)
s.sendall(("\r\n".join(req)).encode("utf-8"))

实跑输出:

--------------------------------------------------------------------
手写一条 HTTP 请求,看它的骨架
--------------------------------------------------------------------
**SOAP 跑在 HTTP 上,所以 HTTP 不清楚,SOAP 一定学不明白。**
  这一节不用任何库,直接用 socket 发一条 HTTP/1.1 请求。

**发出去的请求(原样打印):**

  POST /onvif/device_service HTTP/1.1
  Host: 127.0.0.1:49898
  Content-Type: application/soap+xml; charset=utf-8
  SOAPAction: http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation
  Content-Length: 2
  {}

  **四段结构:请求行、头、空行、体。**
  **空行是硬分隔符,少了它服务端会一直等。**

**收到的响应:**

  HTTP/1.1 200 OK
  Content-Type: text/html
  Content-Length: 28
  <html><body>ok</body></html>

  **响应的第一行是「版本 + 状态码 + 原因」,之后同样是头、空行、体。**

**几个在 SOAP 场景里必须记牢的头:**

  头                    作用                                写错的后果
  ----------------------------------------------------------------------------------------------
  `Content-Type`        体是什么格式                        SOAP 1.2 要 `application/soap+xml`
  `Content-Length`      体有多少字节                        少了服务端一直等,直到超时
  `SOAPAction`          调哪个操作(1.1)                   服务端不知道调谁

  **第二行是新手最容易踩的**:HTTP 不像 TCP 有「包边界」,
  **它靠 `Content-Length` 或 `chunked` 判断体有多长。**
  **不给 `Content-Length`,服务端就不知道读完了没有**,只能等超时。

  **第三行是 SOAP 1.1 的遗留**:SOAP 1.1 用 `SOAPAction` 头指定操作,
  **SOAP 1.2 把它挪进了 `Content-Type` 的 `action` 参数里。**
  **ONVIF 用的是 1.2,但很多设备两个都认**——所以发请求时常常两个都带。

注意体里只有 {} 两个字节,Content-Length: 2 正好对上。 这条请求在语法上完全合法——服务端确实回了 200 OK。 但它不是一条合法的 SOAP 请求:{} 不是 XML,真正的 ONVIF 设备会回 400 或 Fault。 这一节只验证 HTTP 层,SOAP 层留到 06.01.05。 响应里没看到 Connection 头,那是迷你服务端没写; 真实的 HTTP/1.1 服务端默认 keep-alive,一条连接可以连发多条请求。

30 秒实验

  1. 把 Content-Length 那一行删掉,重跑,看是不是卡住直到超时(_serve 里设了 2 秒)。
  2. 把 \r\n 换成 \n,看迷你服务端还认不认(本机的 _serve 找的是 \r\n\r\n,会直接卡住)。
  3. 把 Content-Length 故意写成 5,体还是 {},看服务端等多久。

常见误区

  1. 以为空行只是排版。 它是语法的一部分,\r\n\r\n 是头结束的唯一标志。
  2. 以为 HTTP 能自己知道体有多长。 TCP 是字节流, 必须靠 Content-Length 或 chunked 告诉它。
  3. 以为 200 OK 就是成功。 SOAP 的业务错误装在信封里的 Fault,HTTP 状态码照样是 200。
自测三问(点开对答案) 1. HTTP 请求的四段是什么?哪一段可以被省略? —— 请求行、头、空行、体。 **体可以为空,但空行不能省**——哪怕是 `GET` 没有体,也要留那一行 `\r\n`。 2. 为什么 `Content-Length` 算错会「卡住」而不是「报错」? —— 因为服务端认为「还没收完」,**它会继续等剩下的字节**。 TCP 层没有「这一条消息结束了」的标记,**只能靠数字判断。** 3. ONVIF 设备回 `401` 说明什么? —— **身份没通过**。 要么没带 WS-Security 头,要么密码错,**不是地址错也不是网络不通**——那两种通常是超时或 404。

下一节钻到体里面去:XML 和它的命名空间,SOAP 全靠它区分「谁家的 Body」。

相关笔记