RTSP 的三个特点可以一句话概括:有状态、可控制、命令与数据分家。 这三条每一条都直接决定了写客户端时的代码结构。
2.1.1 说了 RTSP 是遥控器,这一节说这个遥控器跟 HTTP 到底差在哪。 后面 2.3 的交互流程、2.6 的状态机,都是这三个特点推出来的。
一、特点一:有状态
HTTP 是无状态的:每个请求自带全部信息,服务器不记得你是谁。 RTSP 反过来——服务器记着每一条会话。
证据就是 Session 头。
SETUP 之后服务器发一个 Session 号,后面每条 PLAY、PAUSE、TEARDOWN 都必须带上它。
不带或者带错,服务器就回 454 Session Not Found。
「有状态」带来两个直接后果: ① 服务器要维护一张会话表——每个 Session 对应哪条连接、哪个客户端端口、当前是播还是停。 ② 会话会超时——客户端崩了不会通知服务器,所以服务器只能靠「多久没收到请求」来判断死活,通常 60 秒。
这也解释了为什么 RTSP 客户端必须发心跳。 不发心跳,播着播着就会被服务器单方面挂断,而客户端那边看起来像是「画面突然卡住」。
二、特点二:可控制
HTTP 只有一个「取」的动作:GET 一次,拿完就结束。 没有「停在半路」「退回去一点」「换个清晰度再来」这些说法。
RTSP 有。 PLAY 开始,PAUSE 停在原地,再 PLAY 从这里接着走,TEARDOWN 拆掉——这四个动作可以任意来回。
这才是 RTSP 存在的理由。 它诞生在监控场景:要暂停看细节、要拖动时间轴回放、要切换多路画面。这些需求 HTTP 一个都满足不了。
代价是复杂度。 客户端要自己维护状态机(2.6.1 讲),要知道「现在能发哪个方法」;服务器要维护会话表、要处理超时、要管端口回收。 一行 RTSP 客户端代码,顶得上五行 HTTP 客户端。
三、特点三:命令与数据分家
这一条最容易踩坑:RTSP 的 554 端口上永远只有文本命令,一个字节的画面都没有。
画面走哪?走 SETUP 时协商出来的那对端口。
客户端在 Transport 头里说「我在 5000-5001 收」,服务器就记住这对端口,PLAY 之后往那里发 RTP。
所以一次会话实际占用四个端口: 客户端两个:5000 收 RTP(偶数)、5001 收 RTCP(奇数)。 服务器两个:由服务器在 SETUP 的响应里告知,通常也是连号的一偶一奇。
端口成对、偶奇相邻,这是 RTP 的硬规矩。 很多防火墙配置出错,就是把这一对当成了一个端口。
顺便说清一件事:所谓「RTSP 端口」只有 554 是固定的,数据端口每次会话都可能不同。 这也是 RTSP 穿 NAT 麻烦的根源——不像 HTTP 一个 80 端口走天下。
四、和 HTTP 的逐项对比
把差别列成一张表,比记三条特点更省事:
① 状态:RTSP 服务器记住会话;HTTP 1.0 每个请求都独立。 ② 连接:RTSP 一条长连接走很多请求;HTTP 1.0 默认一请求一连接。 ③ 端口:RTSP 命令 554、数据动态;HTTP 只有 80/443。 ④ 动作:RTSP 有 PLAY/PAUSE;HTTP 只有 GET/POST。 ⑤ 缓存:RTSP 不能缓存;HTTP 天生适合 CDN。
第 ⑤ 条是致命的。 正因为 RTSP 有状态、不能缓存,所以观众一多,服务器就顶不住——每一路都要单独维持一条会话、单独发一份数据。
这就是后来的 HLS、HTTP-FLV 存在的理由:把「有状态」换成「可缓存」,用 CDN 摊掉成本。 而代价正好是丢掉可控制性——你现在没法在一个 HLS 直播里「暂停服务器发流」了。
五、动手 Demo:一条连接连发四条,再看它和 HTTP 的差别
同一台服务器、同一条 TCP 连接,连发 OPTIONS、DESCRIBE、SETUP、PLAY。 如果服务器真的「有状态」,它就该在 PLAY 时认得前一步分配的 Session。
import socket, threading
SDP = "v=0\r\ns=Live\r\nm=video 0 RTP/AVP 96\r\n"
SID = "1A2B3C4D"
N = [0]
def handle(txt, sess):
head = txt.split("\r\n")
meth = head[0].split(" ")[0]
hd = dict(l.split(": ", 1) for l in head[1:] if ": " in l)
cseq = hd.get("CSeq", "0")
if meth == "DESCRIBE":
return ("RTSP/1.0 200 OK\r\nCSeq: %s\r\n"
"Content-Length: %d\r\n\r\n%s" % (cseq, len(SDP), SDP))
if meth == "SETUP":
sess[0] = SID
return "RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n\r\n" % (cseq, SID)
if meth == "PLAY":
ok = "200 OK" if hd.get("Session") == sess[0] else "454 Session Not Found"
return "RTSP/1.0 %s\r\nCSeq: %s\r\n\r\n" % (ok, cseq)
return "RTSP/1.0 200 OK\r\nCSeq: %s\r\n\r\n" % cseq
def serve(sock):
c, _ = sock.accept()
sess = [None]
buf = b""
while True:
d = c.recv(8192)
if not d:
return
buf += d
while b"\r\n\r\n" in buf:
req, buf = buf.split(b"\r\n\r\n", 1)
c.sendall(handle(req.decode(), sess).encode())
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(4)
port = srv.getsockname()[1]
threading.Thread(target=serve, args=(srv,), daemon=True).start()
s = socket.create_connection(("127.0.0.1", port), 3.0)
for i, meth in enumerate(("OPTIONS", "DESCRIBE", "SETUP", "PLAY"), 1):
extra = "Session: %s\r\n" % SID if meth == "PLAY" else ""
s.sendall(("%s rtsp://127.0.0.1/live/test RTSP/1.0\r\n"
"CSeq: %d\r\n%s\r\n" % (meth, i, extra)).encode())
r = s.recv(8192).decode()
print("%d %-9s %s" % (i, meth, r.split("\r\n")[0]))
s.close()
实跑输出:
--------------------------------------------------------------------
RTSP 的三个特点——有状态、可控制、另一套端口
--------------------------------------------------------------------
① 一条连接连发四条,服务器全程记得你是谁
次序 请求 响应 耗时
-------------------------------------------------------
1 OPTIONS RTSP/1.0 200 OK 0.32 毫秒
2 DESCRIBE RTSP/1.0 200 OK 0.09 毫秒
3 SETUP RTSP/1.0 200 OK 0.05 毫秒
4 PLAY RTSP/1.0 200 OK 0.05 毫秒
**四个请求都走同一条 TCP 连接——服务器靠 Session 头记住上下文。**
**这就是「有状态」。**
② 和 HTTP 的「无状态」对比
RTSP HTTP(1.0)
--------------------------------------------------------
状态 服务器记住会话 每个请求都独立
连接 一条长连接走很多请求 默认一个请求一条
端口 默认 554(数据走 UDP) 80/443
动作 有 PLAY/PAUSE 只有 GET/POST
③ 一个常被忽略的点:RTSP 只发命令,不发画面
**RTSP 是「遥控器」,真正的画面走 RTP(第 3 章)。**
**所以 RTSP 的报文都很小,几十到几百字节。**
**也正因为如此,RTSP 通常跑在 TCP 上,而 RTP 跑在 UDP 上——一个要可靠,一个要快。**
四个请求都走同一条 TCP 连接,没有重连、没有重新握手。这就是「长连接」。
PLAY 之所以能成功,是因为服务器在 SETUP 那一步把 Session 存进了 sess 变量——这个变量就是「状态」。
如果把 PLAY 的 Session 拿掉,服务器立刻回 454。这就是「有状态」最直白的证明。
30 秒实验
- 把 PLAY 那一条的
Session头删掉,看它是不是回 454 Session Not Found。 - 把
SID改成别的值再发 PLAY,验证服务器比对的是「值」而不是「有没有」。 - 把四个请求分别用四条独立连接发(每条 new 一个 socket),看还能不能播——这就是「连接」和「会话」的区别。
常见误区
- 「RTSP 有状态,所以 TCP 连接不能断」——连接断了会话还在,重连后带着原 Session 号还能接着播。状态存在服务器内存里,不绑定在 socket 上。
- 「554 端口传视频」——554 上只有文本,视频走 SETUP 协商出来的另一对端口。用抓包工具只看 554 会以为没数据。
- 「RTSP 延迟低所以画质好」——延迟和画质没关系。RTSP 延迟低是因为不切片、不缓冲,画质取决于码率。
自测三问(点开对答案)
1. 三个特点分别对应代码里的什么结构? —— **有状态 → 客户端要存 Session 号;可控制 → 客户端要维护状态机;命令数据分家 → 客户端要单独开端口收 RTP。** 2. 为什么 RTSP 穿 NAT 比 HTTP 麻烦? —— **因为它要动态开一对 UDP 端口。**NAT 后面的端口映射要额外配置,而 HTTP 一个 80 端口就够了。 3. 把 RTSP 换成 HLS,丢掉的到底是哪个特点? —— **可控制。**HLS 是一堆静态文件,服务器没法「暂停给你看」——你暂停只是播放器不往下取切片了。- ☐ (本节完,下一节 2.2.1 请求消息)