如果把直播服务器比作一座电视台,那
NetConnection就是观众或主播刚拨进来的那根电话线。电话一通,后面才有“我要看频道”“我要开播”“ping 一下你还活着吗”。
这份 net-connection.go,讲的就是:一根 TCP 连接,如何变成一条 RTMP 会话。
一、开场:NetConnection 是谁?
在代码里,NetConnection 不是“网络连接”这么朴素。
type NetConnection struct {
task.Job
*util.BufReader
net.Conn
...
}
它同时是三样东西:
- 一个任务(Job):能被调度、能跑定时任务;
- 一个带缓冲的读器(BufReader):解决“TCP 是流,RTMP 是块”的矛盾;
- 一个 net.Conn:真正的 socket。
重点来了:
TCP 给你字节流,RTMP 给你“消息 + 分块(chunk)”。
NetConnection 的核心使命,就是在这两者之间当翻译。
二、初始化:给连接“备齐家当”
func (nc *NetConnection) Init(conn net.Conn) {
nc.Conn = conn
nc.BufReader = util.NewBufReader(conn)
nc.BufReader.SetTimeout(time.Second * 30)
nc.ReadChunkSize = RTMP_DEFAULT_CHUNK_SIZE
nc.WriteChunkSize = RTMP_DEFAULT_CHUNK_SIZE
nc.incommingChunks = make(map[uint32]*Chunk)
nc.mediaDataPool = gomem.NewScalableMemoryAllocator(...)
}
这里有几个“伏笔”:
incommingChunks:
RTMP 一个流会被切成很多 chunk,同一个 CSID(Chunk Stream ID)要拼起来。mediaDataPool:
音视频数据很大,不能随便make([]byte)。ReadChunkSize / WriteChunkSize:
后面客户端可能会改,这是“协商出来的规矩”。
难点预警:
RTMP 不是“一个消息 = 一次 read”。
一个消息可能是:
- 1 个 chunk
- 100 个 chunk
- 一个 chunk 里塞半条消息
三、Chunk:RTMP 的灵魂(也是噩梦)
1. 先读 1 个字节
head, err := nc.ReadByte()
ChunkStreamID := uint32(head & 0x3f)
ChunkType := head >> 6
这一行信息密度极高:
| 2 bit | 6 bit |
| fmt | chunk stream id |
- fmt = 0/1/2/3:四种 header 压缩方式
- csid:逻辑通道(不是 TCP 端口)
2. CSID 还能“续命”
case 0:
chunkStreamID = 64 + uint32(u8)
case 1:
chunkStreamID = 64 + uint32(u16_0) + (uint32(u16_1) << 8)
这就是 RTMP 的“变长 CSID”:
- 0 → 用 1 个字节扩展
- 1 → 用 2 个字节扩展
- 2 → 保留
- 3~63 → 直接用
亮点:
用极小的头,支持大量流。这是 RTMP 能在 2003 年跑起来的关键。
四、Chunk Header:四种格式,一种耐心
func (nc *NetConnection) readChunkType(h *ChunkHeader, chunkType byte) (err error)
Chunk Type 速记表
| Type | 含义 |
|---|---|
| 0 | 完整头(绝对时间戳) |
| 1 | 有时间 + 长度 + 类型 |
| 2 | 只有时间 |
| 3 | 啥都没有(继续上一次) |
最阴险的是 type 3:
“我和上一个 chunk 一模一样,只是数据接着来。”
如果你不懂“状态机”,这里一定会写崩。
而这份代码的做法是:
chunk, ok := nc.incommingChunks[ChunkStreamID]
if !ok {
chunk = &Chunk{}
nc.incommingChunks[ChunkStreamID] = chunk
}
把“流”变成对象
把“分片”变成状态
这是整个文件最值钱的设计。
五、音视频:不是“收下来”,而是“长出来”
这是最惊艳的一段:
case RTMP_MSG_AUDIO:
if writer, ok := nc.Writers[chunk.MessageStreamID]; ok {
if writer.PubAudio {
chunk.buf = writer.AudioFrame.NextN(msgLen)
}
}
一般 RTMP 实现:
读 buffer → copy → 解析 → 再 copy → 推给播放器
这份代码:
直接从“帧对象”里拿内存,让 socket 数据直接长进帧里
结果:
- 少一次拷贝
- GC 压力骤降
- 音视频帧和 chunk 天然对齐
mediaDataPool 在这里不是“优化”,是架构本身。
六、消息拼好了,然后呢?
if chunk.bufLen == msgLen {
switch chunk.MessageTypeID {
case RTMP_MSG_AUDIO:
writer.AudioFrame.SetTS32(...)
err = writer.NextAudio()
}
}
重点不是“收到了”,而是:
“这一帧现在可以对外宣布了。”
RTMP 的难点从来不是协议,而是:
- 时间戳怎么算
- 扩展时间戳怎么补
- 多流并发怎么不串
这里用:
chunk.ExtendTimestamp += delta
把“相对时间”悄悄累积成“绝对时间”。
亮点:
没有大段 if-else 算时间,而是让 Chunk 自己记住历史。
七、RecvMessage:一个大循环,一场 dispatcher
for err == nil {
if msg, err = nc.readChunk(); msg != nil ...
}
像不像:
邮局分拣中心:
包裹不断来,拆包 → 看类型 → 分发
它处理了什么?
- 改 chunk size
- 删半截流(abort)
- 对端带宽
- ping / pong
- AMF0 命令
其中最像“人话”的,是这段:
case RTMP_USER_PING_REQUEST:
nc.SendUserControl(RTMP_USER_PING_RESPONSE)
翻译一下:
“你还活着吗?”
“在。”
八、AMF:用“脚本语言思维”写协议
RTMP 命令不是 protobuf,也不是 JSON。
是 AMF:
02 00 07 connect
00 3F F0 00 00 00 00 00 00
代码里:
cmd := amf.ReadShortString()
cmdMsg := CommandMessage{
cmd,
uint64(amf.ReadNumber()),
}
像什么?
像 JS 解释器:
- 第一个参数是函数名
- 第二个是事务 ID
- 后面是对象 / 字符串 / 数字 / 布尔
命令全景:
| 命令 | 含义 |
|---|---|
| connect | 连上来 |
| createStream | 开频道 |
| publish | 开播 |
| play | 观看 |
| pause / seek | 播放控制 |
| deleteStream | 拜拜 |
难点:
AMF 是“弱类型协议 + 强约定语义”。
你读错一个字段,OBS 就不会推流,但 TCP 一切正常。
九、发送:比“写 socket”难得多
func (nc *NetConnection) SendMessage(t byte, msg RtmpMessage) (err error)
亮点有三:
1️:写锁不是 mutex,而是原子自旋
for !nc.writing.CompareAndSwap(false, true) {
runtime.Gosched()
}
defer nc.writing.Store(false)
为什么?
- RTMP 消息不能“半个出去”
- Go 调度器友好
- 高并发下比锁更轻
AMF0 / AMF3 自动切换
if nc.ObjectEncoding == 0 {
msg.Encode(&nc.tmpBuf)
} else {
amf3 := AMF3{AMF: nc.tmpBuf}
}
一份接口,两套编码。
Chunk 发送是“拼 Buffers”
nc.sendBuffers = append(nc.sendBuffers, nc.chunkHeaderBuf)
...
nc.sendBuffers = append(nc.sendBuffers, buf)
用 net.Buffers:
- 少系统调用
- 零拷贝友好
- 大视频帧也不怕
这是“能扛直播流量”的关键。
十、PingTask:让连接“有心跳”
type PingTask struct {
task.TickTask
NetConnection *NetConnection
}
func (t *PingTask) GetTickInterval() time.Duration {
return time.Second * 10
}
每 10 秒问一次:
“你还活着吗?”
如果 RTMP 没有 ping:
- NAT 把你忘了
- 防火墙把你丢了
- 对端死了你还在等
小代码,大稳定性。
十一、这份代码的“魂”是什么?
如果只记三句话:
重点:
RTMP = TCP 流 + Chunk 状态机 + AMF 命令
难点:
时间戳、CSID、分片重组、弱类型命令解析
亮点:
音视频内存从“帧里长出来”,不是“从 socket 拷出来”
十二、结尾:一根线,一座台
当第一个 connect 进来:
- 没人知道后面是主播还是观众
- 没人知道是 OBS、FFmpeg 还是浏览器
- 但
NetConnection不在乎
它只做一件事:
把混乱的字节,变成有序的流;把陌生的连接,变成可管理的会话
这就是为什么--
直播能“看起来很简单”,
是因为有人把复杂,藏进了
readChunk。