🎬 第二章 RTSP流媒体协议
📄 一句话概括

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 秒实验

  1. 把 PLAY 那一条的 Session 头删掉,看它是不是回 454 Session Not Found。
  2. 把 SID 改成别的值再发 PLAY,验证服务器比对的是「值」而不是「有没有」。
  3. 把四个请求分别用四条独立连接发(每条 new 一个 socket),看还能不能播——这就是「连接」和「会话」的区别。

常见误区

  1. 「RTSP 有状态,所以 TCP 连接不能断」——连接断了会话还在,重连后带着原 Session 号还能接着播。状态存在服务器内存里,不绑定在 socket 上。
  2. 「554 端口传视频」——554 上只有文本,视频走 SETUP 协商出来的另一对端口。用抓包工具只看 554 会以为没数据。
  3. 「RTSP 延迟低所以画质好」——延迟和画质没关系。RTSP 延迟低是因为不切片、不缓冲,画质取决于码率。
自测三问(点开对答案) 1. 三个特点分别对应代码里的什么结构? —— **有状态 → 客户端要存 Session 号;可控制 → 客户端要维护状态机;命令数据分家 → 客户端要单独开端口收 RTP。** 2. 为什么 RTSP 穿 NAT 比 HTTP 麻烦? —— **因为它要动态开一对 UDP 端口。**NAT 后面的端口映射要额外配置,而 HTTP 一个 80 端口就够了。 3. 把 RTSP 换成 HLS,丢掉的到底是哪个特点? —— **可控制。**HLS 是一堆静态文件,服务器没法「暂停给你看」——你暂停只是播放器不往下取切片了。

相关笔记