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 秒实验
- 把
Content-Length那一行删掉,重跑,看是不是卡住直到超时(_serve里设了 2 秒)。 - 把
\r\n换成\n,看迷你服务端还认不认(本机的_serve找的是\r\n\r\n,会直接卡住)。 - 把
Content-Length故意写成 5,体还是{},看服务端等多久。
常见误区
- 以为空行只是排版。 它是语法的一部分,
\r\n\r\n是头结束的唯一标志。 - 以为 HTTP 能自己知道体有多长。 TCP 是字节流,
必须靠
Content-Length或 chunked 告诉它。 - 以为
200 OK就是成功。 SOAP 的业务错误装在信封里的Fault,HTTP 状态码照样是 200。
自测三问(点开对答案)
1. HTTP 请求的四段是什么?哪一段可以被省略? —— 请求行、头、空行、体。 **体可以为空,但空行不能省**——哪怕是 `GET` 没有体,也要留那一行 `\r\n`。 2. 为什么 `Content-Length` 算错会「卡住」而不是「报错」? —— 因为服务端认为「还没收完」,**它会继续等剩下的字节**。 TCP 层没有「这一条消息结束了」的标记,**只能靠数字判断。** 3. ONVIF 设备回 `401` 说明什么? —— **身份没通过**。 要么没带 WS-Security 头,要么密码错,**不是地址错也不是网络不通**——那两种通常是超时或 404。下一节钻到体里面去:XML 和它的命名空间,SOAP 全靠它区分「谁家的 Body」。
- ☐ (本节完,下一节 06.01.03 XML简介)