RTSP 的请求报文只有三行骨架:请求行、一堆头域、一个空行。 真正需要背下来的只有 CSeq——它是唯一一个错了就整条会话串掉的字段。
2.1.2 讲了 RTSP 有状态,这一节开始看它长什么样。 报文格式是后面所有 Demo 的地基,抓包时认的就是这几行。
一、三行骨架
把一条请求摊平,结构简单到不像话:
① 请求行:方法 URL 版本,三个字段用空格分开,以 \r\n 结尾。
② 头域:名字: 值,一行一个,顺序无所谓。
③ 空行:一个孤零零的 \r\n,标记头域结束。
请求没有「体」。 这是 RTSP 和 HTTP 的一个区别:HTTP 的 POST 后面跟着一堆数据,RTSP 的请求几乎永远是纯文本头域,一个字节的媒体数据都不带。
唯一的例外是 ANNOUNCE——推流时要把 SDP 正文附在请求后面,那时才会出现 Content-Length 和真正的体。但那是推流场景,拉流用不到。
二、请求行三个字段
SETUP rtsp://127.0.0.1/live/test RTSP/1.0——三个字段各有讲究:
① 方法:大写,必须是服务器 Public 头里列出来的那些。写个小写的 play 会被当成未知方法回 405。
② URL:完整地址,含 rtsp://、主机、端口(可省略)、路径。注意 SETUP 时 URL 后面要带 trackID,因为一路流可能有视频和音频两条轨道。
③ 版本:固定 RTSP/1.0。目前实际部署的全是 1.0,虽然标准里有 2.0,但基本没人用。
URL 里那个 trackID 是最容易漏的。
DESCRIBE 拿回来的 SDP 里每条 m= 行都有一个 a=control: 属性,它的值就是这一路的 trackID。
SETUP 时必须把它拼到 URL 后面,服务器才知道你要建哪一路的通道。
三、头域哪些必填
头域十几个,但真正必填的只有两三个:
① CSeq:所有请求必填。序号,通常从 1 开始递增。
② Transport:只有 SETUP 必填。它说清「用什么协议传、我在哪个端口收」,格式如 RTP/AVP;unicast;client_port=5000-5001。
③ Session:SETUP 之后的请求必填。PLAY、PAUSE、TEARDOWN 都得带上,否则 454。
选填的两三个也有用:
User-Agent:客户端标识。有些服务器会按它做差异化处理,比如给 VLC 和给摄像头返回不同的 SDP。
Range:播哪一段。npt=0- 表示从头播到尾,npt=10-20 表示只播第 10 到 20 秒。回放功能就靠它。
Accept:能接受什么格式的 SDP 描述。用得不多,默认就好。
四、CSeq 为什么最要命
因为 RTSP 允许「流水线」:客户端可以不等响应,连着发好几条请求。 这时候响应回来的顺序可能是乱的,唯一的配对依据就是 CSeq。
规则只有一条:响应里的 CSeq 必须等于请求里的 CSeq。
服务器照抄一遍,客户端拿它去查「这条响应是回哪条请求的」。
错了会怎样? 轻则客户端把 PLAY 的响应当成 SETUP 的,以为没拿到 Session,于是一直在重发; 重则整条会话的状态全乱,后面每一步都基于错误的假设。
实践中的建议: ① 单调递增,不要复用(有些实现会拒绝重复的 CSeq)。 ② 每条请求都检查响应里的 CSeq,对不上就丢掉,别往下走。 ③ 不要用 0——很多库把 0 当「未设置」。
五、动手 Demo:手写一条 SETUP,再逐字段拆开
不连服务器,先把一条真实的 SETUP 请求写成字符串,再按 \r\n 切开逐行看。
这比抓包更直观:你能看到每一个字段到底在哪个位置。
req = ("SETUP rtsp://127.0.0.1/live/test/trackID=0 RTSP/1.0\r\n"
"CSeq: 3\r\n"
"Transport: RTP/AVP;unicast;client_port=5000-5001\r\n"
"User-Agent: ch30-demo\r\n"
"\r\n")
print("整条请求 %d 字节" % len(req.encode()))
print(repr(req))
line, rest = req.split("\r\n", 1)
meth, url, ver = line.split(" ")
print("方法=%s 版本=%s" % (meth, ver))
print("主机+路径 = %s" % url[len("rtsp://"):])
for h in rest.split("\r\n"):
if not h:
print("-- 空行,头域结束 --")
continue
k, v = h.split(": ", 1)
print(" %-10s | %s" % (k, v))
# Transport 里的两个端口
cp = "5000-5001"
a, b = cp.split("-")
print("RTP 收在 %s(偶),RTCP 收在 %s(奇)" % (a, b))
实跑输出:
--------------------------------------------------------------------
请求消息——RTSP 的报文只有三行骨架
--------------------------------------------------------------------
① 一条完整的 SETUP 请求
| SETUP rtsp://127.0.0.1/live/test RTSP/1.0
| CSeq: 3
| Transport: RTP/AVP;unicast;client_port=5000-5001
| User-Agent: ch30-demo
② 拆开看
部分 内容 意思
-----------------------------------------------------------------------
请求行 SETUP rtsp://127.0.0.1/live/ 方法 空格 URL 空格 版本
方法 SETUP 告诉服务器要干什么
URL 127.0.0.1/live/test 对哪个资源做
版本 RTSP/1.0 固定 RTSP/1.0
③ 每个头域都在解决一个具体问题
头域 必填 作用
---------------------------------------------------------------------
CSeq 是 请求序号,响应必须原样带回来
Transport SETUP 必填 用什么协议传、客户端收在哪个端口
User-Agent 否 客户端标识,服务器可能据此差别对待
Session PLAY 起必填 认领之前 SETUP 拿到的会话号
Range 否 播哪一段,npt=0- 表示从头
**CSeq 是最容易被忽略又最要命的一个:**
**响应的 CSeq 必须等于请求的 CSeq,**
**客户端就是靠它把响应和请求配对。乱了这个,整条会话就串了。**
整条请求只有一百多字节,比一个 TCP 握手包大不了多少。这就是「遥控器」的体量。
Transport 里那两个端口是一偶一奇、连号的,不是随便两个数。RTP 用偶数,RTCP 用下一个奇数。
URL 末尾的 trackID=0 是拼上去的,不是服务器给的完整地址——它来自 SDP 里的 a=control:。
30 秒实验
- 把
CSeq改成0再跑,看打印出来的字段有没有变——报文本身不会报错,但有些服务器会拒收。 - 把
Transport换成RTP/AVP/TCP;interleaved=0-1再跑,体会一下「RTP over TCP」只是换了个描述,请求格式完全没变。 - 给请求加一行
Range: npt=10-20,这就是回放第 10 到 20 秒的写法。
常见误区
- 「请求里要带媒体数据」——拉流的请求永远不带数据,纯文本头域。数据从 RTP 通道进来。
- 「URL 就是 rtsp://host/path」——SETUP 时必须带 trackID,否则服务器不知道你要建哪条轨道。多轨(音视频分开)时更是必须。
- 「CSeq 随便填」——服务器会原样带回、客户端靠它配对。流水线场景下填错就是整条会话错位。
自测三问(点开对答案)
1. 一条请求的三部分是什么? —— **请求行、头域、空行。**没有体(ANNOUNCE 除外)。 2. `Transport: RTP/AVP;unicast;client_port=5000-5001` 说明什么? —— **用 RTP over UDP、单播,客户端在 5000 收 RTP、5001 收 RTCP。**偶奇成对是硬规矩。 3. 哪个头域错了会让整条会话串掉? —— **`CSeq`。**它是响应与请求之间唯一的配对依据。- ☐ (本节完,下一节 2.2.2 应答消息)