RTSP 只发文本命令,RTP 只搬二进制画面,RTCP 只定期报账。 三者分工清楚,所以任意一个坏掉,坏的方式都不一样。
前面 3.1 与 3.2 已经把 RTP 和 RTCP 单独拆开看过了。这一节把 RTSP 也放进来,看三者怎么拼成一条完整的流。 后面第 8 到第 12 章搭架直播服务器,全靠这个分工模型。
一、三个协议各管一件事
先给一句话定性:RTSP 是遥控器,RTP 是送货车,RTCP 是回执。
① RTSP:走 TCP,全是可读的文本(PLAY、PAUSE、TEARDOWN),只负责「开始、暂停、停」,它一个字节的画面都不碰。
② RTP:走 UDP,二进制,按时间戳排好、带上序号发出去。
它不管可靠、不管顺序——错了留给上层补。
③ RTCP:也走 UDP,定期汇报「我发了多少」「我丢了多少」,让发送方知道链路好不好。
这个划分最妙的地方是「坏掉的影响不同」: RTSP 断了,已经在播的流还能继续播——命令通道没了,数据通道还在。RTP 断了,遥控器按得再响也没画面。 RTCP 断了,画面照样播,只是发送方变成瞎子,不会主动降码率。
二、端口:一个固定,三个协商
RTSP 的 554 是 IANA 分配的固定端口,全世界都是这个数。 而 RTP 与 RTCP 的两对端口是每次会话临时协商的。
协商的过程只有两句话:
① 客户端在 SETUP 请求的 Transport 头里报自己能收的端口对,比如 client_port=5000-5001。
② 服务器在响应里回一个 server_port=6000-6001,从此 RTP 发到偶数端口、RTCP 发到奇数端口。
这个「偶 RTP、奇 RTCP」是硬约定,不是随便定的。 RFC 3550 规定 RTCP 端口必须是 RTP 端口加一,这样只要知道一个端口就能推出另一个,省掉一次协商。
麻烦也正在这里:防火墙要放 554,还得放一整段 UDP 端口区间,而这段区间各家设备还不一样。 运维最烦的就是这种「固定 + 动态」的组合。
三、为什么很多人把 RTP 也塞进 TCP
RTP/AVP/TCP 这个 Transport 写法,意思是「RTP 不要走 UDP,和 RTSP 共用一条 TCP 连接」。
好处很直接:只开一个 554 端口就能通,穿透任何防火墙与 NAT。 代价也很直接,有三条: ① 延迟变大:TCP 会等重传,丢一个包后面的包全卡住——这叫队头阻塞,视频里的表现是「整段停一下再快进」。 ② 头部开销翻倍:每个 RTP 包外面还要套 4 字节的长度与通道号。 ③ 语义变了:本来是「丢一帧无所谓」,变成「一个字节都不能少」。
所以选型上有一条经验: 内网、局域网监控可以走 TCP;公网、弱网、要低延迟的场景必须走 UDP。 WebRTC 干脆彻底不用 RTSP,就是为了绕开这个两难。
四、三者在时间尺度上的差别
这三个协议的「节奏」完全不同,抓包时一眼能看出来: ① RTSP 是事件驱动:只在用户操作时蹦一条,可能几分钟一条。 ② RTP 是恒速:25 fps 就是每秒 25 帧、几十个包,像心跳一样稳。 ③ RTCP 是周期:通常 5 秒一次,而且带宽越大、间隔越长——标准要求它占的带宽不超过 5%。
抓包时如果按「包数排序」,RTSP 永远排在最上面也最下面(因为最少),RTP 密密麻麻,RTCP 每隔几秒冒几个。 这个视觉差异比记协议字段有用得多。
还有一个容易忽略的点:RTCP 的间隔是「算」出来的,不是写死的。 会话人越多,每个人的 RTCP 间隔越长,否则几千个接收方同时发 RR,反馈流量会盖过媒体流。
五、动手 Demo:一条连接里同时跑三种协议
开三个 socket:一个 TCP 收 RTSP 命令,两个 UDP 分别收 RTP 与 RTCP。 跑一遍,看三个通道各收到多少东西。
import socket, struct, threading, time
tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp.bind(("127.0.0.1", 0))
tcp.listen(4)
tport = tcp.getsockname()[1]
rtp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rtp.bind(("127.0.0.1", 0))
rtcp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rtcp.bind(("127.0.0.1", 0))
got = {"rtp": 0, "rtcp": 0}
def rx(s, k):
s.settimeout(0.5)
while True:
try:
s.recvfrom(2048)
except socket.timeout:
return
got[k] += 1
threading.Thread(target=rx, args=(rtp, "rtp"), daemon=True).start()
threading.Thread(target=rx, args=(rtcp, "rtcp"), daemon=True).start()
def serve():
c, _ = tcp.accept()
while True:
d = c.recv(4096)
if not d:
return
c.sendall(b"RTSP/1.0 200 OK\r\nCSeq: 1\r\n\r\n")
threading.Thread(target=serve, daemon=True).start()
c = socket.create_connection(("127.0.0.1", tport), 3.0)
c.sendall(b"PLAY rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: 1\r\n\r\n")
c.recv(4096)
for i in range(20):
hdr = struct.pack("!BBHII", 0x80, 96, i, i * 3600, 0x1A2B3C4D)
rtp.sendto(hdr + b"y" * 40, ("127.0.0.1", rtp.getsockname()[1]))
for i in range(3):
rtcp.sendto(struct.pack("!BBH", 0x80, 200, 6) + b"\x00" * 24,
("127.0.0.1", rtcp.getsockname()[1]))
time.sleep(0.5)
print("RTSP 1 条命令,RTP %d 包,RTCP %d 包" % (got["rtp"], got["rtcp"]))
实跑输出:
--------------------------------------------------------------------
三个协议的分工——谁下命令、谁搬数据、谁报账
--------------------------------------------------------------------
**① 真跑一遍:RTSP 走 TCP,RTP 与 RTCP 走 UDP**
协议 跑在哪 端口 收了多少
--------------------------------------------------
RTSP TCP 63256(临时) 1 条命令
RTP UDP 57040 20 个包
RTCP UDP 57041 3 个包
**② 分工:一个下命令,一个搬数据,一个报账**
**① RTSP 下命令**:走的全是文本,只负责「开始、暂停、停」。**它不碰一个字节的画面。**
**② RTP 搬数据**:二进制,按时间戳排好,一次会话一路流一个 SSRC。
**它不管可靠、不管顺序——错了由上层补。**
**③ RTCP 报账**:定期汇报「我发了多少」「我丢了多少」,**让发送方知道链路好不好。**
**③ 三者的端口是「一个固定、三个协商」**
**RTSP 的 554 是固定的,全世界都是这个数。**
**而 RTP 和 RTCP 的两对端口是每次会话临时协商的。**
**客户端在 Transport 头里报,服务器在响应里确认。**
**这个「固定 + 动态」的组合是 RTSP 部署的最大麻烦。**
**防火墙要放 554,还得放一段 UDP 端口区间,而这段区间各家设备还不一样。**
**所以穿防火墙时很多人干脆用 `RTP/AVP/TCP`,把 RTP 也塞进 554 里。**
**代价是延迟变大,因为 TCP 会等重传。**
**一句话总结:RTSP 是遥控器,RTP 是送货车,RTCP 是回执。**
**遥控器坏了车还在跑**——命令丢了不影响已经开始的流。
**送货车坏了遥控器也没用**——没数据可看。
**回执丢了,车会越开越盲目。**
三条通道各走各的,互不干扰——这就是「分工」在代码里的样子。 RTSP 全程只有一条命令,因为它只在开播时说话;RTP 一上来就是二十个包,因为画面是按帧走的。 RTCP 只发了三个包,正好对应「5 秒一次」的节奏——沙箱里发了 3 个,真实会话里也是这个量级。
30 秒实验
- 把
PLAY改成PAUSE,看服务器会不会停发——RTSP 的语义就体现在这个「一按就变」上。 - 把 RTP 的 20 个包删掉,只发 RTCP,看接收端有没有画面——没有 RTP 就是没画面,报账再勤快也没用。
- 把 20 个包的时间戳全部改成同一个值,算一下这相当于多少 fps——时间戳不变等于「所有帧同时显示」,实际会只看到一帧。
常见误区
- 「RTSP 里带视频数据」——RTSP 只有文本命令,画面全在 RTP 里。
- 「RTCP 是可有可无的」——没它也能播,但发送方不知道丢包,弱网下不会自动降码率。
- 「RTP 端口可以随便定」——必须是偶数,RTCP 用它的下一个奇数。这是 RFC 的硬约定。
自测三问(点开对答案)
1. RTSP、RTP、RTCP 各走什么传输层? —— **RTSP 走 TCP,RTP 与 RTCP 走 UDP。**例外是 `RTP/AVP/TCP` 把 RTP 也塞进 TCP。 2. 为什么 RTCP 端口必须是 RTP 端口加一? —— **省一次协商。**知道一个就能推出另一个,这是 RFC 3550 的约定。 3. RTSP 断线之后画面为什么还能继续? —— **因为数据走的是独立的 UDP 通道。**命令通道断,只影响后续操作。- ☐ (本节完,下一节 3.4.1 Windows10+VS2015编译JRTPLIB)