四条请求、四个响应,加起来 477 字节,这就是一次完整的握手。 PLAY 的 URI 会变短,因为服务器给了 Content-Base。
前三节分别讲了协议、服务器、客户端,这一节把一次会话完完整整走一遍。 看的是「顺序、字节、时间」三件事,这三样决定了排查时该看哪里。
一、四问四答的字节账
一次拉流,客户端一共只发四条请求:
序 方法 字节 距上一条
1 OPTIONS 95 19.5 毫秒 2 DESCRIBE 121 0.3 毫秒 3 SETUP 141 1.1 毫秒 4 PLAY 120 0.7 毫秒
四条加起来 477 字节。 第一条那 19.5 毫秒里其实包含了 TCP 建连接的时间,后面三条都在毫秒级。
SETUP 最长(141 字节),因为它要带一整行 Transport。 这也说明:字节数基本跟着「信息量」走,越复杂的协商越长。
注意「距上一条」这一列,全部是亚毫秒到毫秒级。 所以协商本身不慢,慢的是后面的媒体与缓冲(8.1.3 的首帧 999 毫秒)。
二、PLAY 为什么换了 URI
四条请求的 URI 前三条一样,最后一条短一截:
请求 URI
OPTIONS rtsp://127.0.0.1:端口/live/streamid=0 DESCRIBE rtsp://127.0.0.1:端口/live/streamid=0 SETUP rtsp://127.0.0.1:端口/live/streamid=0 PLAY rtsp://127.0.0.1:端口/live/
不是客户端写错了,是服务器在 DESCRIBE 响应里给了 Content-Base: rtsp://…/live/。
规矩是:相对地址接到 Content-Base 后面。
所以 a=control:streamid=0 拼出来是 …/live/streamid=0,
而 PLAY 针对整个会话,只到 …/live/ 这一层。
这条规矩有实际后果:服务器换了路径,客户端不用改代码, 因为它每次都从 Content-Base 重新拼。 反过来说,Content-Base 写错一个斜杠,后面所有请求都会打在错的地方。
三、CSeq 与 Session
两个头分工明确:
头 谁生成 干什么
CSeq 客户端 请求与响应配对 Session 服务器 认出是哪个会话
CSeq 从 1 开始,一条加一,响应里必须原样抄回来。 RTSP 允许同时有好几条请求在飞,靠 CSeq 才不会把响应配错。
Session 是服务器编的,客户端只负责带。 它还有一个隐藏作用:超时计时。长时间不发请求,会话就过期了, 所以才需要 GET_PARAMETER 定期续命。
还有一个细节:每条请求都带 User-Agent: Lavf62.12.102。
Lavf 是 libavformat 的缩写,服务器日志里看到它,就知道对面是 ffmpeg 系。
四、一次完整会话的时序
把四个阶段的时间与字节放在一条时间轴上,就是这样:
时刻 发生什么 关键字段
0 TCP 连上 554 没有 约 20 毫秒 OPTIONS → 200 Public 列表 约 20 毫秒 DESCRIBE → 200 SDP、Content-Base 约 21 毫秒 SETUP → 200 Transport、Session 约 22 毫秒 PLAY → 200 Range: npt=0- 之后 RTP 开始流 序号、时间戳
注意最后一行:RTP 是在 PLAY 的响应之后才开始流的。 所以排查「没有画面」时,先确认 PLAY 有没有回 200,再看 RTP 有没有来。
Range: npt=0.000- 是「从现在开始播」的意思。
npt 是 normal play time,单位是秒;写 npt=10- 就是从那一段的第十秒开始。
直播流一般不接受 npt 回退——过去的画面已经没有了。 这也是为什么直播的进度条拖不动(8.3.3 还会再说一次这件事)。
五、动手 Demo:抓四条请求,量字节与间隔
下面这段代码起服务器、让 libav 来连,把收到的每条请求连字节数与到达时刻一起记下来。 最后把四条请求的 URI 排在一起,PLAY 那一行的差别就露出来了。
import socket, threading, time
BUF = []
def serve(sdp, port):
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", port))
srv.listen(1)
c, _ = srv.accept()
buf = b""
t0 = time.time()
while True:
d = c.recv(4096)
if not d:
break
buf += d
while b"\r\n\r\n" in buf:
head, buf = buf.split(b"\r\n\r\n", 1)
txt = head.decode("utf-8", "replace")
first = txt.split("\r\n")[0]
BUF.append((first.split(" ")[0], first, len(txt) + 4,
(time.time() - t0) * 1000))
# 这里按方法回 200,代码见生成器完整版
c.close()
for m, first, n, ms in BUF:
print(u"%-9s %4d 字节 %6.1f ms %s" % (m, n, ms, first.split(" ")[1]))
实跑输出:
--------------------------------------------------------------------
8.1.4 RTSP 交互流程分析
--------------------------------------------------------------------
**① 四问四答,一条不多一条不少**
序 方法 字节 距上一条
-----------------------------------------
1 OPTIONS 95 0.0 ms
2 DESCRIBE 121 0.3 ms
3 SETUP 141 0.6 ms
4 PLAY 120 0.6 ms
**四条请求加起来 477 字节。**
**② 第一条与最后一条的 URI 不一样**
请求 URI
-------------------------------------------------------
OPTIONS rtsp://127.0.0.1:57432/live/streamid=0
DESCRIBE rtsp://127.0.0.1:57432/live/streamid=0
SETUP rtsp://127.0.0.1:57432/live/streamid=0
PLAY rtsp://127.0.0.1:57432/live/
**前三条都对着 `…/live/streamid=0`,PLAY 却对着 `…/live/`。**
**不是客户端乱写,是服务器在 DESCRIBE 的响应里给了 `Content-Base`。**
**规矩是:URL 以 `/` 结尾时,相对地址接在后面。**
**所以 `a=control:streamid=0` 拼到 `…/live/` 后面才是完整地址。**
**PLAY 是针对整个会话,所以只到目录那一层。**
**③ 每条请求都带 CSeq,用来配对响应**
**CSeq 从 1 开始,一条加一。响应里必须把它原样抄回来。**
**RTSP 允许同时在飞多条请求,靠 CSeq 才不会把响应配错。**
**④ 每条请求都带 User-Agent: Lavf62.12.102**
**`Lavf` 就是 libavformat 的缩写,后面是版本号。**
**所以服务器日志里看到 Lavf,就知道对面是 ffmpeg 系(ffplay 也一样)。**
四条请求的字节数是 95/121/141/120,加起来 477。 这个数看着不起眼,但它就是「一次握手的全部成本」。
时间那一列全在毫秒级,而且第一条明显最长—— 因为它前面还有一次 TCP 三次握手,那才是真正的固定成本。
最后打出来的 URI 会看到:前三条对着 streamid=0,PLAY 只到 live/。
这个差别是服务器用 Content-Base 定的,不是客户端乱写。
30 秒实验
- 改 CSeq 起点:让服务器回错 CSeq,看客户端多久才放弃。
- 去掉 Session 头:服务器不认会话,客户端会重发 SETUP。
- 把 Content-Base 指向别的路径:观察四条请求的 URI 怎么跟着变。
常见误区
- 以为 PLAY 的 URI 是客户端写错。是 Content-Base 决定的。
- 把 CSeq 当成序号无关紧要。它错了,响应就配不上。
- 以为直播也能 npt 回退。过去的数据已经没了,服务器只能拒绝。
自测三问(点开对答案)
1. 一次握手一共几条请求、多少字节? —— 四条:OPTIONS、DESCRIBE、SETUP、PLAY,合计 477 字节。 2. PLAY 的 URI 为什么比前三条短? —— 它针对整个会话,只到 Content-Base 那一层,前三条要指到具体那一路流。 3. RTP 从什么时候开始流? —— PLAY 回 200 之后。所以没画面要先查 PLAY。- ☐ (本节完,下一节 8.1.5 VLC 使用摄像头模拟 RTSP 直播流)