序:流媒体王国的邮局
在 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,用来配对请求和响应
}
故事解读:
每封命令信,开头都必须写两样东西:
- CommandName:你要干什么?("connect"、"play"、"publish")
- 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 能读懂的"信笺模板"。