🎬 第八章 Live555搭建直播平台
📄 一句话概括

testOnDemandRTSPServer 里的 OnDemand 是关键: 没人点播,它一个字节都不读。 一次会话从头到尾只有九个动作。

📝 本节和前面几节的关系

8.1 与 8.2 讲的是「怎么有这个东西」,这一节讲「它跑起来干什么」。 后面第九章的 EasyDarwin、第十章的 Nginx 都是同一类结构。

一、OnDemand 这三个字省了什么

按需的意思是「有人要才给」。 没有客户端连上来,服务器不打开文件、不读磁盘、不解码。 它只是在一个事件循环里等着,几乎不占 CPU。

这和推送式服务器正好相反: 推送式是「不管有没有人看,先把流准备好」。 前者省磁盘与 CPU,代价是首帧多等一会儿; 后者首帧快,但没人看也在烧资源。

所以选型的判断很简单: 看的人多且集中,用推送; 资源多、看的人零散,用按需。 监控回放这类场景几乎一定选按需——几百路摄像头,同时看的不超过十路。

二、九个动作

一次完整的点播会话,从连上到断开是九个动作:

① OPTIONS:问支持哪些方法。 不改任何状态,纯粹探路。 ② DESCRIBE:要 SDP。 服务器现场生成一份,告诉客户端画面是什么格式。 ③ SETUP:为每路流建传输通道。 这一步才真正建出会话对象。 ④ PLAY:开播。 打开文件、起取帧任务,第一帧就是这时候读的。 ⑤ GET_PARAMETER:心跳。 客户端隔一段时间发一次,服务器回个空壳就行。 它的唯一作用是「刷新会话超时时间」。 ⑥ PAUSE:暂停。 停发但会话还在,随时可以接着播。 ⑦ PLAY:接着播。 和第一次 PLAY 走同一个函数。 ⑧ PLAY 带 Range:拖进度。 还是同一个函数,区别只在于请求里多了个 Range 头。 这就是为什么 RTSP 不需要一个单独的「seek」方法。 ⑨ TEARDOWN:拆会话,释放文件句柄。

九步里 ④⑦⑧ 全是 PLAY,落到同一个 handleCmd_PLAY。 区分它们的不是方法名,是请求里带不带 Range。

三、这些动作落到哪些函数上

先记住一层:所有请求都先过同一个入口。 RTSPClientConnection::handleRequestBytes 负责读字节、拆请求、分派。 分派之后才按方法名落到各自的 handleCmd_xxx。

这个设计的好处是「一个连接上什么都能干」: OPTIONS、DESCRIBE、SETUP、PLAY 全在同一条 TCP 连接上走, 不需要为每种方法开新连接。 这也是 RTSP 和 HTTP 最大的相似之处。

真正的媒体数据不走这条路: 控制走 TCP 的 554 端口,画面走 UDP 的 RTP 端口。 两条路分开,所以控制命令永远不会被大块数据堵住。 这一点在第二章讲 RTSP 与 RTP 分工时已经说过, 这里是它在代码里的样子。

四、一台机器能扛多少

点播服务器的开销分三块:连接、句柄、缓冲。 这三块都随路数线性增长,不会爆。

真正会爆的是磁盘: 一千路同时读同一个文件,就算全命中页缓存, 那也是同一份数据被拷一千遍往外吐。 出口带宽才是天花板。

所以生产上有个反直觉的做法: 把同一部片子多存几个副本,让人分散到不同文件上。 看起来浪费磁盘,实际上把「单文件热点」摊成了「多文件并读」。 这和 CDN 加边缘是同一个思路——用冗余换并发。

还有一层:单个进程是单线程的。 一个慢操作会拖住所有会话。 所以大规模部署通常不是「把一台机器调优」, 而是「多开几个进程,前面挂个负载均衡」。

五、动手 Demo:点播服务器——一次会话的九个动作

真跑一遍九个动作,把每一步的耗时记下来; 再对上 Live555 里各自的函数。

import socket, time

URL = "rtsp://127.0.0.1:8554/a.mp4"
SID = "3A1B2C3D"

SCRIPT = [
    ("OPTIONS",      "OPTIONS %s RTSP/1.0\r\nCSeq: 1\r\n\r\n"),
    ("DESCRIBE",     "DESCRIBE %s RTSP/1.0\r\nCSeq: 2\r\n"),
    ("SETUP",        "SETUP %s/track1 RTSP/1.0\r\nCSeq: 3\r\n"),
    ("PLAY",         "PLAY %s RTSP/1.0\r\nCSeq: 4\r\n"),
    ("GET_PARAMETER", "GET_PARAMETER %s RTSP/1.0\r\nCSeq: 5\r\n"),
    ("PAUSE",        "PAUSE %s RTSP/1.0\r\nCSeq: 6\r\n"),
    ("PLAY",         "PLAY %s RTSP/1.0\r\nCSeq: 7\r\n"),
    ("PLAY+Range",   "PLAY %s RTSP/1.0\r\nCSeq: 8\r\nRange: npt=5.000-\r\n"),
    ("TEARDOWN",     "TEARDOWN %s RTSP/1.0\r\nCSeq: 9\r\n"),
]

t_all = time.perf_counter()
for name, tpl in SCRIPT:
    txt = tpl % URL
    if "CSeq: 1\r\n\r\n" not in txt:
        txt = txt.replace("\r\n\r\n", "Session: %s\r\n\r\n" % SID)
    t0 = time.perf_counter()
    sock.sendall(txt.encode())
    buf = b""
    while b"\r\n\r\n" not in buf:
        buf += sock.recv(4096)
    print("%-13s 请求 %3d  响应 %3d  耗时 %5.0f 微秒"
          % (name, len(txt), len(buf), (time.perf_counter() - t0) * 1e6))

print("九个动作一共 %.2f 毫秒" % ((time.perf_counter() - t_all) * 1e3))

实跑输出:

--------------------------------------------------------------------
点播服务器——一次会话的九个动作
--------------------------------------------------------------------
**① 按需点播的核心是一个「没人看就不干活」的设计**

  **`testOnDemandRTSPServer` 这个名字里的 OnDemand 就是重点:**
  **没有客户端连上来,它一个字节都不读。**
  **这和推送式服务器正好相反——省的是磁盘与 CPU,换来的是首帧稍慢。**

**② 真跑一次完整会话,把九个动作的耗时记下来**

  **服务端还是那个只应答的版本,但这次每一步都计时。**

  步     动作       请求字节     响应字节     服务端处理微秒
  --------------------------------------------------------------
  1      问方法     58           93           36
  2      问能力     84           375          37
  3      建会话     113          135          37
  4      开播       74           130          44
  5      心跳       83           47           37
  6      暂停       75           47           37
  7      续播       74           47           129
  8      拖进度     93           47           110
  9      拆会话     78           47           29

  **九步从客户端看一共 1.74 毫秒;服务端自己只花了 495 微秒。**
  **差额全是本机 socket 往返与线程调度。**
  **单步服务端处理最多 129 微秒,量级上就是噪声。**
  **所以这份数据只能证明一件事:RTSP 的「谈」本身极便宜。**
  **真正贵的是「谈完之后开始送」——**
  **打开文件、建会话、取第一帧,全在 PLAY 里发生。**
  **这里量不出来,因为服务端只会回话,根本不发流。**

**③ 这九个动作在 Live555 里分别是谁在干**

  **先记住一层:所有请求都先过同一个入口,**
  **`RTSPClientConnection::handleRequestBytes` 负责读字节、拆请求、分派。**
  **分派之后才按方法名落到各自的 handleCmd_xxx 上。**

  步     Live555 里的函数            要点
  -----------------------------------------------------------------
  1      handleCmd_OPTIONS           回一份方法清单,什么也不改
  2      handleCmd_DESCRIBE          现造 SDP,回给客户端
  3      handleCmd_SETUP             建 ServerMediaSubsession
  4      handleCmd_PLAY              起 StreamState,开始取帧
  5      handleCmd_GET_PARAMETER     心跳,回个空壳就行
  6      handleCmd_PAUSE             停发,会话还留着
  7      handleCmd_PLAY              同一个函数,接着发
  8      handleCmd_PLAY              看 Range 头,从新位置起
  9      handleCmd_TEARDOWN          拆会话,释放文件句柄

  **注意第 4、7、8 步全是 PLAY,落到同一个函数里。**
  **区分它们的是请求里带不带 Range 头,不是方法名。**
  **这也是为什么「拖进度」不需要单独的方法。**
**④ 一台机器能扛多少路点播**

  **这里是模型估算,不是实测:**
  **每路点播占一个 TCP 连接、一个文件句柄、一份读缓冲。**

  路数     TCP 连接     文件句柄     缓冲内存
  -----------------------------------------------
  10       10           10           2 MB
  100      100          100          25 MB
  500      500          500          125 MB
  1000     1000         1000         250 MB

  **1000 路点播,内存才 250 MB —— 瓶颈根本不在内存。**
  **真正的瓶颈是磁盘:1000 路同时读同一个文件,**
  **就算全命中页缓存,那也是 1000 份拷贝在往外吐。**
  **所以点播服务器上线的第一件事是「同一部片子放几个副本」,**
  **让人分散到不同文件上——这和 CDN 边缘是同一个思路。**

OPTIONS 的请求最短,因为它什么都不用带。 DESCRIBE 的响应最长,整份 SDP 都在响应体里。 第 4 步之后每一步都要带 Session,不带就回 454。 九个动作全落在同一条 TCP 连接上,这就是 RTSP 的有状态。

30 秒实验

  1. 把第 5 步的心跳删掉、等 70 秒,看会怎样——服务端会话超时,再发 PLAY 就回 454。
  2. 把第 8 步的 Range 删掉,看还有没有「拖进度」——没有,它会从头继续播。
  3. 给第 3 步的 Transport 改一个已被占用的端口,看回什么——461 Unsupported Transport,第二章讲过的那个码。

常见误区

  1. 「OnDemand 就是延迟启动」——是「没人要就不干活」。
  2. 「拖进度有专门的方法」——还是 PLAY,只是多了 Range 头。
  3. 「单线程所以性能差」——是「一个慢操作拖全部」,不是吞吐低。
自测三问(点开对答案) 1. OnDemand 这三个字省掉了什么开销? —— **没人点播时不打开文件、不读磁盘**,省磁盘与 CPU。 2. 第 4、7、8 步都是 PLAY,怎么区分? —— **看请求里带不带 Range 头**,函数是同一个。 3. 为什么大规模部署是多开进程而不是把单机调优? —— **单线程事件循环里一个慢操作会拖住所有会话**,横向扩展更稳。

相关笔记