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 秒实验
- 把第 5 步的心跳删掉、等 70 秒,看会怎样——服务端会话超时,再发 PLAY 就回 454。
- 把第 8 步的 Range 删掉,看还有没有「拖进度」——没有,它会从头继续播。
- 给第 3 步的 Transport 改一个已被占用的端口,看回什么——461 Unsupported Transport,第二章讲过的那个码。
常见误区
- 「OnDemand 就是延迟启动」——是「没人要就不干活」。
- 「拖进度有专门的方法」——还是 PLAY,只是多了 Range 头。
- 「单线程所以性能差」——是「一个慢操作拖全部」,不是吞吐低。
自测三问(点开对答案)
1. OnDemand 这三个字省掉了什么开销? —— **没人点播时不打开文件、不读磁盘**,省磁盘与 CPU。 2. 第 4、7、8 步都是 PLAY,怎么区分? —— **看请求里带不带 Range 头**,函数是同一个。 3. 为什么大规模部署是多开进程而不是把单机调优? —— **单线程事件循环里一个慢操作会拖住所有会话**,横向扩展更稳。- ☐ (本章完,下一章 9.1 EasyDarwin 项目简介)