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

RTSP 和 HTTP 都是文本协议、都靠请求响应,但它们的分岔点在「记不记得你」和「能不能缓存」。 记住会话的代价是不可缓存,不可缓存就上不了 CDN——这才是直播协议演进的真正主线。

📝 本节和前面几节的关系

2.8 用抓包把 RTSP 看透了,这一节把它和 HTTP 放在一起比。 后面第 5、6 章的 HLS 和 HTTP-FLV,本质就是「用 HTTP 重做一遍直播」。

一、表面上很像,其实只有四处相同

RTSP 在设计时大量照抄了 HTTP,所以乍看几乎一样:

① 都是纯文本:请求行加头域,一行一条,\r\n 分隔。 ② 都是请求—响应:客户端发一条,服务器回一条。 ③ 都有状态码:三位数字,200、404、500 这些编号体系直接借用。 ④ 都有 URL:rtsp:// 和 http:// 的结构完全相同。

但只有这四处。 再往下走一步就全不一样了——HTTP 是无状态的「取文件」,RTSP 是有状态的「操作一台设备」。 这个区别不是风格差异,是设计目标不同:一个为了「让全世界都能缓存」,一个为了「让一个人能暂停」。

二、分岔点一:记不记得你

HTTP 的每个请求都自带全部信息:我要哪个 URL、我接受什么编码、我带什么 Cookie。 服务器处理完就忘,下一个请求来了重新认。

RTSP 反过来:SETUP 之后服务器给你一个 Session,之后每条请求都要带。 服务器靠它在几百条会话里找到「你这条」。

这个差别带来连锁反应: ① 服务器要存状态 → 内存占用随并发线性增长,十万并发就是一千万级的会话对象。 ② 服务器不能随便重启 → 一重启全部会话作废,所有客户端要重连。HTTP 服务器可以随便重启,因为它是无状态的。 ③ 负载均衡变难 → 同一个会话的请求必须落到同一台机器上,要么做会话粘滞,要么把会话存到共享存储。

第 ③ 条是致命的。 HTTP 只要在 DNS 上轮询就能水平扩容,RTSP 必须保证「同一会话同一台机器」,扩容就没那么自由了。

三、分岔点二:能不能缓存

HTTP 天生可缓存:一个 GET 的结果,谁拿到都一样,所以中间可以插无数层缓存,这就是 CDN。

RTSP 不可缓存,原因有三个: ① 内容是「正在产生的」,不是文件。缓存没法预先取。 ② 每个客户端的状态不同(播到第几秒、暂停还是播),同一份响应不能给第二个人用。 ③ 传输走 UDP 的 RTP,CDN 的缓存节点压根不碰 UDP。

所以 RTSP 的扩展方式是「加服务器 + 做级联」,而 HTTP 的扩展方式是「加缓存节点」。 前者成本高一个数量级:每个观众都要占一份连接和一份带宽,后者一个缓存节点能服务几千人。

这就是为什么 RTSP 在监控领域活得好好的(几十路,自己能扛),却在互联网直播上彻底输给 HTTP 派(百万级观众)。

四、历史是这么走的

把时间线拉出来看,演进逻辑非常清楚:

① 1990 年代中后期,RTSP 出现。 因为它要「控制」——监控要暂停、要回放、要拖时间轴。 HTTP 那时候连「取一段」都做不到,更别说暂停。 ② 2000 年代,互联网直播起来,RTSP 撞墙。 观众一多,服务器就顶不住,而且穿 NAT、穿防火墙都麻烦。 ③ 2009 年,Apple 推出 HLS。 思路是把直播切成一个个几秒的小文件,用普通 HTTP 分发。 代价是延迟涨到十几秒,收益是能直接上 CDN。 这一步的赌注是「观众不在乎多等十秒」。 ④ 2010 年代,HTTP-FLV 出现。 它把延迟压回 1~3 秒,但仍然是 HTTP,仍然能上 CDN。 这是「两个都要」的方案。 ⑤ 2017 年之后,WebRTC 起来了。 它回到 UDP,用丢了不重传换 200 毫秒延迟。 代价是又要面对 NAT 和扩展性问题——绕了一圈,回到了 RTSP 当年的困境。

所以现在没有「最好的协议」,只有「这个场景选哪个」: 监控用 RTSP,大规模直播用 HLS/HTTP-FLV,连麦和互动用 WebRTC。 三者常常同时存在于一个系统里。

五、动手 Demo:RTSP 与 HTTP:一个有记忆,一个每次重来

同样 10 条命令,两种发法:一条连接复用,或者每条都新建连接。 再列一张对比表,看两边真正的差距在哪。

import socket, threading, time

def serve(sock):
    while True:
        try:
            c, _ = sock.accept()
        except OSError:
            return
        buf = b""
        while True:
            d = c.recv(8192)
            if not d:
                break
            buf += d
            while b"\r\n\r\n" in buf:
                req, buf = buf.split(b"\r\n\r\n", 1)
                cseq = "0"
                for l in req.decode().split("\r\n")[1:]:
                    if l.lower().startswith("cseq:"):
                        cseq = l.split(":", 1)[1].strip()
                c.sendall(("RTSP/1.0 200 OK\r\nCSeq: %s\r\n\r\n" % cseq).encode())
        c.close()

srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(64)
port = srv.getsockname()[1]
threading.Thread(target=serve, args=(srv,), daemon=True).start()

MSG = "OPTIONS rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: %d\r\n\r\n"
N = 10

t0 = time.perf_counter()
s = socket.create_connection(("127.0.0.1", port), 3.0)
for i in range(1, N + 1):
    s.sendall((MSG % i).encode())
    s.recv(8192)
s.close()
keep_ms = (time.perf_counter() - t0) * 1000

t0 = time.perf_counter()
for i in range(1, N + 1):
    s = socket.create_connection(("127.0.0.1", port), 3.0)
    s.sendall((MSG % i).encode())
    s.recv(8192)
    s.close()
new_ms = (time.perf_counter() - t0) * 1000

print("一条连接复用  %.2f 毫秒  单条平均 %.2f 毫秒" % (keep_ms, keep_ms / N))
print("每次新建连接  %.2f 毫秒  单条平均 %.2f 毫秒" % (new_ms, new_ms / N))
print("相对倍数  %.2f" % (new_ms / keep_ms))

实跑输出:

--------------------------------------------------------------------
RTSP 与 HTTP——一个有记忆,一个每次重来
--------------------------------------------------------------------
① 真跑:同样 10 条 RTSP 请求,两种连接方式

  方式             总耗时        单条平均      相对
  -----------------------------------------------------
  一条连接复用     0.86 毫秒     0.09 毫秒     1.00
  每次新建连接     6.02 毫秒     0.60 毫秒     6.98

  **环回上差别只有 7.0 倍,因为建连几乎不要钱。**
  **换成跨省 30 毫秒 RTT,差别就是十几个 RTT 的事。**

② 但真正拉开距离的不是速度,是「记不记得你」

  对比项           RTSP                       HTTP
  -------------------------------------------------------------------
  状态             服务器记住会话             每个请求独立
  播放控制         PLAY/PAUSE/TEARDOWN      没有
  传输             命令走 TCP,媒体走 UDP     全走 TCP
  缓存             基本不可缓存               层层可缓存
  扩到十万观众     很难                       天然(加 CDN 就行)

③ 所以历史是这么走的

  **RTSP 先出现,因为它要「控制」——监控要暂停、要回放、要拖动。**
**但它不可缓存,观众一多就顶不住。**
  **HTTP 派(HLS/HTTP-FLV)后出现,把「控制」扔掉换「可缓存」,于是能上 CDN、能扛千万人。**
  **现在的方案通常是两个都上:监控用 RTSP,直播用 HTTP 派。**

环回上差别只有 7 倍左右,因为建连几乎不要钱(都在本机内存里)。 换成跨省 30 毫秒 RTT,每建一次连就多一个 RTT,10 条就是十几个 RTT 的差距。 但真正的差距不在速度——在「记不记得你」。没有状态,就没有暂停、没有回放、没有拖动。

30 秒实验

  1. 把 N 改成 100,看两种方式的差距放大到什么程度——建连成本是固定的,命令越多、复用越划算。
  2. 在服务端加一句 time.sleep(0.01) 模拟处理耗时,看倍数怎么变——处理时间长了,建连的占比就被稀释了。
  3. 把 N 条命令改成四种不同的方法(OPTIONS/DESCRIBE/SETUP/PLAY 循环),体会「HTTP 也能这么发,但它不会记住你在播」——这就是状态的价值。

常见误区

  1. 「RTSP 就是跑在别的端口上的 HTTP」——端口只是表象,无状态 vs 有状态是根本差别。
  2. 「RTSP 更快所以更好」——环回上确实快几倍,但它在扩展性上彻底输给 HTTP 派。直播选型看的是能扛多少人,不是单条快多少。
  3. 「HLS 延迟高是技术不行」——那是「可缓存」换来的代价。想低延迟就必须回到 UDP,就回到 RTSP 当年的困境。
自测三问(点开对答案) 1. RTSP 和 HTTP 相同的四点是什么? —— **纯文本、请求响应、三位状态码、URL 结构。**再往下就全不一样。 2. 为什么 RTSP 不能上 CDN? —— **因为有状态、内容正在产生、走 UDP。**缓存节点没法预先取、也没法一份给多人。 3. HLS 的十几秒延迟换来了什么? —— **可缓存。**切片是普通文件,CDN 用现成的静态缓存就能分发。

相关笔记