RTMP 王国的“信笺百科全书”

序:流媒体王国的邮局

在 Monibuca 流媒体服务器的世界里,有一个叫 RTMP 邮局​ 的地方。

这个邮局不送快递,只处理一种东西:消息(Message)。

而 msg.go 就是邮局里那本 《信笺分类大典》------它规定了:

  • 信有哪几种?
  • 每封信的信封怎么写?
  • 信里第一句话是什么?
  • 怎么把信塞进信封(编码)?

没有这本大典,整个 RTMP 插件就会变成一座失语的城堡。

第一:信笺的"身份证号"------常量定义

打开msg.go ,最醒目的就是一堆常量。它们不是随便写的数字,而是 RTMP 协议的身份证号。

go 复制代码
const (
    RTMP_MSG_CHUNK_SIZE = 1
    RTMP_MSG_ABORT      = 2
    RTMP_MSG_ACK        = 3
    RTMP_MSG_USER_CONTROL  = 4
    RTMP_MSG_ACK_SIZE      = 5
    RTMP_MSG_BANDWIDTH     = 6
    RTMP_MSG_AUDIO         = 8
    RTMP_MSG_VIDEO         = 9
    RTMP_MSG_AMF0_COMMAND  = 20
    // ...
)

故事解读

想象你是一个邮差,手里拿到一封信。信封上印着一个数字:

数字 含义 邮差的反应
8 音频信 "哦,这是声音,送去音频处理科"
9 视频信 "这是画面,送去视频处理科"
20 命令信 "这是指令,送去控制室"
4 用户控制信 "这是系统广播,送去事件中心"

重点 :这些数字不是 Monibuca 发明的,它们来自 Adobe 的 RTMP 官方规范。Monibuca 只是把它们翻译成了 Go 常量,让代码可读性更高。

难点提示 :注意 RTMP_MSG_AMF0_COMMAND = 20 和 RTMP_MSG_AMF3_COMMAND = 17 的区别。AMF0 和 AMF3 是两种不同的序列化格式,就像中文和英文写信。Monibuca v5 主要用 AMF0,所以你看到 CommandMessage 默认走的是 AMF0 编码。

第二:信使的"专用通道"------CSID 映射

继续往下看,你会看到一组"奇怪"的常量:

go 复制代码
const (
    RTMP_CSID_CONTROL = 0x02
    RTMP_CSID_COMMAND = 0x03
    RTMP_CSID_AUDIO   = 0x06
    RTMP_CSID_DATA    = 0x05
    RTMP_CSID_VIDEO   = 0x05
)

故事解读

RTMP 世界里,信不是随便丢到马路上的。每种信有自己专用的 Chunk Stream(传送带)。

  • 0x02:控制通道------专门跑系统控制信(如 Set Chunk Size)
  • 0x03:命令通道------专门跑 connect、publish 等指令
  • 0x05:数据通道------跑元数据
  • 0x06:音频通道------跑音频数据

重点 :newChunkHeader 函数就用了这些常量:

go 复制代码
func newChunkHeader(messageType byte) *ChunkHeader {
    head := new(ChunkHeader)
    head.ChunkStreamID = RTMP_CSID_CONTROL
    if messageType == RTMP_MSG_AMF0_COMMAND {
        head.ChunkStreamID = RTMP_CSID_COMMAND
    }
    head.MessageTypeID = messageType
    return head
}

这就像邮局的分拣机:

  • 默认把信放到 2号传送带(控制通道)
  • 但如果发现是命令信(type 20),就自动切换到 3号传送带

难点 :为什么 RTMP_CSID_VIDEO 和 RTMP_CSID_DATA 都是 0x05?

因为 RTMP 规范里,视频和元数据在早期版本共用同一个 CSID。这不是 bug,是历史遗留。实际使用中,视频数据会通过 Message Type ID(9)来区分,不会和数据消息混淆。

第三:信的"骨架"------接口与基础结构

3.1 信封接口

go 复制代码
type RtmpMessage interface {
    Encode(IAMF)
}

这是所有消息的"最高宪法":

任何 RTMP 消息,都必须能把自己编码进一个 AMF 缓冲区。

IAMF 是一个接口,代表"我能写 AMF 格式数据"。所以 Encode 方法就是:

"把我这封信的内容,按照 AMF 格式写进信封。"

3.2 命令信的"标准抬头"

go 复制代码
type CommandMessage struct {
    CommandName   string // 命令名,如 "connect"
    TransactionId uint64 // 事务ID,用来配对请求和响应
}

故事解读:

每封命令信,开头都必须写两样东西:

  1. CommandName:你要干什么?("connect"、"play"、"publish")
  2. TransactionId:这是第几次对话?(用来匹配"我问你答")

就像你打电话给客服:

  • "你好,我要查账单"(CommandName = "查账单")
  • "工单号 12345"(TransactionId = 12345)

服务端回信时,也会带上同样的 TransactionId,这样你就知道"哦,这是对我那次查询的回复"。

重点 :TransactionId 在 RTMP 里非常关键。客户端每发一个命令,TransactionId 递增。服务端响应时原样返回,客户端据此找到对应的回调函数。

第四:命令信的"家族谱"

msg.go 里定义了大量命令消息。它们都继承自 CommandMessage,形成一个家族树。

4.1 客户端 → 服务器(请求)

消息类型 故事角色 作用
CallMessage 远程调用使者 执行服务器端的 RPC 函数
PlayMessage 观众 "我要看这个流"
PublishMessage 主播 "我要推这个流"
PauseMessage 暂停按钮 "暂停/继续播放"
SeekMessage 进度条 "跳到这个时间戳"
CURDStreamMessage 流管家 创建/删除流

重点示例:PublishMessage

go 复制代码
type PublishMessage struct {
    CURDStreamMessage
    PublishingName string  // 流名,如 "live/stream1"
    PublishingType string  // "live"、"record" 或 "append"
}

故事:一个主播走进邮局说:

"我要发布一个叫 live/stream1 的流,类型是 live(实时直播)。"

邮局登记后,所有观众就可以用 PlayMessage 来"订阅"这个流。

难点 :PublishingType 的三种模式:

  • live:纯直播,不存文件
  • record:录制到新文件,如果文件存在就覆盖
  • append:追加到已有文件末尾

这决定了服务器端是只转发、还是同时写磁盘。

4.2 服务器 → 客户端(响应)

消息类型 故事角色 作用
ResponseConnectMessage 门卫 "连接成功/失败,这是服务器信息"
ResponseCreateStreamMessage 发号员 "这是你的 StreamID"
ResponsePlayMessage 放映员 "开始播放"
ResponsePublishMessage 确认回执 "发布成功,状态码 xxx"

重点 :注意 ResponseCreateStreamMessage 的 Encode 方法:

go 复制代码
func (msg *ResponseCreateStreamMessage) Encode(buf IAMF) {
    buf.Marshals(msg.CommandName, msg.TransactionId, nil, msg.StreamId)
}

第三个参数是 nil(命令对象为空),第四个参数是 StreamId。

这对应 RTMP 规范:_result 响应的格式是 [命令名, 事务ID, null, 流ID]。

难点 :为什么命令对象是 nil?

因为 _result 响应不需要携带额外的命令对象,只需要告诉客户端"你的流ID是 xxx"。而 ResponseConnectMessage 的 Encode 里带了 Properties 和 Infomation 两个 map,因为 connect 响应需要返回服务器版本、能力等信息。

第五:协议控制信------邮局的"内部规章"

这些消息不走普通通道,而是走 CSID=2 的控制通道。

5.1 Set Chunk Size(消息类型 1)

go 复制代码
type Uint32Message uint32

func (msg Uint32Message) Encode(buf IAMF) {
    binary.BigEndian.PutUint32(buf.GetBuffer().Malloc(4), uint32(msg))
}

故事:邮局经理说:"以后每个包裹最大 65536 字节。"

这是一个 4 字节的大端整数,直接写入缓冲区。

重点 :Uint32Message 是一个 类型别名 ,不是结构体。它直接把 uint32 值编码成 4 字节网络字节序。简单粗暴。

5.2 Set Peer Bandwidth(消息类型 6)

go 复制代码
type SetPeerBandwidthMessage struct {
    AcknowledgementWindowsize uint32
    LimitType                 byte
}

故事:客户端对服务器说:"我的接收窗口是 5000000 字节,限制类型是 Hard(0)。"

服务器收到后,就会控制发送速度,避免把客户端压垮。

难点 :LimitType 的三种值:

  • 0 (Hard):硬限制,必须严格遵守
  • 1 (Soft):软限制,可以超过
  • 2 (Dynamic):动态,结合 Hard 和 Soft

第六:User Control Message------邮局的"广播喇叭"

go 复制代码
type UserControlMessage uint16

这只是一个 事件类型​ 的容器。真正的消息是它的一堆"子类":

事件类型 常量 故事
Stream Begin 0 "3号流准备好了!"
Stream EOF 1 "3号流结束了"
Set Buffer Length 3 "我的播放缓冲区是 3000ms"
Ping Request 6 "你还活着吗?"
Ping Response 7 "我还活着!"

重点 :UserControlMessage 的 Encode 方法只写 2 字节的事件类型:

go 复制代码
func (msg UserControlMessage) Encode(buf IAMF) {
    buf.GetBuffer().WriteUint16(uint16(msg))
}

子类(如 StreamIDMessage)会先调用父类的 Encode 写事件类型,再写自己的数据:

go 复制代码
func (msg StreamIDMessage) Encode(buf IAMF) {
    msg.UserControlMessage.Encode(buf)  // 写事件类型 0
    binary.BigEndian.PutUint32(buf.GetBuffer().Malloc(4), msg.StreamID) // 写流ID
}

难点 :这是一种 组合+继承 ​ 的编码模式。Go 没有 extends,所以用"结构体嵌入"模拟继承。StreamIDMessage 嵌入了 UserControlMessage,调用 msg.UserControlMessage.Encode(buf) 就是"先写父类的部分"。

第七:Metadata------流的"身份证信息"

go 复制代码
type MetadataMessage struct {
    Proterties map[string]interface{} `json:",omitempty"`
}

故事:主播推流时,会附带一段 JSON 格式的元数据:

bash 复制代码
{
  "width": 1920,
  "height": 1080,
  "framerate": 30,
  "videocodecid": "avc1",
  "audiocodecid": "mp4a"
}

这就是 MetadataMessage。它告诉播放器:"这个流是 1080p、30fps、H264+AAC。"

重点 :map[string]interface{} 意味着值可以是任意类型。编码时,IAMF 接口会遍历 map,把每个键值对按 AMF 格式写入。

第八:难点大汇总------读 msg.go 的"心法"

难点 1:TransactionId 的配对机制

RTMP 是 异步协议。客户端可以连续发多个命令,服务端按任意顺序回复。

靠什么匹配?TransactionId。

go 复制代码
客户端: connect(TransactionId=1)
客户端: createStream(TransactionId=2)
服务端: _result(TransactionId=1, ...)  // 回应 connect
服务端: _result(TransactionId=2, ...)  // 回应 createStream

难点 2:AMF 编码的"隐式约定"

Encode 方法里,buf.Marshals(...) 的调用顺序就是 AMF 编码的顺序。

比如 PlayMessage.Encode:

go 复制代码
buf.Marshals(msg.CommandName, msg.TransactionId, nil, msg.StreamName, -2000)

对应 RTMP 规范:play 命令的格式是:

bash 复制代码
[命令名, 事务ID, 命令对象(null), 流名, 开始位置(-2000=直播)]

**如果顺序错了,对方就解析失败。**​ 这是读代码时最容易踩的坑。

难点 3:CSID 与 Message Type 的关系

  • CSID(Chunk Stream ID):决定信走哪条传送带
  • Message Type ID:决定信的内容类型

它们是两个维度的标识。代码中 newChunkHeader 展示了这种映射关系。

难点 4:User Control Message 的继承链

go 复制代码
UserControlMessage (uint16, 事件类型)
  ├── StreamIDMessage (加 StreamID)
  │     └── SetBufferMessage (再加 Millisecond)
  ├── PingRequestMessage (加 Timestamp)

编码时,子类先调用父类的 Encode,再写自己的字段。这是一种 手动的继承链编码。

终章:msg.go 在 Monibuca 中的位置

bash 复制代码
handshake.go  →  建立连接(三次握手)
chunk.go      →  拆包/组包(把信切成小块)
msg.go        →  定义所有消息类型(本篇主角)
netConnection.go → 处理 connect 等连接级命令
netStream.go      → 处理 play/publish 等流级命令

msg.go 是 纯定义,不含网络 IO 逻辑。它就像一本字典,其他文件负责"查字典"来编码/解码。

附录:快速查阅表

你想知道的 去看哪个结构体
推流怎么发 PublishMessage
拉流怎么发 PlayMessage
连接怎么回 ResponseConnectMessage
流ID怎么分配 ResponseCreateStreamMessage
心跳怎么搞 PingRequestMessage / PingResponseMessage
带宽怎么协商 SetPeerBandwidthMessage
元数据怎么传 MetadataMessage

总结一句话:

msg.go 是 RTMP 协议的"数据结构字典",它用 Go 的 struct 和 interface,把 Adobe 那份晦涩的 RTMP 规范,翻译成了 Monibuca 能读懂的"信笺模板"。

相关推荐
北冥you鱼1 小时前
Go 语言 Channel 机制详解:从原理到实战
开发语言·后端·golang
谢亮_vipxieliang1 小时前
Go 并发控制进阶:sync 包高级原语与 Context 生命周期管理
开发语言·后端·golang
用户9479135811622 小时前
ReAct 到底在循环什么:从零拆解 Agent 的「想一步、做一步」机制
后端
TechLee2 小时前
Go 泛型统一 API 响应设计的最佳实践
后端·架构·go
晴天小庭2 小时前
Sael——基于Jev的AI中转站的安全风控网关,现已开源
人工智能·后端·react.js
知守观2 小时前
一个半天需求干了三天:代码腐化的五个信号与自查命令
java·后端·代码规范
过客123452 小时前
从"发现"到"处置":一个无人值守 AI 闭环的完整拆解(85 天真实数据)
后端·agent·ai编程
黎燃2 小时前
我搭了个“大模型辩论赛“:让 DeepSeek、Qwen、GLM 在蓝耘上吵了一架
后端
墨家句子2 小时前
服务器被反复尝试登录之后:fail2ban 加密钥登录的五道加固
linux·后端