3. RTSP会话建立流程:C/S交互模型、SDP协议简介、完整的RTSP握手过程

RTSP的会话建立,说白了就是客户端和服务器之间的一场「对话」。我刚开始接触的时候,总觉得它跟HTTP很像——都是文本协议,都有请求和响应。但用起来才发现,RTSP的「状态机」特性才是它的灵魂。

这一章,我们就把RTSP的握手流程彻底拆开。从C/S模型讲起,再到SDP这个「媒体描述文件」,最后把OPTIONS到PLAY的完整过程走一遍。

3.1 C/S交互模型:谁主动,谁被动?

RTSP的交互模型很清晰:客户端主动发起请求,服务器被动响应。但这里有个容易混淆的点——RTSP服务器通常也是RTP数据的发送方,但控制信令和媒体数据是两条独立的通道。

我画了一张图,帮你快速理解这个架构:

RTSP C/S交互模型与协议栈 RTSP 客户端 发送控制请求 解析SDP描述 接收RTP媒体数据 维护会话状态 RTSP 服务器 响应控制请求 提供SDP描述 推送RTP/RTCP数据 管理会话状态机 RTSP (TCP 554) RTP/RTCP (UDP 动态端口) 关键点 控制信令走TCP,媒体数据走UDP(或TCP)。两者独立,互不阻塞。 协议栈层次 RTSP (控制) RTP/RTCP (媒体) SDP (描述) TCP/UDP (传输)

你看,客户端在左边,服务器在右边。控制信令是客户端主动发起的,而媒体数据是服务器推过来的。这个模型决定了RTSP的「请求-响应」模式。

我的经验: 在实际项目中,很多开发者会把RTSP和RTP混为一谈。记住,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的配置信息
避坑指南: 我曾经在对接一个摄像头时,发现视频花屏。查了半天,原来是SDP里的 sprop-parameter-sets 字段里的SPS/PPS是Base64编码的,但摄像头给的是十六进制字符串。解码器不认识,自然就花屏了。记住,sprop-parameter-sets 必须是Base64编码,多个NALU用逗号分隔。

3.3 完整的RTSP握手过程:从OPTIONS到PLAY

好了,理论讲完了,我们直接上手。RTSP的握手过程,我习惯把它分成5个步骤。每一步都有明确的目的,少一步都不行。

先看流程图:

RTSP完整握手流程(OPTIONS → PLAY) 客户端 服务器 1. OPTIONS 请求 200 OK + 支持方法列表 2. DESCRIBE 请求 200 OK + SDP 描述 3. SETUP 请求 200 OK + Session ID 4. PLAY 请求 200 OK + RTP开始推送 RTP 媒体数据流 注意 每个请求都必须携带 CSeq 头,用于匹配请求和响应。 SETUP 之后服务器会分配 Session ID,后续请求必须带上。 如果有音频和视频两个流,需要分别 SETUP。

嗯,这张图把整个流程串起来了。下面我们逐条拆解。

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。
我曾经踩过的坑: 有一次对接一个海康的摄像头,SETUP时我传了 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里的编码信息进行解码播放。

我的习惯: 在PLAY请求里,我通常会带上 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的握手流程就这些。说白了就是四步走,每一步都有明确的目的。你在实际项目中遇到问题时,回头看看这四步,基本都能找到原因。