3. RTSP会话建立流程:C/S交互模型、SDP协议简介、完整的RTSP握手过程
RTSP的会话建立,说白了就是客户端和服务器之间的一场「对话」。我刚开始接触的时候,总觉得它跟HTTP很像——都是文本协议,都有请求和响应。但用起来才发现,RTSP的「状态机」特性才是它的灵魂。
这一章,我们就把RTSP的握手流程彻底拆开。从C/S模型讲起,再到SDP这个「媒体描述文件」,最后把OPTIONS到PLAY的完整过程走一遍。
3.1 C/S交互模型:谁主动,谁被动?
RTSP的交互模型很清晰:客户端主动发起请求,服务器被动响应。但这里有个容易混淆的点——RTSP服务器通常也是RTP数据的发送方,但控制信令和媒体数据是两条独立的通道。
我画了一张图,帮你快速理解这个架构:
你看,客户端在左边,服务器在右边。控制信令是客户端主动发起的,而媒体数据是服务器推过来的。这个模型决定了RTSP的「请求-响应」模式。
3.2 SDP协议简介:媒体会话的「身份证」
SDP(Session Description Protocol)不是传输协议,它就是个文本格式的描述文件。说白了,就是告诉客户端:「我这路视频是什么编码、什么分辨率、用什么端口传」。
一个典型的SDP长这样:
v=0
o=- 1234567890 1234567890 IN IP4 192.168.1.100
s=Live Stream
c=IN IP4 0.0.0.0
t=0 0
m=video 5000 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; profile-level-id=42e01e; sprop-parameter-sets=Z0IACpZUBQFs,aMljiA==
m=audio 5002 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/16000/1
a=fmtp:97 streamtype=5; profile-level-id=1; mode=AAC-hbr; config=1408; sizeLength=13; indexLength=3; indexDeltaLength=3
我来拆解一下关键字段:
| 字段 | 含义 | 我的备注 |
|---|---|---|
v=0 |
SDP版本号,目前只有0 | 基本不会变 |
o= |
会话发起者信息 | 包含用户名、会话ID、版本、地址类型 |
s= |
会话名称 | 给人类看的,机器不关心 |
c= |
连接信息 | 推流时通常填0.0.0.0,表示由服务器决定 |
t= |
会话有效时间 | 0 0 表示无限制 |
m= |
媒体描述 | 媒体类型、端口、传输协议、RTP载荷类型 |
a=rtpmap: |
RTP参数映射 | 编码名称、时钟频率、声道数 |
a=fmtp: |
编码格式特定参数 | H.264的SPS/PPS、AAC的配置信息 |
sprop-parameter-sets 字段里的SPS/PPS是Base64编码的,但摄像头给的是十六进制字符串。解码器不认识,自然就花屏了。记住,sprop-parameter-sets 必须是Base64编码,多个NALU用逗号分隔。
3.3 完整的RTSP握手过程:从OPTIONS到PLAY
好了,理论讲完了,我们直接上手。RTSP的握手过程,我习惯把它分成5个步骤。每一步都有明确的目的,少一步都不行。
先看流程图:
嗯,这张图把整个流程串起来了。下面我们逐条拆解。
3.3.1 OPTIONS:探路
客户端先发一个OPTIONS请求,看看服务器支持哪些方法。说白了就是打个招呼:「嘿,你支持什么?」
C->S: OPTIONS rtsp://192.168.1.100/live/stream RTSP/1.0
CSeq: 1
User-Agent: MyRTSPClient/1.0
S->C: RTSP/1.0 200 OK
CSeq: 1
Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER
Date: Mon, 10 Mar 2025 08:30:00 GMT
服务器返回的 Public 头里列出了它支持的方法。我建议你拿到这个列表后,先检查一下有没有 PLAY 和 SETUP,没有的话后面就不用玩了。
3.3.2 DESCRIBE:要「身份证」
知道服务器支持什么之后,客户端就要SDP了。这一步很关键,因为后面SETUP需要的端口、编码信息都在这里。
C->S: DESCRIBE rtsp://192.168.1.100/live/stream RTSP/1.0
CSeq: 2
Accept: application/sdp
User-Agent: MyRTSPClient/1.0
S->C: RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: 256
v=0
o=- 1234567890 1234567890 IN IP4 192.168.1.100
s=Live Stream
c=IN IP4 0.0.0.0
t=0 0
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=control:trackID=0
m=audio 0 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/16000/1
a=control:trackID=1
注意这里的 a=control: 字段——它告诉客户端每个媒体流的SETUP URL是什么。有些服务器用 trackID=0 这种相对路径,有些给完整URL。我遇到过一种情况,服务器返回的control URL是相对路径,但客户端拼接错了,导致SETUP失败。建议你写代码时,先判断control URL是相对还是绝对,再决定怎么拼接。
3.3.3 SETUP:建立传输通道
这一步是握手过程中最复杂的一步。客户端要告诉服务器:「我用什么端口收数据,用什么传输模式」。
C->S: SETUP rtsp://192.168.1.100/live/stream/trackID=0 RTSP/1.0
CSeq: 3
Transport: RTP/AVP/UDP;unicast;client_port=5000-5001
User-Agent: MyRTSPClient/1.0
S->C: RTSP/1.0 200 OK
CSeq: 3
Session: 12345678
Transport: RTP/AVP/UDP;unicast;client_port=5000-5001;server_port=6000-6001
这里有几个关键点:
- Transport头:指定传输协议。常见的有
RTP/AVP/UDP(UDP传输)和RTP/AVP/TCP(TCP传输,也叫interleaved模式)。 - client_port:客户端用于接收RTP和RTCP的端口。偶数端口收RTP,奇数端口收RTCP。这是惯例,别搞反了。
- server_port:服务器返回的端口,客户端可以忽略,但可以用来做防火墙策略。
- Session:服务器分配的会话ID。后续所有请求都必须带上这个ID。
client_port=5000-5001,但服务器返回的 server_port 是 6000-6001。我以为RTP数据会从6000端口发过来,结果抓包一看,服务器还是从5000端口发的。后来查文档才知道,server_port 只是建议值,实际源端口由服务器自己决定。所以,不要依赖 server_port 做防火墙规则,直接用 client_port 收数据就行。
如果有音频和视频两个流,需要分别SETUP。第二次SETUP时要带上第一次返回的Session ID:
C->S: SETUP rtsp://192.168.1.100/live/stream/trackID=1 RTSP/1.0
CSeq: 4
Session: 12345678
Transport: RTP/AVP/UDP;unicast;client_port=5002-5003
3.3.4 PLAY:开始播放
所有传输通道都建立好了,客户端就可以发PLAY了。这一步很简单,但有个细节要注意——Range头。
C->S: PLAY rtsp://192.168.1.100/live/stream RTSP/1.0
CSeq: 5
Session: 12345678
Range: npt=0.000-
S->C: RTSP/1.0 200 OK
CSeq: 5
Session: 12345678
RTP-Info: url=rtsp://192.168.1.100/live/stream/trackID=0;seq=12345;rtptime=987654321,
url=rtsp://192.168.1.100/live/stream/trackID=1;seq=12346;rtptime=987654322
PLAY之后,服务器就开始往客户端推RTP数据了。客户端收到RTP包后,根据SDP里的编码信息进行解码播放。
Range: npt=0.000-。NPT是Normal Play Time,表示从开头播放。对于直播流,有些服务器会忽略这个头,但带上总没错。另外,RTP-Info 头里的 seq 和 rtptime 很重要——它们告诉客户端第一个RTP包的序列号和时间戳,方便客户端做同步和丢包检测。
3.4 完整握手过程的代码示例
下面是一个用Python写的简化版RTSP客户端,演示了完整的握手过程。实际项目中建议用现成的库,但理解底层逻辑很重要。
import socket
def rtsp_handshake(url):
# 解析URL,建立TCP连接
host, path = parse_rtsp_url(url)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, 554))
cseq = 0
session_id = None
# 1. OPTIONS
cseq += 1
send_request(sock, f"OPTIONS {url} RTSP/1.0\r\nCSeq: {cseq}\r\n\r\n")
resp = recv_response(sock)
print(f"OPTIONS: {resp.status_code}")
# 2. DESCRIBE
cseq += 1
send_request(sock, f"DESCRIBE {url} RTSP/1.0\r\nCSeq: {cseq}\r\nAccept: application/sdp\r\n\r\n")
resp = recv_response(sock)
sdp = resp.body
print(f"DESCRIBE: {resp.status_code}, SDP length: {len(sdp)}")
# 解析SDP,获取track信息
tracks = parse_sdp(sdp)
# 3. SETUP (每个track)
for track in tracks:
cseq += 1
setup_url = track['control']
transport = f"RTP/AVP/UDP;unicast;client_port={track['rtp_port']}-{track['rtp_port']+1}"
req = f"SETUP {setup_url} RTSP/1.0\r\nCSeq: {cseq}\r\nTransport: {transport}\r\n"
if session_id:
req += f"Session: {session_id}\r\n"
req += "\r\n"
send_request(sock, req)
resp = recv_response(sock)
session_id = extract_session_id(resp)
print(f"SETUP {track['id']}: {resp.status_code}, Session: {session_id}")
# 4. PLAY
cseq += 1
req = f"PLAY {url} RTSP/1.0\r\nCSeq: {cseq}\r\nSession: {session_id}\r\nRange: npt=0.000-\r\n\r\n"
send_request(sock, req)
resp = recv_response(sock)
print(f"PLAY: {resp.status_code}")
return sock, session_id
这段代码虽然简化了,但核心逻辑都在。你想想看,实际项目中无非就是多了异常处理、超时重试、RTP接收线程这些东西。
3.5 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| SETUP返回461 Unsupported Transport | 服务器不支持UDP传输,只支持TCP | 改用 RTP/AVP/TCP;interleaved=0-1 |
| PLAY返回454 Session Not Found | 没有带上Session ID,或者Session ID过期 | 检查SETUP返回的Session ID,确保后续请求都带上 |
| 收到RTP数据但解码花屏 | SDP里的SPS/PPS信息错误或缺失 | 检查 sprop-parameter-sets 的Base64编码是否正确 |
| PLAY后收不到RTP数据 | 防火墙阻挡了UDP端口,或者NAT问题 | 改用TCP传输模式,或者检查防火墙规则 |
| DESCRIBE返回404 | URL路径错误,或者流不存在 | 确认URL格式,检查服务器端流是否已发布 |
总结一下RTSP握手的核心要点:
- OPTIONS探路,DESCRIBE拿SDP,SETUP建通道,PLAY开始推流——顺序不能乱。
- CSeq必须递增,Session ID必须携带。
- Transport头决定了传输模式,UDP效率高但穿透差,TCP稳定但开销大。
- SDP里的
a=control决定了SETUP的URL,解析时要注意相对路径和绝对路径。
嗯,RTSP的握手流程就这些。说白了就是四步走,每一步都有明确的目的。你在实际项目中遇到问题时,回头看看这四步,基本都能找到原因。