环形缓冲区应用实例

2,810 阅读5分钟

一、环形缓冲区的介绍

如果您对CPU缓冲区或者对环形缓冲区有一定了解,您可以直接跳到第二章阅读使用场景

来自维基百科
(图来自:来自维基百科)

概念(来自维基百科):圆形缓冲区(circular buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),环形缓冲区(ring buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流

用法(来自维基百科):圆形缓冲区的一个有用特性是:当一个数据元素被用掉后,其余数据元素不需要移动其存储位置。相反,一个非圆形缓冲区(例如一个普通的队列)在用掉一个数据元素后,其余数据元素需要向前搬移。换句话说,圆形缓冲区适合实现先进先出缓冲区,而非圆形缓冲区适合后进先出缓冲区。 圆形缓冲区适合于事先明确了缓冲区的最大容量的情形。扩展一个圆形缓冲区的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。 写操作覆盖圆形缓冲区中未被处理的数据在某些情况下是允许的。特别是在多媒体处理时。例如,音频的生产者可以覆盖掉声卡尚未来得及处理的音频数据。

工作过程(来自维基百科):圆形缓冲区(circular buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),环形缓冲区(ring buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流

圆形缓冲区工作机制(来自维基百科):

二、业务场景

1. 系统功能说明

我们的系统需要操作多台终端并且实时监听终端的健康状态

2. 如何实现

我们让服务端与终端都建立了一个socket通讯,让服务端和各个终端保持了长连接,这让我们能实现服务端能够操作终端的同时能够接收到终端的健康状态报文

3.生产过程中的问题

1.终端的健康报文数据过大,socket自动的分包,拆包,服务端常常出现异常,无法及时正确获取终端的健康状态

三、解决方案

1. 问题分析

服务端与终端之间其实是一个长时间的数据传输,这让我们想到了流,在我们面临大文件处理时,我们首先想到的就是缓冲,这和cpu的缓冲层有些相似,cpu的缓冲层会默认一个大小,一边一直读取,一边一直写入,如果缓冲层到达最大限制,那么就会进入阻塞。这个思想和我们的业务相似,终端一直发起,服务端一直接受,换个说法就是,终端一直写入,服务端一直读取。

2. 如何实现

我们借鉴了环形缓冲区的思想,使用了一个1024大小的数组,一个线程一直从数组中读取,另外一个线程一直写入。从而保证无论socket是否把我们的报文拆分,我们都能确保这个报文是完整的可使用的。

环形缓冲区代码如下(go语言):


import (
	"errors"
	"io"
	"time"
)

type CircleByteBuffer struct {
	io.Reader
	io.Writer
	io.Closer
	datas []byte

	start   int
	end     int
	size    int
	isClose bool
	isEnd   bool
}

func NewCircleByteBuffer(len int) *CircleByteBuffer {
	var e = new(CircleByteBuffer)
	e.datas = make([]byte, len)
	e.start = 0
	e.end = 0
	e.size = len
	e.isClose = false
	e.isEnd = false
	return e
}

func (e *CircleByteBuffer) GetLen() int {
	if e.start == e.end {
		return 0
	} else if e.start < e.end {
		return e.end - e.start
	} else {
		return e.size - (e.start - e.end)
	}
}
func (e *CircleByteBuffer) GetFree() int {
	return e.size - e.GetLen()
}
func (e *CircleByteBuffer) Clear() {
	e.start = 0
	e.end = 0
}
func (e *CircleByteBuffer) PutByte(b byte) error {
	if e.isClose {
		return io.EOF
	}
	e.datas[e.end] = b
	var pos = e.end + 1
	if pos == e.size {
		pos = 0
	}
	for pos == e.start {
		if e.isClose {
			return io.EOF
		}
		time.Sleep(time.Millisecond)
	}
	e.end = pos
	return nil
}

func (e *CircleByteBuffer) GetByte() (byte, error) {
	if e.isClose {
		return 0, io.EOF
	}
	for e.GetLen() <= 0 {
		if e.isClose || e.isEnd {
			return 0, io.EOF
		}
		time.Sleep(time.Millisecond)
	}
	var ret = e.datas[e.start]
	pos := e.start + 1
	if pos == e.size {
		pos = 0
	}
	e.start = pos
	return ret, nil
}
func (e *CircleByteBuffer) Geti(i int) byte {
	if i >= e.GetLen() {
		panic("out buffer")
	}
	var pos = e.start + i
	if pos >= e.size {
		pos -= e.size
	}
	return e.datas[pos]
}

/*func (e*CircleByteBuffer)puts(bts []byte){
	for i:=0;i<len(bts);i++{
		e.put(bts[i])
	}
}
func (e*CircleByteBuffer)gets(bts []byte)int{
	if bts==nil {return 0}
	var ret=0
	for i:=0;i<len(bts);i++{
		if e.GetLen()<=0{break}
		bts[i]=e.get()
		ret++
	}
	return ret
}*/
func (e *CircleByteBuffer) Close() error {
	e.isClose = true
	return nil
}
func (e *CircleByteBuffer) Read(bts []byte) (int, error) {
	if e.isClose {
		return 0, io.EOF
	}
	if bts == nil {
		return 0, errors.New("bts is nil")
	}
	var ret = 0
	for i := 0; i < len(bts); i++ {
		b, err := e.GetByte()
		if err != nil {
			return ret, err
		}
		bts[i] = b
		ret++
	}
	if e.isClose {
		return ret, io.EOF
	}
	return ret, nil
}
func (e *CircleByteBuffer) Write(bts []byte) (int, error) {
	if e.isClose {
		return 0, io.EOF
	}
	if bts == nil {
		e.isEnd = true
		return 0, io.EOF
	}
	var ret = 0
	for i := 0; i < len(bts); i++ {
		err := e.PutByte(bts[i])
		if err != nil {
			return ret, err
		}
		ret++
	}
	if e.isClose {
		return ret, io.EOF
	}
	return ret, nil
} 

3.总结

我们在解决业务需求时,可能会受限于当时的技术水平或者其他因素,做出的选择在现在看来并不是最优,但想想看,在这个轮子横飞的年代,我相信一个会造轮子的人定会比只能使用轮子的人更加优秀,他们敲下的每一行代码对程序的影响他们都了然于心,他们会更加及时的发现问题,更加完善的解决问题。

我们专注于真实案例挖掘并写下这系列的文章,我希望能让大家保持思考,保持一颗热爱编码的心,在借鉴这些案例时能够蹦出灵感的火花,那是我最开心的事

文章首发于微信公众号:java企业级应用(WaXiData)