Go语言之pion/ion-sfu中的Buffer源码分析

275 阅读3分钟

「这是我参与2022首次更文挑战的第36天,活动详情查看:2022首次更文挑战

bucket

结构分析:

Bucket是结构, NewBucket是构造,其他都是非对暴露的.

其他内部功能函数 get/set是为addPacket/getPacket/push提供功能.

	type Bucket struct {
		buf    []byte
		nacker *nackQueue

		headSN   uint16
		step     int
		maxSteps int

		onLost func(nack []rtcp.NackPair, askKeyframe bool)
	}

从后面的分析也可以看出,buf里存放的是rtp包的数据.

	func NewBucket(buf []byte, nack bool) *Bucket {
		b := &Bucket{
			buf:      buf,
			maxSteps: int(math.Floor(float64(len(buf))/float64(maxPktSize))) - 1,
		}
		if nack {
			b.nacker = newNACKQueue()
		}
		return b
	}

这个构造里也非常有意思: step表示此桶Bucket放了多少rtp包,maxSteps表示最多可以放多少rtp包, step只在每次push包时增加,最有趣的时step和maxSteps都是以索引方式表示. 再回头看构造时的默认处理: maxSteps有各减1操作,step直接初始化为0, 真是精致.

	func (b *Bucket) push(pkt []byte) []byte {
		binary.BigEndian.PutUint16(b.buf[b.step*maxPktSize:], uint16(len(pkt)))
		off := b.step*maxPktSize + 2
		copy(b.buf[off:], pkt)
		b.step++
		if b.step >= b.maxSteps {
			b.step = 0
		}
		return b.buf[off : off+len(pkt)]
	}

从这里可以看出,缓冲Bucket.buf是被分成了几段,每一段都足够放一个rtp包, 每段的存储方式是:前两字节存rtp包的长度,之后才是rtp包的数据. 一旦所有的段都存放了,step就会重置,下一次push就会替换第一各段. 有点"元素固定长度+环队"的意思. push中还有一个点:step指向的是下一个rtp包要放的位置:push中是先拷贝后自增的.

push返回的是缓冲中,rtp包数据.

	func (b *Bucket) set(sn uint16, pkt []byte) []byte {

		// 这一小段需要单独说一下:
		// step是自己维护的环队
		// rtp是按sn连续地摆在这个环队中
		// 如果有丢失,step也会自增
		// step指向的是headSN的下一个环队段索引
		// 所以在计算pos时,step-1表示headSN的索引,之后在计算sn的偏移
		pos := b.step - int(b.headSN-sn+1)
		if pos < 0 {
			// maxSteps也是一个索引,所以maxSteps+1才是真的段数量
			// 这里的+1和计算pos是的+1并不是同一个逻辑
			pos = b.maxSteps + pos + 1
		}

		off := pos * maxPktSize

		// 保证不set的sn在一定范围内
		// get()函数使用另一种方式在实现
		if off > len(b.buf) || off < 0 {
			return nil
		}
		binary.BigEndian.PutUint16(b.buf[off:], uint16(len(pkt)))
		copy(b.buf[off+2:], pkt)
		return b.buf[off+2 : off+2+len(pkt)]
	}

set是直接在缓冲中替换一个rtp的数据:

headSN表示最近添加到缓冲中的rtp包sn号,在获取环队的段地址时, 非常有意思,具体可以看上面的注释.

另外,对与同一个逻辑,在不同地方,开源贡献者都在秀她的技术. 开源就是程序员自己表达自己,宣泄自己的地方.

set里的具体逻辑还是前2字节放长度,后面跟rtp包数据.

	func (b *Bucket) addPacket(pkt []byte, sn uint16, latest bool) []byte {

		// 如果是前面的rtp包
		if !latest {
			if b.nacker != nil {
				b.nacker.remove(sn)
			}
			return b.set(sn, pkt)
		}

		// 如果是后面的rtp包
		diff := sn - b.headSN
		b.headSN = sn
		for i := uint16(1); i < diff; i++ {

			// 中间缺失都被认为是丢失的
			b.step++
			if b.nacker != nil {
				b.nacker.push(sn - i)
			}
			if b.step >= b.maxSteps {
				b.step = 0
			}
		}

		// 每次处理rtp包,都触发onLost处理
		if b.nacker != nil {
			np, akf := b.nacker.pairs()
			if len(np) > 0 {
				b.onLost(np, akf)
			}
		}
		return b.push(pkt)
	}

addPacket里面封装了几个逻辑:

  • rtp包的乱序处理,新包用push,其他用set
  • 新包到上次标记的最大包之间的,全部视为丢包
  • 每次处理rtp包,都会触发对丢包的处理

看完了添加,再看看获取:

	func (b *Bucket) get(sn uint16) []byte {
		pos := b.step - int(b.headSN-sn+1)
		if pos < 0 {
			if pos*-1 > b.maxSteps {
				return nil
			}
			pos = b.maxSteps + pos + 1
		}
		off := pos * maxPktSize
		if off > len(b.buf) {
			return nil
		}
		if binary.BigEndian.Uint16(b.buf[off+4:off+6]) != sn {
			return nil
		}
		sz := int(binary.BigEndian.Uint16(b.buf[off : off+2]))
		return b.buf[off+2 : off+2+sz]
	}

在get中,sn首先要转换成环队的具体段,也就是上面的pos, rtp包的2-4字节是序号.需要澄清一点:rtp的序号是16位,rtcp的序号是15位. 在get中还会验证序号sn是否正确,之后再去rtp包数据.

	func (b *Bucket) getPacket(buf []byte, sn uint16) (i int, err error) {
		p := b.get(sn)
		if p == nil {
			err = errPacketNotFound
			return
		}
		i = len(p)
		if cap(buf) < i {
			err = errBufferTooSmall
			return
		}
		if len(buf) < i {
			buf = buf[:i]
		}
		copy(buf, p)
		return
	}

getPacket仅仅是将get出来的拷贝到一个缓冲.

总结

总体来说,Bucket使用环队来存rtp包数据,使用nackQueue来维护丢包队列. 对外提供的功能函数是addPacket/getPacket,分别用于读和写.

最后的扩展时对nack队列的处理onLost.这个在Buffer中有,到时候再分析这个.