2026/9/16 21:21:34

手写RTSP服务器系列:RTSP协议核心原理与实现要点详解

手写RTSP服务器系列:RTSP协议核心原理与实现要点详解 看到这个标题估计你已经猜到了我想写的是一个“手写 RTSP 服务器”系列的完整记录。作为系列第一篇我不急着甩代码而是先把 RTSP 协议本身掰开揉碎讲清楚。协议都没吃透后面写出来的服务器大概率只能自娱自乐VLC、FFmpeg 这些客户端连进来都会给你脸色看。RTSP 服务器的核心价值在于它能让任意一个 IP 网络中的客户端实时拉取音视频流无论是摄像头、直播推流、还是本地视频文件转流。整个行业里做流媒体接入、安防平台、直播网关的人基本每天都在跟这个协议打交道。这篇文章适合所有打算自己实现 RTSP 服务器、或者想彻底搞懂拉流协议细节的开发者我尽量把协议设计背后的逻辑也讲透不只是罗列字段。1. RTSP 到底是什么控制面与媒体面分离的设计1.1 RTSP 在直播架构里的位置先说结论RTSPReal Time Streaming Protocol实时流协议是一个“控制协议”它不负责传输音视频数据本身。如果你把它想象成一个遥控器RTPReal-time Transport Protocol才是那台正在播放画面的电视。RTSP 负责的是“按播放键”“暂停”“拖进度条”“切换通道”这些动作RTP 负责把画面一帧一帧搬到客户端屏幕上。在我们最常见的直播场景里整体链路是这样的摄像头或编码器推流上来你的 RTSP 服务器收到之后缓存或转封装然后等待客户端发起拉流请求。客户端发一个OPTIONS、DESCRIBE、SETUP、PLAY的请求序列服务器依次响应最终双方通过 RTP 开始传数据。这个“请求-响应”的玩法本质上和 HTTP 长得非常像结构都是文本行加头域这也是为什么很多人第一次看 RTSP 抓包会觉得眼熟。1.2 为什么 RTSP 要做成“控制传输”分离很多新手会问为什么不能像 HTTP 那样一个请求直接把媒体流带走原因是实时流的传输要求和普通文件下载完全不同。普通文件可以随便重传断点续传但实时视频有严格的时延要求过期的帧再重传没有任何意义。所以 RTSP 把控制信令独立出来用可靠的 TCP 传输保证“播放”“暂停”这些指令不会丢把媒体数据交给 RTP用 UDP 传输保证低时延丢几个包可以靠解码容错硬扛。如果你所在网络的 UDP 被限制还可以退回到 RTP over TCP 的模式把媒体数据也塞进 TCP 通道传输这也是很多安防平台在复杂网络下的保底方案。控制面与媒体面分离让两端各自做自己最擅长的事这个设计到今天看仍然非常合理。1.3 RTSP、RTP、RTCP 三者的分工RTSP负责会话管理包括请求描述、建立传输通道、控制播放状态默认端口 554明文也可以跑在高位端口比如 8554。RTP负责实际传输音视频数据包封装格式按照封装协议来比如 H.264 的 RTP 打包规范是 RFC 3984新版是 RFC 6184。RTCP负责传输统计信息比如发送端报告SR、接收端报告RR每间隔一段时间交换一次用于计算丢包率、抖动、时延服务器和播放器靠它感知网络质量。写 RTSP 服务器时如果只做基础拉流RTCP 可以先实现最简单的 SR 发送客户端其实对 RTCP 的完整性容忍度很高但完全不发 RTCP有些播放器在长时间播放后会因为收不到反馈而判定会话异常这点后面我会再细说。2. 报文长什么样RTSP 的文本协议细节2.1 请求与响应的基本格式RTSP 1.0RFC 2326的报文格式和 HTTP/1.1 非常像客户端发的是请求行服务器回的是状态行。一个典型的 OPTIONS 请求是这个样子OPTIONS rtsp://127.0.0.1:8554/live RTSP/1.0 CSeq: 1 User-Agent: VLC/3.0.18 libVLC/3.0.18服务器对应的响应RTSP/1.0 200 OK CSeq: 1 Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER Session: 12a34b5c这里有一个细节需要注意CSeq头在 RTSP 里极其重要。CSeq是“sequence number”的缩写请求和响应必须一一对应服务器返回的CSeq必须和请求一致。客户端可能同时发多个请求虽然实际中很少这么干如果没有CSeq做对应两边就乱套了。2.2 与 HTTP 的核心异同RTSP 和 HTTP 都基于文本行和头域但两个协议本质上是不同的。HTTP 是无状态的每个请求之间没有必然联系RTSP 是有状态的服务器必须记住当前会话处于什么阶段。此外HTTP 主要是客户端主动、服务器被动RTSP 则允许服务器主动向客户端发请求比如ANNOUNCE服务器宣告新流、REDIRECT服务器重定向到另一个地址。维度HTTPRTSP传输对象文档、资源媒体流控制指令状态性无状态HTTP/1.1 可配合 Cookie 维持状态有状态SETUP 后建立会话请求方向客户端请求服务器响应客户端和服务端都可以主动发起媒体数据响应体直接传输通过 RTP/RTCP 另行传输代表方法GET、POST、PUT、DELETEOPTIONS、DESCRIBE、SETUP、PLAY、PAUSE、TEARDOWN这种相似与不同导致很多新手犯一个典型错误拿 HTTP 服务器的框架去套 RTSP结果CSeq处理不好会话状态管理混乱。记住RTSP 的本质是“有状态的远程控制协议”。2.3 关键头域逐个拆解CSeq每个请求必须带从 1 开始递增服务器响应必须原样返回。自己实现时建议用无符号整数存储不做特殊处理只做回显。SessionSETUP成功后服务器会返回这个头格式通常是Session: 一串ID后面所有的 PLAY、PAUSE、TEARDOWN 请求都要带上它服务器用它定位会话。Transport传输协商头客户端在SETUP请求中表明自己想用哪种传输方式服务器返回确认后的结果。PublicOPTIONS 响应中的头告诉客户端这个服务器支持哪些 RTSP 方法。有些实现不全的服务器客户端看到 Public 里没有 PLAY 就直接不播了。Require/Unsupported客户端通过Require表示自己必须使用某个扩展特性如果服务器不支持就回Unsupported头。这个机制是 RTSP 扩展能力下兼容的关键自己实现时遇到不认识的头域一定要忽略不能直接报错这也是 RFC 的要求。3. 一个完整的 RTSP 会话是如何走通的3.1 从 OPTIONS 到 TEARDOWN标准交互流程我直接写一个完整的、最典型的 RTSP 拉流交互序列大家对着抓包看会发现 99% 的播放器都是这么跑的客户端发OPTIONS询问服务器支持哪些方法。服务器回 200列出 Public 方法列表。客户端发DESCRIBE要求获取流描述通常带Accept: application/sdp。服务器回 200body 是 SDP 描述包括流的编码格式、分辨率、帧率等元信息。客户端解析 SDP知道这个流有几路媒体视频、音频各一路然后为每个媒体轨发一次SETUP协商传输端口与传输模式。客户端发PLAY服务器收到后正式启动向客户端发送 RTP 包。播放过程中客户端可能发PAUSE暂停也可能发GET_PARAMETER做保活探测。停止播放时客户端发TEARDOWN服务器释放会话资源。每一步都有严格的先后关系。客户端没做 SETUP 就发 PLAY服务器必须回455 Method Not Valid In This State客户端对一个已经 TEARDOWN 的会话发请求服务器要回454 Session Not Found。3.2 CSeq 与事务一一对应我单独把CSeq提出来讲是因为太多人在这个地方翻车。CSeq是客户端自己维护的计数器每次发新请求都递增服务器不能自作主张修改响应里的CSeq必须原样带回。响应只负责告诉“我收到了你的第 N 个请求结果是这样”。有个容易被忽视的坑由于 RTSP 同时支持请求与响应复用同一条 TCP 连接客户端做并行请求的概率虽然低但服务器在处理时不能假设请求是串行到达的。就算串行服务器也必须严格根据CSeq确认“当前响应对应哪个请求”。我的经验是服务器内部用 map 按CSeq维护每个待处理请求处理完一个就删掉一个这样即使乱序也不会出大问题。3.3 状态机与 Session 生命周期RTSP 会话的状态机不复杂一共三个核心状态Init初始状态连接建立后、SETUP 完成前。DESCRIBE、OPTIONS 在这个阶段可以任意发。ReadySETUP 成功后的状态媒体传输通道已协商好但没有开始播放。此时可发 PLAY也可以再 SETUP 其他媒体轨。PlayingPLAY 成功后的状态正在传输媒体数据。此时 PAUSE 会回到 ReadyTEARDOWN 则直接销毁会话。当前状态收到方法结果InitSETUP成功则进入 Ready分配 Session IDReadyPLAY成功则进入 Playing开始发送 RTPPlayingPAUSE成功则回到 Ready暂停发送 RTPReady / PlayingTEARDOWN销毁会话关闭媒体发送资源InitPLAY回 455状态不变服务器实现时必须为每个会话保存当前状态并且在状态非法时返回对应的 RTSP 状态码。很多客户端对状态码非常敏感你回一个笼统的 500播放器可能直接退出回 455播放器知道你是个守规矩的服务器还能等待下一步操作。4. SDP 与 SETUP 里的学问如何真正把媒体流“牵起来”4.1 SDP 必须提供的字段DESCRIBE响应里的 SDP 是客户端决定怎么播放的核心依据。SDP 全程 Session Description Protocol会话描述协议RTSP 服务器对它做的是一个“打包发送”的工作但打包内容有讲究。一个最基础的视频流 SDP 长这样v0 o- 1589986800000000 1 IN IP4 0.0.0.0 sLive Stream t0 0 mvideo 0 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1;profile-level-id64001f acontrol:trackID0字段拆解v0SDP 版本号固定写 0。o会话发起者包含用户名、会话 ID、网络类型、地址类型、地址。在自己实现时会话 ID 可以填时间戳地址可以填服务器 IP也可以填 0.0.0.0。s会话名必须有内容随意。t0 0会话时间0 0 表示未知持续时间即直播。mvideo 0 RTP/AVP 96媒体描述。这里的 0 是端口在没有 SETUP 之前可以写成 0真正传输时的端口由 SETUP 协商决定96 是 RTP 负载类型Payload Type后面我称 PT。artpmap:96 H264/90000负载格式映射表示 PT 96 对应的编码格式是 H.264采样时钟频率是 90000 Hz。acontrol:trackID0控制 URL客户端会用它拼接该媒体轨的 SETUP 地址。4.2 Transport 头传输方式协商SETUP请求里最重要的头域是Transport它是整个 RTSP 协议最值得仔细琢磨的地方之一。客户端发来的一行请求可能是这样SETUP rtsp://127.0.0.1:8554/live/trackID0 RTSP/1.0 CSeq: 3 Transport: RTP/AVP;unicast;client_port4000-4001这里的RTP/AVP表示通过 UDP 传输 RTPunicast表示单播client_port4000-4001表示客户端期望 RTP 包发到 UDP 4000 端口RTCP 包发到 4001 端口。注意RTP 端口和 RTCP 端口总是相邻的且 RTP 用偶数端口、RTCP 用奇数端口。服务器收到后需要确认自己是否支持该方式如果支持就返回自己的端口RTSP/1.0 200 OK CSeq: 3 Transport: RTP/AVP;unicast;client_port4000-4001;server_port5000-5001如果服务器觉得 UDP 不保险也可以主动降级。比如服务器只支持 TCP 传输它可以返回Transport: RTP/AVP/TCP;interleaved0-1interleaved表示 RTP 和 RTCP 数据会复用到 RTSP 的 TCP 连接里后面跟的数字是通道编号0 代表 RTP1 代表 RTCP。很多播放器同时支持 UDP 和 TCP就看服务器怎么选。网络环境复杂时TCP 模式成功率远高于 UDP原因很简单TCP 不怕 NAT不会丢包缺点是实时性略差。4.3 端口、PT 值、时钟频率的选择这里解答几个实现时必然遇到的问题。第一端口怎么选客户端发来的client_port是客户端自己的端口服务器要选一个本地空闲端口给server_port。选择时注意RTP 示例中我用的是 5000实际线上部署建议避开系统保留端口段并且让 RTP 端口为偶数、RTCP 端口为奇数。很多系统的 UDP 端口是随机分配的服务器启动后可以预分配一段范围比如 50000-51000 之间的偶数避免和高位动态端口冲突。第二PT 值怎么填H.264 这类动态负载类型通常用 96-127 区间不需要全局统一只要 RTSP 服务器和客户端在 SDP 的rtpmap里达成一致就行。使用 96 完全是约定俗成你可以用 97、98只要 SDP 里写清楚。我自己习惯视频固定用 96音频用 97这样排查问题时看包一眼就知道是啥。第三时钟频率为什么视频基本都是 90000因为 RTP 时间戳的单位是 1/90000 秒这个频率能被绝大多数常见帧率整除25fps 一帧是 3600 个时间戳单位30fps 是 3000 个单位计算下来都是整数能很好避免时间戳累计误差。音频则不同PCM 用 8000AAC 用 44100 或 48000按实际采样率填。5. 从零实现 RTSP 服务器的三道坎5.1 鉴权与 AuthorizationRTSP 支持 Basic 和 Digest 两种鉴权方式Base 方式就是 base64 编码用户名密码很好实现但不安全Digest 方式需要做 MD5 摘要计算稍微复杂一些。大部分场景比如家庭局域网里的摄像头默认是不开启鉴权的。但你要考虑一个情况客户端在 URL 里带了用户名密码比如rtsp://admin:123456192.168.1.10:554/live客户端实际上会先请求一个不带 Authorization 的请求服务器回 401并带上WWW-Authenticate头客户端才重新带上 Authorization 头请求。如果服务器不实现鉴权逻辑或者面对不认识的 Authorization 头直接报错就会导致客户端永远无法通过。我的建议是第一版服务器先支持 Basic 鉴权代码简单也很好调试。但响应 401 时WWW-Authenticate头一定要写对格式WWW-Authenticate: Basic realmMyRTSP。否则有的客户端会直接放弃。5.2 超时、Keep-Alive 与连接管理RTSP 会话不是建了就不管的服务器需要通过超时机制来回收那些悄悄消失的客户端。SETUP成功时服务器可以在Session头里带上超时时间Session: 12a34b5c;timeout60这个timeout表示 60 秒内如果客户端没有发任何 RTSP 请求服务器有权利关闭会话。但实际运行中很多客户端在播放状态下不会主动发请求因为 RTP 包一直在传输。这时服务器不应该单纯以“是否收到新的 RTSP 请求”来判断客户端是否存活应该结合 RTP/RTCP 的发送状态来做判断只要 RTP 还在正常发送就认为会话还是活的。等到 PAUSE 之后没有媒体流了再依赖 timeout 机制。有一个典型坑VLC 在播放中会定期发GET_PARAMETER来探活如果服务器收到GET_PARAMETER后不回 200VLC 过一会儿就会判定连接异常。所以哪怕你暂时没有实现任何可获取的参数收到GET_PARAMETER也一定要回一个空 body 的 200 OK。这是很多手写服务器被 VLC 秒断的常见原因。5.3 兼容性同样的报文为什么有的客户端不认你可能会遇到一种情况用 FFmpeg 拉流一切正常换 VLC 就黑屏或者海康的播放器正常蓝石云的 SDK 却拉不起流。这种兼容性问题绝大多数出在服务器的“回答太死板”上。RTSP 协议里有一个原则对未知的请求头域和未知的 SDP 属性应当忽略而不是报错。但很多新手写得服务器会检查每个字段一旦不认识就回 400。这样做的后果是客户端一旦带了一些额外扩展字段整个会话就立不起来。正确的做法是过滤出自己关心的字段其他一律忽略将 RTSP 作为一种“尽量协商、绝不决裂”的协议对待。另外一个常见兼容性问题发生在 URL 拼接上。SETUP请求的 URL 可能是完整 URL也可能是相对路径媒体轨的 URL 可能是trackID0也可能是trackID1。服务器解析时不能用简单字符串匹配要用 URL 标准解析逻辑把基础路径和 control 属性拼起来。比如基础 URL 是rtsp://server/live/streamSDP 的acontrol:trackID0客户端 SETUP 的地址可能是rtsp://server/live/stream/trackID0也可能是rtsp://server/live/trackID0取决于服务器怎么设计。你必须在实现时明确定义一套规则并且兼容常见播放器的习惯。6. 调试 RTSP 服务器必备的排查技巧6.1 抓包看 RTSP 层做 RTSP 开发Wireshark 是绝对的主力工具。Wireshark 对 RTSP 有完整的协议解析只要在过滤栏输入rtsp就能看到所有 RTSP 请求与响应。有一点要提醒如果 RTSP 走的是 TCP 端口Wireshark 默认能正确解析如果走的是非标准端口比如 8554Wireshark 可能不认识需要右键选择“Decode As”然后手动指定为 RTSP。这一步不设置你会看到一堆难以理解的 TCP 报文。抓包时重点关注四件事请求的CSeq和响应是否一一对应。Transport的协商结果是否和实际发送 RTP 的端口一致。Session头是否在 SETUP 之后被正确带上。PLAY 之后是否真的有 RTP 包发往 client_port。6.2 用 VLC 与 FFmpeg 做验证客户端调试服务器最省事的验证工具就是ffplay和ffprobe。命令行方式非常直接ffplay -rtsp_transport tcp rtsp://127.0.0.1:8554/live ffprobe -rtsp_transport udp rtsp://127.0.0.1:8554/liveffplay负责实际播放ffprobe负责查看流信息。-rtsp_transport参数强制指定传输方式非常利于分别测试 TCP 和 UDP 两种模式。FFmpeg 的 RTSP 客户端实现非常规范如果你的服务器能过了 FFmpeg大概率离成功就不远了。VLC 也是很好的验证工具而且 VLC 的行为比 FFmpeg 更“苛刻”比如前面说的GET_PARAMETER保活问题就是 VLC 特有的。我习惯用 VLC 做第二道验证关卡FFmpeg 能通VLC 必须也能通两个都通了才敢把服务器端出去给别人对接。GStreamer 的rtspsrc组件也是常见的测试客户端不过门槛稍高一般验证阶段用不到。6.3 常见“看起来正常却连不上”的原因根据我自己调试 RTSP 服务器的经验以下问题出现频率最高列成表方便排查。现象可能原因解决办法OPTIONS 正常DESCRIBE 之后卡住SDP 格式不合法服务器响应没带Content-Type: application/sdp检查 SDP 每行结尾是不是 CRLF响应头必须带 Content-Type 和 Content-LengthSETUP 返回 461 Unsupported Transport服务器没有实现客户端要求的传输方式要么实现对应模式要么在响应中明确告知支持的其他模式PLAY 返回 200 但客户端不显示画面RTP 包的负载类型与 SDP 不一致SSRC 抖动时间戳异常抓包确认 PT 值、SSRC 是否稳定时间戳是否递增播放几秒后卡住服务器发送 RTP 速度失控没有实现 RTCP 反馈控制发送速率定期发送 RTCP SR即使不能基于反馈调整码率也要应答客户端断开后服务器 CPU 上升会话资源没有在 TEARDOWN 后释放统一在 TEARDOWN 分支释放 RTP 发送线程、端口、内存这些坑我在写第一个 RTSP 服务器时几乎全踩过。最难受的不是单个问题而是这些问题往往交织在一起比如客户端因为 SDP 里的时间戳异常而表现成“播放几秒后卡死”排查时容易误判成网络问题。所以调试 RTSP 一定不要靠猜严格抓包对照协议字段一一排查才是省时间的唯一路径。协议层面的事情先讲到这里。RTSP 的细节还有很多但把 OPTIONS、DESCRIBE、SETUP、PLAY、TEARDOWN 这条主线吃透加上 SDP 和 Transport 协商这两个核心模块搞明白你已经具备写一个可运行服务器的前提了。我个人在实际操作中最深的体会是RTSP 协议并不难难的是耐住性子把报文和抓包慢慢对齐吃透不急于写代码协议理清楚了后面真正实现的时候就会顺利很多。下一篇我打算开始动手写代码到时候我会从 socket 层开始一步步把控制信令和 RTP 发送实现出来欢迎继续关注这个系列。