ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Netty实现RTMP服务器的粘包处理与心跳保活实战

2026/9/24 21:52:06 拓冰建站 浏览量
Netty实现RTMP服务器的粘包处理与心跳保活实战 简介这是一份基于Netty实现的轻量级RTMP服务器开源项目面向Java后端开发者、流媒体初学者及实时音视频服务实践者解决从零构建RTMP推拉流服务器的核心技术落地问题。资源共50个文件含16个Java源码涵盖Netty服务启动、RTMP握手解析、Chunk解复用、AMF消息处理等核心逻辑、19个编译后class文件、9个XML配置与依赖管理文件以及README文档和IDE配置文件整体仅59KB结构精简、模块职责清晰便于快速理解协议层与框架层协同机制。已有767人学习下载适合用于深入掌握RTMP协议交互流程、Netty事件驱动编程模型以及FFmpeg推流联调验证。读者可直接运行服务端结合FFmpeg命令完成推流测试并通过源码逐层分析连接建立、音视频包解析、会话管理等关键环节获得可调试、可扩展的RTMP服务开发范例。1. 为什么你本地跑通的 RTMP 推流一上生产就卡顿、断连、花屏——这不是网络问题是 Netty 层没扛住粘包和心跳你用 FFmpeg 推流到rtmp://localhost:1935/live/stream能播但换到局域网另一台机器就黑屏用 OBS 推流 10 分钟后自动断开日志里只有一行Channel inactive更玄的是同一份推流配置在 Windows 上稳如泰山Linux 上每 37 秒必丢一次 GOP。这些不是“玄学”而是rtmpServer-master_nettyrtmp这类基于 Netty 实现的轻量级 RTMP 服务在真实场景中暴露出的典型失稳现象。它不依赖 Nginx-RTMP 或 SRS 那样的重型中间件靠纯 Java Netty 构建协议栈优势是代码透明、可定制强、嵌入成本低——特别适合 IoT 设备端嵌入、教育录播系统二次开发、或作为微服务架构中的流媒体网关模块。但代价是Netty 的 ChannelHandler 链必须亲手处理 RTMP 协议握手、Chunk Stream 复用、AMF0/AMF3 解析、时间戳校准、以及最关键的——TCP 层粘包与半包。很多开发者卡在“能跑通”就停步结果上线后被并发推流压垮、被弱网环境反向击穿、被安卓端低版本 MediaCodec 推出的非标 FLV Tag 搞崩溃。这篇笔记就是帮你把rtmpServer-master从“玩具级 demo”拉回“可交付服务”的实操路径。2. 从源码结构到启动流程看清rtmpServer-master_nettyrtmp真正的骨架这个项目名里的nettyrtmp不是噱头它本质是一个Netty 4.x 驱动的 RTMP 协议解析器 内存级流管理器而非完整 CDN 或转码服务。它的价值不在功能堆砌而在协议层的可控性——你能直接修改RtmpDecoder中的decode()方法来兼容某款国产 IPC 的私有 Header也能在RtmpSession里注入自定义鉴权逻辑。先厘清它到底由哪几块组成再决定改哪里、不动哪里。2.1 源码目录的真实分工以主流 fork 版本为准提示不要迷信 GitHub 上 star 数最高的分支。实际生产中我们选的是 commit 在2022-08-15后、明确标注support-h265且pom.xml中 Netty 版本为4.1.92.Final的 fork。旧版4.1.43存在ByteBuf内存泄漏风险已在该 commit 中修复。目录路径核心职责是否建议修改关键文件举例src/main/java/com/github/rtmpserver/protocol/rtmpRTMP 协议状态机、消息编解码RtmpMessage,RtmpDecoder,RtmpEncoder⚠️ 高风险仅当需支持 H.265 Annex B 或自定义 AMF 结构时动RtmpHandshake.java,RtmpMessageDecoder.javasrc/main/java/com/github/rtmpserver/handlerNetty ChannelHandler 链连接管理、心跳保活、流注册、推拉流路由✅ 推荐重点改造这是业务逻辑入口RtmpConnectionHandler.java,RtmpStreamHandler.javasrc/main/java/com/github/rtmpserver/storage流数据暂存策略内存队列默认、可插拔的 Redis 缓存、或文件落地需自行实现✅ 必调决定并发承载力上限MemoryStreamStorage.java,StreamStorage.javasrc/main/resources/application.conf启动参数端口、最大连接数、chunk size、心跳间隔、流超时阈值✅ 必配不调等于裸奔rtmp.port1935,rtmp.max.connections5002.2 启动入口RtmpServerApplication的三步初始化链它不走 Spring Boot AutoConfiguration而是手动构建 NettyServerBootstrap。关键在于initPipeline()中的 Handler 注册顺序——这直接决定粘包是否被正确切分// RtmpServerApplication.java private void initPipeline(ChannelPipeline pipeline) { // Step 1: 必须最先加——解决 TCP 粘包 pipeline.addLast(frameDecoder, new LengthFieldBasedFrameDecoder( 10 * 1024 * 1024, // max frame length: 10MB覆盖大关键帧 0, // length field offset 4, // length field length -4, // length adjustment 0 // initial bytes to strip )); // Step 2: RTMP 协议解码器依赖上一步已切好的完整 chunk pipeline.addLast(rtmpDecoder, new RtmpDecoder()); // Step 3: 业务处理器连接、流、心跳 pipeline.addLast(connectionHandler, new RtmpConnectionHandler()); pipeline.addLast(streamHandler, new RtmpStreamHandler()); }为什么LengthFieldBasedFrameDecoder是生死线RTMP 协议本身不带长度头但 Netty 要求每个ByteBuf必须代表一个完整 RTMP Message即一个 Chunk Stream 的完整 payload。而真实网络中TCP 层会把多个小 chunk 合并发送粘包或把一个大 chunk 拆成多段拆包。LengthFieldBasedFrameDecoder就是靠读取每个 chunk 的message length字段RTMP Header 后第 4 字节起的 3 字节来切分。如果这里配错后续所有解码都会错位——表现为AMF decode error、invalid timestamp、或直接ChannelInactiveException。2.3 默认流地址规则与测试验证闭环它不生成rtmp://xxx/live/xxx这种固定路径而是按appname两级动态注册。推流地址格式为rtmp://host:port/app-name/stream-name例如rtmp://192.168.1.100:1935/live/camera01app-name对应application.conf中rtmp.app.name默认live也是流存储的命名空间stream-name客户端指定服务端不做校验直接注册为StreamKey验证是否真正跑通不能只看 FFmpeg 命令返回success要三步确认连接层telnet 192.168.1.100 1935能通说明 TCP 监听正常协议层Wireshark 抓包过滤rtmp ip.addr192.168.1.100看到connect→createStream→publish完整握手数据层用 VLC 打开rtmp://192.168.1.100:1935/live/camera01播放 5 分钟无卡顿、无花屏、时间戳连续VLC 右下角显示FPS: 25.0且不跳变。3. 推流稳定性攻坚Netty 粘包处理、心跳保活与安卓端兼容三板斧推流中断不是偶然是 TCP 连接在无数据时被中间设备路由器、防火墙、NAT静默回收。rtmpServer-master默认的心跳机制极其简陋——只在RtmpConnectionHandler中响应客户端发来的ping却不主动探测。而安卓端尤其 Android 8的 MediaCodec 推流常因省电策略导致onVideoFrameAvailable回调延迟造成心跳超时。这三件事必须一起调。3.1 粘包处理从LengthFieldBasedFrameDecoder到RtmpChunkDecoder的双保险上面提到的LengthFieldBasedFrameDecoder是第一道防线但它只保证“字节流被切成 chunk”不保证“chunk 被正确组装成 message”。RTMP 的 Chunk Stream 机制允许一个 message 被拆成多个 chunk 发送RtmpDecoder必须缓存未完成的 chunk 并等待chunk type为0full message或1first chunk的标志位。常见错误是未设置chunkSize全局变量默认 128 字节导致大 I 帧被过度切分RtmpChunkDecoder中未处理timestamp delta跨 chunk 累加造成音视频不同步。实操修正在RtmpDecoder.java中强化 chunk 组装逻辑// RtmpDecoder.java private void decodeChunk(ByteBuf in, ListObject out) { // ... 解析 chunk basic header ... if (chunkType 0) { // full message // 直接解码 RtmpMessage msg decodeMessage(in); out.add(msg); } else if (chunkType 1 || chunkType 2) { // first or middle chunk // 缓存到 currentChunkBuffer并记录 expectedLength if (currentChunkBuffer null) { currentChunkBuffer Unpooled.buffer(expectedLength); } currentChunkBuffer.writeBytes(in.readBytes(in.readableBytes())); // 关键检查是否收齐 if (currentChunkBuffer.readableBytes() expectedLength) { RtmpMessage msg decodeMessage(currentChunkBuffer); out.add(msg); currentChunkBuffer.release(); currentChunkBuffer null; } } }参数说明expectedLength来自 chunk header 中的message length字段必须在chunkType 0或1时准确读取Unpooled.buffer()创建堆外内存缓冲区避免频繁 GCcurrentChunkBuffer.release()必须显式释放否则内存泄漏这是rtmpServer-master旧版高频 Bug。3.2 心跳保活双向探测 可配置超时阈值默认实现只响应ping但安卓端可能因后台限制不发ping。我们必须服务端主动writeAndFlush(new RtmpPingMessage())客户端连接后启动IdleStateHandler超时后执行closeOnIdle()而非粗暴channel.close()。在RtmpConnectionHandler.java中注入心跳逻辑// initPipeline 中添加 pipeline.addLast(idleStateHandler, new IdleStateHandler( 30, // readerIdleTimeSeconds30秒没收到数据则触发 IDLE_STATE_EVENT 0, // writerIdleTimeSeconds不主动发心跳时不启用 0 // allIdleTimeSeconds )); pipeline.addLast(heartbeatHandler, new HeartbeatHandler()); // HeartbeatHandler.java public class HeartbeatHandler extends ChannelInboundHandlerAdapter { Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent event (IdleStateEvent) evt; if (event.state() IdleState.READER_IDLE) { // 主动发 ping 探测 ctx.writeAndFlush(new RtmpPingMessage()); // 同时启动超时计时器防 ping 丢失 ctx.channel().attr(HEARTBEAT_TIMEOUT).set(System.currentTimeMillis()); } } } }配套配置项application.confrtmp.heartbeat { interval 25s # 心跳间隔必须 readerIdleTimeSeconds timeout 45s # 连续2次ping无响应则断连 enable true }3.3 安卓端兼容绕过MediaCodec输出的非标 FLV Tag安卓MediaCodec输出的 H.264 Annex B 数据常在 SPS/PPS 前插入0x00000001起始码但RtmpMessageDecoder默认按标准 FLV Tag 解析Tag Header 后直接是 NALU。若不解析起始码会导致AVCDecoderConfigurationRecord解析失败进而无法生成正确的onMetaData。解决方案在RtmpMessageDecoder.java中预处理 NALUprivate byte[] normalizeH264Nalu(byte[] raw) { ByteBuffer bb ByteBuffer.wrap(raw); // 检查是否以 0x00000001 开头 if (bb.remaining() 4 bb.get(0) 0 bb.get(1) 0 bb.get(2) 0 bb.get(3) 1) { // 跳过起始码提取真实 NALU byte[] nalu new byte[bb.remaining() - 4]; bb.position(4); bb.get(nalu); return nalu; } return raw; // 已是标准格式 }注意此逻辑必须放在decodeVideoData()中且仅对AVC NALU类型生效tagType 9 avcPacketType 1。对音频 AAC 数据无效。4. 避坑指南生产环境踩过的 5 个血泪坑每个都让服务停摆超 2 小时别等线上报警才翻日志。这 5 个坑是我们用 3 台边缘盒子、7 种安卓机型、21 天压力测试撞出来的。它们不写在 README 里但每个都足以让rtmpServer-master在凌晨 3 点崩给你看。4.1 现象推流 1 分钟后ChannelInactiveException日志只显示connection reset by peer原因Linux kernel 的tcp_fin_timeout默认 60 秒而rtmpServer-master的StreamStorage默认streamTimeout 30s。当推流端如 IPC因网络抖动暂停发送超过 30 秒服务端主动 close channel但 TCP FIN 包未被 ACK导致内核重传 FIN 失败后强制 reset。解决调大application.conf中rtmp.stream.timeout 90s同步调整 Linux 参数echo 120 /proc/sys/net/ipv4/tcp_fin_timeout关键在RtmpStreamHandler.channelInactive()中添加ctx.close()前的日志打点确认是服务端主动关闭还是对端异常断开。4.2 现象多路推流时 CPU 暴涨至 95%top显示java进程占满单核原因MemoryStreamStorage使用ConcurrentLinkedQueue存储待消费的RtmpMessage但RtmpStreamHandler的read()方法未做批处理每次只取 1 条 message 进行编码转发导致高频锁竞争。解决改造MemoryStreamStorage.poll()为批量获取public ListRtmpMessage pollBatch(int maxCount) { ListRtmpMessage batch new ArrayList(maxCount); for (int i 0; i maxCount !queue.isEmpty(); i) { RtmpMessage msg queue.poll(); if (msg ! null) batch.add(msg); } return batch; }在RtmpStreamHandler中调用pollBatch(32)替代单条poll()设置 JVM 参数-XX:UseG1GC -XX:MaxGCPauseMillis200避免 GC 导致消息积压。4.3 现象VLC 播放首帧黑屏 3 秒之后正常原因RtmpServer-master默认不发送onMetaData而 VLC 依赖此信息初始化解码器。缺少duration、width、height、framerate等字段导致解码器等待超时。解决在RtmpStreamHandler.publishStart()后立即构造并发送onMetaDataAmfObject metaData new AmfObject(); metaData.put(duration, 0.0); // live stream metaData.put(width, 1280.0); metaData.put(height, 720.0); metaData.put(framerate, 25.0); metaData.put(videocodecid, avc1); metaData.put(audiocodecid, mp4a); // ... 其他必要字段 ctx.writeAndFlush(new RtmpNotifyMessage(onMetaData, metaData));注意videocodecid必须与实际推流的 codec id 一致H.264 为avc1H.265 为hvc1否则 VLC 拒绝解码。4.4 现象同一stream-name被重复推流旧流未被踢出新流无法播放原因StreamStorage的registerStream()方法未做冲突检测直接覆盖ConcurrentHashMap中的 key。旧流的ChannelHandlerContext仍持有引用但RtmpStreamHandler已失去对其控制。解决在registerStream()中添加踢出逻辑public void registerStream(String streamKey, RtmpStream stream) { RtmpStream old streams.put(streamKey, stream); if (old ! null old.getChannel() ! null old.getChannel().isActive()) { old.getChannel().close(); // 主动关闭旧连接 logger.warn(Stream {} replaced by new connection, streamKey); } }血泪经验必须close()而非disconnect()后者不触发channelInactive()事件。4.5 现象使用ffmpeg -re -i input.mp4 -f flv rtmp://...推流服务端日志疯狂打印AMF decode error原因FFmpeg 的-re模式会严格按原始帧率发送但rtmpServer-master的RtmpDecoder对timestamp的单调递增校验过于激进——当输入 MP4 的dts有轻微抖动如 123456, 123458, 123457校验失败直接抛异常。解决修改RtmpMessageDecoder.decodeTimestamp()放宽校验if (Math.abs(timestamp - lastTimestamp) 1000) { // 允许 1 秒内跳变 logger.debug(Timestamp jump detected: {} - {}, ignored, lastTimestamp, timestamp); timestamp lastTimestamp 40; // 伪补帧保持 25fps } lastTimestamp timestamp;警告此修改仅用于测试文件推流生产环境 IPC 推流必须关闭此宽松模式。5. 性能压测与监控用 3 个命令摸清你的rtmpServer-master真实吞吐边界能跑通不等于能扛住。真正的交付标准是在目标硬件如 Intel NUC i3 8GB RAM上稳定支撑 200 路 720p25fps H.264 推流CPU 70%内存 3GB无丢帧。以下方法不依赖 Grafana 或 Prometheus用原生命令就能拿到可信数据。5.1 实时连接数与流数监控netstatjstack黄金组合不要信application.conf里的max.connections要看真实占用# 查看 ESTABLISHED 连接数排除 TIME_WAIT netstat -an | grep :1935 | grep ESTABLISHED | wc -l # 查看每个连接对应的 Java 线程确认是否线程池耗尽 jstack $(pgrep -f RtmpServerApplication) | grep nioEventLoopGroup | wc -l # 查看当前注册的流数量直接读内存状态 jmap -histo $(pgrep -f RtmpServerApplication) | grep RtmpStream解读若netstat结果接近max.connections但jmap显示RtmpStream实例远少于连接数说明大量连接未完成 publish卡在 handshake 阶段——检查RtmpHandshakeHandler是否阻塞若nioEventLoopGroup线程数达2 * CPU cores且jstack中大量线程处于RUNNABLE状态说明 Netty EventLoop 过载需调大bossGroup和workerGroup线程数application.conf中netty.boss.threads 2,netty.worker.threads 16。5.2 帧率与丢包率抓取Wireshark 过滤 Python 脚本分析用 Wireshark 抓rtmp流导出为rtmp.pcapng然后用脚本统计关键指标# analyze_rtmp.py import pyshark cap pyshark.FileCapture(rtmp.pcapng, display_filterrtmp.msg_type 9) # video data timestamps [] for pkt in cap: try: ts int(pkt.rtmp.timestamp) timestamps.append(ts) except: continue # 计算 FPS每秒帧数 import numpy as np diffs np.diff(timestamps) fps 1000 / np.mean(diffs[diffs 0]) # ms to sec loss_rate 1 - len(timestamps) / (max(timestamps) - min(timestamps)) * 1000 / 40 # 40ms per frame 25fps print(fAverage FPS: {fps:.1f}, Loss Rate: {loss_rate:.2f}%)参数说明rtmp.msg_type 9过滤视频帧10 为音频40ms是 25fps 的理论帧间隔loss_rate计算基于时间窗口内应有帧数 vs 实际帧数若loss_rate 2%优先检查MemoryStreamStorage的queue.size()是否持续 1000 —— 这意味着消费者拉流端跟不上生产者推流端。5.3 内存泄漏定位jmapjhat三步法rtmpServer-master最隐蔽的坑是ByteBuf泄漏。症状运行 24 小时后Used Heap从 500MB 涨到 2.8GBFull GC 频繁。# 1. 生成堆转储 jmap -dump:formatb,fileheap.hprof $(pgrep -f RtmpServerApplication) # 2. 启动 jhat 分析JDK8 自带 jhat -J-Xmx4g heap.hprof # -J-Xmx4g 防止 jhat 自身 OOM # 3. 浏览 http://localhost:7000搜索 io.netty.buffer重点关注 # - PooledUnsafeDirectByteBuf 实例数是否持续增长 # - 是否存在大量 io.netty.util.ResourceLeakDetector$DefaultResourceLeak 报告根治方案所有ByteBuf的readBytes()、writeBytes()操作后必须调用buf.release()在RtmpDecoder和RtmpEncoder的encode()方法末尾添加if (buf.refCnt() 0) buf.release()终极保险在 JVM 启动参数中加入-Dio.netty.leakDetection.levelparanoid让 Netty 在每次ByteBuf未释放时打印完整调用栈。我坚持在每个ChannelHandler的exceptionCaught()里加一行logger.error(Unexpected exception, cause)并确保cause.printStackTrace()不被注释掉——因为 90% 的线上故障第一次报错日志里就藏着答案只是没人去看。rtmpServer-master不是银弹但它给你的是协议栈完全透明的掌控感。当客户说“你们的推流服务器不如 SRS 稳定”时你不用背锅可以直接打开RtmpDecoder.java指着第 137 行说“这里少了个release()我 5 分钟修好。” 希望帮到你。本文还有配套的精品资源点击获取