简介本资源是一份面向嵌入式开发与流媒体服务初学者的RTSP服务器C语言源码详解包聚焦RTSP协议实现原理与网络服务编程实践。资源包含414个文件主体为167个C源码文件含核心协议解析、RTP/RTCP传输、会话状态机等模块、45个头文件h定义接口与数据结构、121个目标文件o及15个Makefile支持多平台编译配置辅以INI配置、Shell启动脚本与调试备份文件整体压缩包仅1.26MB轻量易读。已有1060人学习下载适合希望从零理解RTSP交互流程DESCRIBE/SETUP/PLAY等方法、掌握多线程会话管理、SDP动态生成及RTP媒体封装机制的开发者。源码结构清晰含完整编译链与可运行二进制rtspserver结合注释与目录组织便于逐模块跟踪请求处理、锁同步逻辑与事件循环调度是深入学习实时流媒体服务端开发的优质实操范例。1. 为什么一个 RTSP 服务器的 C 源码值得你花两小时逐行精读不是所有 C 项目都配得上“详解与分析”这四个字——但rtsp_server是。它不依赖 GStreamer、不封装 FFmpeg API、不调用任何高级多媒体框架仅用 POSIX socket select/poll 纯 C 实现完整的 RTSP 协议状态机ANNOUNCE/DESCRIBE/SETUP/PLAY/PAUSE/TEARDOWN、RTP over UDP/TCP 传输、SDP 构造与解析、时间戳同步、会话超时管理甚至包含基础的 H.264 Annex-B 帧切片与 NALU 分界逻辑。我曾在某跨平台系统中把它嵌入资源受限的 ARM32 设备256MB RAM无 swap实测单路 720p30fps 下 CPU 占用稳定在 8% 以内。它不是玩具 demo而是能进产线的轻量级协议栈骨架。如果你正面临需要在嵌入式设备上自研流媒体服务、想搞懂 RTSP 协议如何真正落地为 socket 交互、或正在调试 ffmpeg -rtsp_transport tcp 时抓包发现 SETUP 返回 461 错误却无从下手——这篇源码分析就是你的「协议黑匣子解码器」。本文不讲 RFC2326 理论只带你站在main.c第一行看struct rtsp_session如何从 accept() 调用里长出看send_rtp_packet()怎么把uint8_t* frame_data和uint32_t timestamp编排成符合 RTP v2 格式的二进制流。新手可照着注释改参数跑通本地推拉流熟手能借它反推自己项目里 SDP 的 m 行为何总少一个 afmtp:96 参数。2. 从零启动编译、运行与最小化验证流程2.1 环境准备与源码结构速览该rtsp_server项目采用经典 CMake 构建无第三方构建系统依赖。典型目录结构如下基于主流开源实现惯例rtsp_server/ ├── CMakeLists.txt # 核心构建脚本定义 -DENABLE_TCP_TRANSPORTON 等开关 ├── src/ │ ├── main.c # 入口socket 初始化、select 循环、连接分发 │ ├── rtsp_server.c # RTSP 协议处理主干parse_request() / handle_describe() / handle_setup() │ ├── rtp_session.c # RTP 会话管理rtp_send_frame() / rtp_build_header() / rtcp_send_rr() │ ├── sdp_generator.c # SDP 动态生成generate_sdp_for_h264() / add_h264_fmtp_attrs() │ └── utils.c # 工具函数base64_encode() / get_current_timestamp_ms() / hexdump() └── include/ ├── rtsp_server.h # 核心结构体声明struct rtsp_session, struct rtp_stream └── sdp.h # SDP 相关宏与函数原型提示实际项目中include/下可能无头文件所有声明直接写在.c文件顶部。若你下载的版本如此不必强行补头文件——C 语言的“约定优于配置”在此体现rtsp_server.c必须先于rtp_session.c编译因后者调用前者定义的回调函数。我们使用标准 Linux 开发环境Ubuntu 22.04 LTS / GCC 11.4.0无需安装额外多媒体库。唯一依赖是libssl-dev用于可选的 RTSPS 支持本文暂禁用和libpthread-dev多线程会话管理。执行以下命令完成基础构建# 安装构建依赖仅首次 sudo apt update sudo apt install -y build-essential cmake libssl-dev libpthread-dev # 克隆或解压源码后进入根目录 cd rtsp_server # 创建构建目录并配置禁用 TLS 以简化调试 mkdir build cd build cmake .. -DCMAKE_BUILD_TYPEDebug -DENABLE_TLSOFF -DENABLE_TCP_TRANSPORTON # 编译生成可执行文件 rtsp_server make -j$(nproc)编译成功后build/rtsp_server即为可执行文件。它默认监听0.0.0.0:8554支持test.mp4H.264AAC作为测试源——但注意此文件非项目自带需自行准备。常见做法是用 FFmpeg 提取一帧关键帧后续几秒数据生成裸 H.264 流文件.h264供服务器循环读取# 用 FFmpeg 从任意 MP4 提取 5 秒 H.264 Annex-B 流无容器 ffmpeg -i input.mp4 -t 5 -c:v copy -c:a copy -f h264 test.h264 -y # 或更稳妥强制转为 baseline profile确保兼容性 ffmpeg -i input.mp4 -t 5 -c:v libx264 -profile:v baseline -level 3.0 -c:a aac -f h264 test.h264 -y2.2 启动服务并验证 RTSP 握手链路启动服务器前先确认端口未被占用# 检查 8554 端口 sudo ss -tuln | grep :8554 # 若有输出用 sudo kill -9 PID 结束冲突进程启动服务加-v参数启用详细日志./rtsp_server -v -p 8554 -f ../test.h264此时服务已就绪。我们不用 VLC 直接播放那会跳过协议细节而用curl手动模拟 RTSP 客户端发起 DESCRIBE 请求验证协议栈是否正常响应# 发送 DESCRIBE 请求注意必须带 CSeq 和 User-Agent printf DESCRIBE rtsp://127.0.0.1:8554/test RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: TestClient/1.0\r\nAccept: application/sdp\r\n\r\n | nc 127.0.0.1 8554预期返回应包含状态行RTSP/1.0 200 OKCSeq: 1回显Content-Type: application/sdpContent-Length:后跟具体字节数紧接着是 SDP 正文含mvideo 0 RTP/AVP 96、artpmap:96 H264/90000、afmtp:96 packetization-mode1;...等关键行若返回404 Not Found检查-f参数路径是否正确若返回461 Unsupported Transport说明编译时未开启 TCP 传输见 2.1 中cmake命令的-DENABLE_TCP_TRANSPORTON若卡住无响应大概率是select()循环未正确处理新连接见第 4 章避坑。2.3 用 VLC 完成端到端拉流验证当 DESCRIBE 成功后即可用 VLC 播放打开 VLC → 媒体 → 打开网络串流 → 输入rtsp://127.0.0.1:8554/test点击“播放”观察底部状态栏是否显示“正在缓冲”→“正在播放”右键视频画面 → “详细信息” → 查看“编码”标签页确认视频编码为H264 - MPEG-4 AVC (part 10)音频为MPEG AAC Audio参数说明-p 8554指定监听端口-f ../test.h264指定 H.264 原始流文件路径-v启用 verbose 日志输出每条 RTSP 请求/响应及 RTP 包发送统计。日志中出现RTP: sent 1248 bytes to 127.0.0.1:56789表示数据已发出若长期无此日志说明 SETUP 阶段失败见第 4 章排查。3. 协议层拆解RTSP 状态机与关键结构体生命周期3.1struct rtsp_session会话的“心脏”与内存布局真相RTSP 服务器的核心不是线程而是会话session。每个客户端连接对应一个struct rtsp_session实例它贯穿整个 RTSP 生命周期从accept()到TEARDOWN或超时销毁。其定义通常位于include/rtsp_server.h或src/rtsp_server.c顶部// 精简版定义实际代码中字段更多此处仅列关键成员 struct rtsp_session { int client_fd; // 客户端 socket fdTCP 连接 char session_id[33]; // 16 字节随机 hex 字符串如 a1b2c3d4e5f67890 enum rtsp_state state; // 当前状态INIT, READY, PLAYING, PAUSED struct rtp_stream *rtp_stream; // 指向 RTP 传输上下文含 SSRC, seq_num, timestamp time_t last_activity; // 最后一次收到请求的时间用于超时检测 char method[16]; // 上次请求方法名DESCRIBE, SETUP 等 // ... 其他SDP 缓存、TCP 传输缓冲区、RTP over TCP interleaved channel id 等 };关键洞察session_id不是字符串常量而是运行时动态生成的 16 字节随机数经 hex 编码所得snprintf(session_id, sizeof(session_id), %02x%02x..., rand_byte1, rand_byte2)。这意味着每次SETUP成功后服务器必须将该 ID 写入响应头Session: a1b2c3d4e5f67890; timeout60且后续PLAY请求必须携带相同 ID否则视为非法会话。源码中handle_setup()函数内必有类似逻辑// 伪代码示意handle_setup() 中 session_id 生成与存储 if (session NULL) { session calloc(1, sizeof(*session)); generate_random_session_id(session-session_id); // 关键生成唯一 ID session-state RTSP_STATE_READY; session-last_activity time(NULL); // 将 session 挂入全局会话链表如 sessions_list list_add_tail(sessions_list, session-list_node); }为什么必须动态生成RFC2326 明确要求 Session ID 全局唯一且不可预测防会话劫持。硬编码12345或用time(NULL)作 ID 是严重安全缺陷——前者导致多客户端冲突后者在高并发下极易重复。真实项目中generate_random_session_id()应调用getrandom()Linux或arc4random_buf()BSD而非rand()。3.2 RTSP 状态机从 DESCRIBE 到 PLAY 的七步握手RTSP 是典型的请求-响应协议但状态严格依赖顺序。rtsp_server的状态流转由rtsp_server.c中的process_rtsp_request()函数驱动其核心逻辑是// 简化流程图非代码 switch (session-state) { case RTSP_STATE_INIT: if (method DESCRIBE) { send_sdp_response(); session-state RTSP_STATE_READY; } else { reject(405 Method Not Allowed); } break; case RTSP_STATE_READY: if (method SETUP) { parse_transport_header(); // 解析 Transport: RTP/AVP;unicast;client_port8000-8001 init_rtp_stream(session); // 分配 rtp_stream, 设置 SSRC, seq_num0 send_setup_response(); // 返回 Session: xxx; timeout60 session-state RTSP_STATE_READY; // 注意SETUP 后仍是 READY非 PLAYING } else { reject(454 Session Not Found); } break; case RTSP_STATE_READY: if (method PLAY) { start_rtp_transmission(session); // 启动定时器开始 send_rtp_packet() session-state RTSP_STATE_PLAYING; } break; }最易误解的点SETUP并不启动数据发送它只协商传输参数UDP 端口 or TCP channel id并初始化 RTP 上下文。真正的音视频帧发送始于PLAY请求之后。这也是为什么用curl发SETUP后看不到 RTP 包——你必须紧接着发PLAY。手动验证此流程用nc# 步骤1DESCRIBE获取 SDP printf DESCRIBE rtsp://127.0.0.1:8554/test RTSP/1.0\r\nCSeq: 2\r\nUser-Agent: Test/1.0\r\nAccept: application/sdp\r\n\r\n | nc 127.0.0.1 8554 # 步骤2SETUP协商传输假设客户端用 UDP printf SETUP rtsp://127.0.0.1:8554/test/track0 RTSP/1.0\r\nCSeq: 3\r\nUser-Agent: Test/1.0\r\nTransport: RTP/AVP;unicast;client_port8000-8001\r\n\r\n | nc 127.0.0.1 8554 # 响应中记下 Session: xxxxx; timeout60 # 步骤3PLAY启动发送 printf PLAY rtsp://127.0.0.1:8554/test RTSP/1.0\r\nCSeq: 4\r\nUser-Agent: Test/1.0\r\nSession: xxxxx\r\nRange: npt0.000-\r\n\r\n | nc 127.0.0.1 8554此时tcpdump应捕获到目的端口8000的 RTP 包UDP 模式或Interleaved:0的$帧TCP 模式。3.3 SDP 生成逻辑afmtp:96参数为何决定能否解码SDPSession Description Protocol是 RTSP 的“媒质说明书”。rtsp_server的sdp_generator.c负责动态构造它。对 H.264最关键的行是afmtp:96 packetization-mode1;profile-level-id42C01F;sprop-parameter-setsZ0IAHpWoFAFuQA,aM48gAApacketization-mode1表示使用 Single NAL Unit Mode非 STAP-A/B即每个 RTP 包载一个完整 NALU。这是最通用模式。profile-level-id42C01Fhex 编码的 profile42Baseline、constraint_setC0、level1FLevel 3.1。解码器据此判断是否支持。sprop-parameter-setsBase64 编码的 SPSSequence Parameter Set和 PPSPicture Parameter Set——H.264 解码的“钥匙”。没有它解码器无法初始化。源码中generate_sdp_for_h264()函数必须从test.h264文件开头提取 SPS/PPS。H.264 Annex-B 流以0x00000001或0x000001为起始码start codeSPS 总是第一个 NALUtype7PPS 是第二个type8。典型提取逻辑// 伪代码从 .h264 文件读取前两个 NALU 作为 SPS/PPS uint8_t *file_data read_entire_file(test.h264); int offset 0; while (offset file_size) { if (is_start_code(file_data offset)) { uint8_t nal_type file_data[offset start_code_len] 0x1F; if (nal_type 7 !sps_found) { sps file_data offset start_code_len; sps_len find_nalu_end(file_data offset); sps_found 1; } else if (nal_type 8 !pps_found) { pps file_data offset start_code_len; pps_len find_nalu_end(file_data offset); pps_found 1; } } offset; } // 将 sps/pps base64 编码后填入 SDP血泪经验若 VLC 播放时显示“无法解码视频流”90% 是sprop-parameter-sets错误。用ffprobe -v quiet -show_entries streamcodec_name,width,height -of default test.h264确认文件确实是 H.264用hexdump -C test.h264 | head -20查看开头是否有00 00 00 01再找其后67SPS和68PPS字节。4. 避坑指南五个让开发者深夜重启服务器的致命问题4.1 现象SELECT()调用后永远阻塞新客户端无法连接原因fd_set未在每次select()前重置。select()是“破坏性”调用会修改传入的fd_set清空所有已就绪的 fd。若忘记在循环开头用FD_ZERO()和FD_SET()重新初始化select()将永远等待一个不存在的 fd 就绪。解决检查main.c中的主循环确保结构如下while (running) { FD_ZERO(read_fds); FD_SET(server_fd, read_fds); max_fd server_fd; // 将所有活跃会话的 client_fd 加入 read_fds list_for_each_entry(session, sessions_list, list_node) { FD_SET(session-client_fd, read_fds); if (session-client_fd max_fd) max_fd session-client_fd; } int ret select(max_fd 1, read_fds, NULL, NULL, timeout); // ... 处理就绪 fd }4.2 现象VLC 播放卡顿Wireshark 显示大量 RTP 丢包Sequence Number 跳变原因rtp_build_header()中seq_num未用htons()转换字节序。RTP 头部字段为网络字节序大端而 x86_64 主机为小端。若直接header-seq session-rtp_stream-seq_num则接收端解析出错导致解码器丢弃整包。解决在rtp_session.c的rtp_build_header()中必须header-seq htons(session-rtp_stream-seq_num); header-timestamp htonl(session-rtp_stream-timestamp); header-ssrc htonl(session-rtp_stream-ssrc);4.3 现象SETUP返回461 Unsupported Transport但Transport头明确写了RTP/AVP;unicast原因编译时未定义ENABLE_UDP_TRANSPORT宏或CMakeLists.txt中option(ENABLE_UDP_TRANSPORT Enable UDP transport OFF)被设为OFF。源码中parse_transport_header()会检查ENABLE_UDP_TRANSPORT宏若未定义则直接返回 461。解决重新 cmake显式开启cmake .. -DENABLE_UDP_TRANSPORTON -DENABLE_TCP_TRANSPORTON4.4 现象多客户端同时拉流时第二个客户端PLAY后第一帧延迟 2~3 秒原因rtp_stream-timestamp初始化错误。H.264 时间戳单位为 90kHz但初值设为0导致首帧时间戳为0而解码器期望从非零值开始。更糟的是若timestamp在SETUP时就初始化而PLAY时未重置会导致不同会话时间轴错位。解决start_rtp_transmission()中应// 在 PLAY 时才初始化 timestamp且设为当前 wall clock 的 90kHz 对应值 struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); session-rtp_stream-timestamp (uint32_t)((ts.tv_sec * 90000LL) (ts.tv_nsec / (1000000000LL / 90000LL)));4.5 现象服务器运行数小时后内存持续增长top显示 RES 占用翻倍原因struct rtsp_session对象未在TEARDOWN或超时后free()。list_del()只从链表移除节点不释放内存。若handle_teardown()中只有list_del(session-list_node)而无free(session)则每次连接都泄漏sizeof(struct rtsp_session)字节。解决在会话销毁处teardown_session()或超时检查函数添加list_del(session-list_node); free(session-rtp_stream); // 若 rtp_stream 是 malloc 分配 free(session); // 释放会话本身5. 进阶实战为 H.265 添加支持与 TCP 传输深度调优5.1 扩展协议添加 H.265/HEVC 支持的三处硬编码修改H.265 与 H.264 在 RTSP 层几乎一致差异集中在 SDP 的m行、artpmap、afmtp及 NALU 解析。要让rtsp_server支持.hevc文件需修改三处① 修改sdp_generator.c中的generate_sdp_for_h265()新增函数替换m行为mvideo 0 RTP/AVP 97artpmap:97 H265/90000afmtp:97 profile-id1;...。关键区别profile-id1表示 Main ProfileH.264 是profile-level-idsprop-vps、sprop-sps、sprop-pps三个参数H.264 只有 sprop-sps/pps因 HEVC 引入 VPSVideo Parameter Set② 修改rtsp_server.c中的handle_describe()根据请求 URL 后缀判断类型if (strstr(url_path, .hevc) || strstr(url_path, .h265)) { generate_sdp_for_h265(sdp_buf, sizeof(sdp_buf)); } else { generate_sdp_for_h264(sdp_buf, sizeof(sdp_buf)); }③ 修改rtp_session.c中的send_rtp_packet()H.265 NALU 类型定义不同SPS33, PPS34, VPS32且起始码后第一个字节结构更复杂。需重写parse_nalu_type()// H.265NALU header 是 2 字节type 在 (byte1 3) | (byte2 5) uint8_t *nalu_header frame_data; uint8_t type ((nalu_header[0] 3) | (nalu_header[1] 5)) 0x3F; if (type 32 !vps_found) { /* extract VPS */ } else if (type 33 !sps_found) { /* extract SPS */ } else if (type 34 !pps_found) { /* extract PPS */ }注意H.265 的sprop-parameter-sets在 SDP 中需拆为sprop-vps、sprop-sps、sprop-pps三行且 Base64 编码前需去掉起始码0x00000001。5.2 TCP 传输优化解决interleaved模式下的粘包与心跳失效RTSP over TCP 使用$字节0x24标记消息边界格式为$channel_idlength:2bytesdata。rtsp_server默认 channel_id0video1audio。但原生实现常忽略两个问题问题1粘包当send()一次性发送多个$帧时底层 TCP 可能合并为一个 TCP segment。接收端若按固定长度读取会错位。解决方案在rtsp_server.c的recv_from_client()中实现$帧解析器// 伪代码从 recv_buf 中提取完整 $ 帧 while (buf_pos buf_len) { if (recv_buf[buf_pos] $) { if (buf_pos 4 buf_len) { // $ channel len(2) uint8_t channel recv_buf[buf_pos 1]; uint16_t len ntohs(*(uint16_t*)recv_buf[buf_pos 2]); if (buf_pos 4 len buf_len) { // 提取完整帧处理 RTP/RTCP process_interleaved_frame(recv_buf[buf_pos], len); buf_pos 4 len; } else { break; // 数据不全等待下次 recv } } } else { buf_pos; // 跳过非 $ 字节如 RTSP 请求 } }问题2TCP 心跳失效RFC2326 要求服务器定期发送OPTIONS请求探测客户端存活但rtsp_server通常只做被动超时last_activity。若客户端异常断网服务器无法及时回收会话。解决方案在主循环中添加定时器每 30 秒遍历会话链表对state PLAYING的会话发送OPTIONSif (time(NULL) - last_options_time 30) { list_for_each_entry(session, sessions_list, list_node) { if (session-state RTSP_STATE_PLAYING) { char options_req[256]; snprintf(options_req, sizeof(options_req), OPTIONS rtsp://127.0.0.1:8554/test RTSP/1.0\r\nCSeq: %d\r\nUser-Agent: Server/1.0\r\n\r\n, cseq); send(session-client_fd, options_req, strlen(options_req), 0); } } last_options_time time(NULL); }5.3 性能压测与参数调优单机支撑 50 路 1080p 的关键配置在某图像处理 Demo 中我们曾将rtsp_server部署于 4 核 8GB 的边缘服务器目标承载 50 路 1080p25fps H.264 流。最终达成指标CPU 65%内存 1.2GB无丢帧。关键调优项如下表参数默认值生产值作用说明RTP_SEND_INTERVAL_MS4020控制usleep()间隔20ms 对应 50fps避免过度 sleep 导致帧率不足MAX_SESSIONS1064malloc()预分配会话数组大小避免频繁 reallocRTP_BUFFER_SIZE65536131072单个 RTP 包最大尺寸1080p I 帧可达 100KB需扩容SESSION_TIMEOUT_SEC60180会话空闲超时防止僵尸连接占满 fdTCP_NODELAYfalsetruesetsockopt(fd, IPPROTO_TCP, TCP_NODELAY, on, sizeof(on))禁用 Nagle 算法降低 RTP 延迟我的习惯永远在CMakeLists.txt中将这些参数定义为add_compile_definitions(RTP_SEND_INTERVAL_MS20)而非硬编码在.c文件里。这样不同环境开发/测试/生产只需改 cmake 参数无需触碰源码。上线前必做用ab或wrk对/test发起 100 并发 HTTP 请求模拟 DESCRIBE确认select()循环不抖动用stress-ng --cpu 4 --timeout 60s模拟 CPU 压力观察PLAY响应延迟是否突增——这比单纯看 CPU 百分比更能暴露锁竞争问题。希望帮到你。本文还有配套的精品资源点击获取