ringbuffer因为它能复用缓冲空间,通常用于网络通信连接的读写,虽然市面上已经有了go写的诸多版本的ringbuffer组件,虽然诸多版本,实现ringbuffer的核心逻辑却是不变的。但发现其内部提供的方法并不能满足我当下的需求,所以还是自己造一个吧。

源码已经上传到github

https://github.com/HobbyBear/ringbuffer

需求分析

我在基于epoll实现一个网络框架时,需要预先定义好的和客户端的通信协议,当从连接读取数据时需要判读当前连接是否拥有完整的协议(实际网络环境中可能完整的协议字节只到达了部分),有才会将数据全部读取出来,然后进行处理,否则就等待下次连接可读时,再判断连接是否具有完整的协议。

由于在读取时需要先判断当前连接是否有完整协议,所以读取时不能移动读指针的位置,因为万一协议不完整的话,下次读取还要从当前的读指针位置开始读取。

所以对于ringbuffer 组件我会实现一个peek方法

func (r *RingBuffer) Peek(readOffsetBack, n int) ([]byte, error)

peek方法两个参数,n代表要读取的字节数, readOffsetBack 代表读取是要在当前读位置偏移的字节数,因为在设计协议时,往往协议不是那么简单(可能是由多个固定长度的数据构成) ,比如下面这样的协议格式。

完整的协议有三段构成,每段开头都会有一个4字节的大小代表每段的长度,在判断协议是否完整时,就必须看着3段的数据是否都全部到达。 所以在判断第二段数据是否完整时,会跳过前面3个字节去判断,此时readOffsetBack 将会是3。

此外我还需要一个通过分割符获取字节的方法,因为有时候协议不是固定长度的数组了,而是通过某个分割符判断某段协议是否结束,比如换行符。

func (r *RingBuffer) PeekBytes(readOffsetBack int, delim byte) ([]byte, error)

接着,还需要提供一个更新读位置的方法,因为一旦判断是一个完整的协议后,我会将协议数据全部读取出来,此时应该要更新读指针的位置,以便下次读取新的请求。

func (r *RingBuffer) AddReadPosition(n int)

n 便是代表需要将读指针往后偏移的n个字节。

ringbuffer 原理解析

接着,我们再来看看实际上ringbuffer的实现原理是什么。

首先来看下一个ringbuffer应该有的属性

type RingBuffer struct {
buf []byte
reader io.Reader
r int // 标记下次读取开始的位置
unReadSize int // 缓冲区中未读数据大小
}

buf 用作连接读取的缓冲区,reader 代表了原链接,r代表读取ringbuffer时应该从字节数组的哪个位置开始读取,unReadSize 代表缓冲区当中还有多少数据没有读取,因为你可能一次性从reader里读取了很多数据到buf里,但是上层应用只取buf里的部分数据,剩余的未读数据就留在了buf里,等待下次被应用层继续读取。

我们用一个5字节的字节数组当做缓冲区, 首先从ringbuffer读取数据时,由于ringbuffer内部没有数据,所以需要从连接中读取数据然后写到ringbuffer里。

如下图所示:

假设ringBuffer规定每次向原网络连接读取时 按4字节读取到缓冲区中(实际情况为了减少系统调用开销,这个值会更多,尽可能会一次性读取更多数据到缓冲区) write pos 指向的位置则代表从reader读取的数据应该从哪个位置开始写入到buf字节数组里。

writePos = (r + unReadSize) % len(buf)



接着,上层应用只读取了3个字节,缓冲区中的读指针r和未读空间就会变成下面这样

如果此时上层应用还想再读取3个字节,那么ringbuffer就必须再向reader读取字节填充到缓冲区上,我们假设这次向reader索取3个字节。缓冲区的空间就会变成下面这样



此时已经复用了首次向reader读取数据时占据的缓冲空间了。

当填充上字节后,应用层继续读取3个字节,那么ringBuffer会变成这样

读指针又指向了数组的开头了,可以得出读指针的计算公式

r = (r + n)% len(buf)

ringBuffer 代码解析

有了前面的演示后,再来看代码就比较容易了。用peek 方法举例进行分析,

func (r *RingBuffer) Peek(readOffsetBack, n int) ([]byte, error) {
// 由于目前实现的ringBuffer还不具备自动扩容,所以不支持读取的字节数大于缓冲区的长度
if n > len(r.buf) {
return nil, fmt.Errorf("the unReadSize is over range the buffer len")
}
peek:
if n <= r.UnReadSize()-readOffsetBack {
// 说明缓冲区中的未读字节数有足够长的n个字节,从buf缓冲区直接读取
readPos := (r.r + readOffsetBack) % len(r.buf)
return r.dataByPos(readPos, (r.r+readOffsetBack+n-1)%len(r.buf)), nil
}
// 说明缓冲区中未读字节数不够n个字节那么长,还需要从reader里读取数据到缓冲区中
err := r.fill()
if err != nil {
return nil, err
}
goto peek
}

peek方法的大致逻辑是首先判断要读取的n个字节能不能从缓冲区buf里直接读取,如果能则直接返回,如果不能,则需要从reader里继续读取数据,直到buf缓冲区数据够n个字节那么长。

dataByPos 方法是根据传入的元素位置,从buf中读取在这个位置区间内的数据。

// dataByPos 返回索引值在start和end之间的数据,闭区间
func (r *RingBuffer) dataByPos(start int, end int) []byte {
// 因为环形缓冲区原因,所以末位置索引值有可能小于开始位置索引
if end < start {
return append(r.buf[start:], r.buf[:end+1]...)
}
return r.buf[start : end+1]
}

fill() 方法则是从reader中读取数据到buf里。

fill 情况分析

reader填充新数据到buf后,未读空间未跨越buf末尾



当从reader读取完数据后,如果 end := r.r + r.unReadSize + readBytes end指向了未读空间的末尾,如果没有超过buf的长度,那么将数据复制到buf里的逻辑很简单,直接在当前write pos的位置追加读取到的字节就行。

// 此时writePos 没有超过 len(buf)
writePos = (r + unReadSize)

未读 空间 本来就 已经从头覆盖

当未读空间本来就重新覆盖了buf头部,和上面类似,这种情况也是直接在write pos 位置追加数据即可。

未读空间未跨越buf末尾,当从reader追加数据到buf后发现需要覆盖buf头部



这种情况需要将读取的数据一部分覆盖到buf的末尾

 writePos := (r.r + r.unReadSize) % len(r.buf)
n := copy(r.buf[writePos:], buf[:readBytes])

一部分覆盖到buf的头部

end := r.r + r.unReadSize + readBytes
copy(r.buf[:end%len(r.buf)], buf[len(r.buf)-writePos:])

现在再来看fill的源码就比较容易理解了。

func (r *RingBuffer) fill() error {
if r.unReadSize == len(r.buf) {
// 当未读数据填满buf后 ,就应该等待上层应用把未读数据读取一部分再来填充缓冲区
return fmt.Errorf("the unReadSize is over range the buffer len")
}
// batchFetchBytes 为每次向reader里读取多少个字节,如果此时buf的剩余空间比batchFetchBytes小,则应该只向reader读取剩余空间的字节数
readLen := int(math.Min(float64(r.batchFetchBytes), float64(len(r.buf)-r.unReadSize)))
buf := make([]byte, readLen)
readBytes, err := r.reader.Read(buf)
if readBytes > 0 {
// 查看读取readBytes个字节后,未读空间有没有超过buf末尾指针,如果超过了,在复制数据时需要特殊处理
end := r.r + r.unReadSize + readBytes
if end < len(r.buf) {
// 没有超过末尾指针,直接将数据copy到writePos后面
copy(r.buf[r.r+r.unReadSize:], buf[:readBytes])
} else {
// 超过了末尾指针,有两种情况,看下图分析
writePos := (r.r + r.unReadSize) % len(r.buf)
n := copy(r.buf[writePos:], buf[:readBytes])
if n < readBytes {
copy(r.buf[:end%len(r.buf)], buf[len(r.buf)-writePos:])
}
}
r.unReadSize += readBytes
return nil
}
if err != nil {
return err
}
return nil
}

go 实现ringbuffer以及ringbuffer使用场景介绍的更多相关文章

  1. 消息中间件activemq的使用场景介绍(结合springboot的示例)

    一.消息队列概述 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题.实现高性能,高可用,可伸缩和最终一致性架构.是大型分布式系统不可缺少的中间件. 目前在生产环境,使 ...

  2. Redis 中 5 种数据结构的使用场景介绍

    这篇文章主要介绍了Redis中5种数据结构的使用场景介绍,本文对Redis中的5种数据类型String.Hash.List.Set.Sorted Set做了讲解,需要的朋友可以参考下 一.redis ...

  3. 从Client应用场景介绍IdentityServer4(五)

    原文:从Client应用场景介绍IdentityServer4(五) 本节将在第四节基础上介绍如何实现IdentityServer4从数据库获取User进行验证,并对Claim进行权限设置. 一.新建 ...

  4. 从Client应用场景介绍IdentityServer4(四)

    原文:从Client应用场景介绍IdentityServer4(四) 上节以对话形式,大概说了几种客户端授权模式的原理,这节重点介绍Hybrid模式在MVC下的使用.且为实现IdentityServe ...

  5. 从Client应用场景介绍IdentityServer4(三)

    原文:从Client应用场景介绍IdentityServer4(三) 在学习其他应用场景前,需要了解几个客户端的授权模式.首先了解下本节使用的几个名词 Resource Owner:资源拥有者,文中称 ...

  6. 从Client应用场景介绍IdentityServer4(一)

    原文:从Client应用场景介绍IdentityServer4(一) 一.背景 IdentityServer4的介绍将不再叙述,百度下可以找到,且官网的快速入门例子也有翻译的版本.这里主要从Clien ...

  7. 从Client应用场景介绍IdentityServer4(二)

    原文:从Client应用场景介绍IdentityServer4(二) 本节介绍Client的ClientCredentials客户端模式,先看下画的草图: 一.在Server上添加动态新增Client ...

  8. SharePoint Server 2013开发之旅(一):新的开发平台和典型开发场景介绍

    我终于开始写这个系列文章,实际上确实有一段时间没有动笔了.最近重新安装了一套SharePoint Server 2013的环境,计划利用工作之余的时间为大家写一点新的东西. SharePoint Se ...

  9. ZooKeeper应用场景介绍

    ZooKeeper是一个高可用的分布式数据管理与系统协调框架.维护着一个树形层次结构,书中的节点被称为znode.znode可以用来存储数据,并且有一个与之相关联的ACL(权限),znode不能大于1 ...

  10. Memcache应用场景介绍,说明

    面临的问题 对于高并发高访问的Web应用程序来说,数据库存取瓶颈一直是个令人头疼的问题.特别当你的程序架构还是建立在单数据库模式,而一个数据池连接数峰 值已经达到500的时候,那你的程序运行离崩溃的边 ...

随机推荐

  1. MySQL 高级查询截取分析

    慢日志分析流程如下:[1]观察(至少跑一天),看看生产慢 SQL 情况.[2]开启慢查询日志,设置阈值,比如对超过5秒的 SQL 语句进行抓取.[3]explain+慢 SQL 分析.[4]show ...

  2. Tomcat启动JSP项目,搞起来了

    虽然有点复古,但是还是有很多小伙伴在使用的,小编来一篇保姆级教程 1.用idea打开jsp项目 2.添加tomcat配置 3.点击后会出现配置框,这里画框的地方都选上,版本选择1.8,其他的信息内容默 ...

  3. 30张图说清楚 TCP 协议

    大家好,我是风筝 前两天分享了 20张图说清楚 IP 协议 今天,继续来网管的自我修养之TCP协议,这可是除 IP 协议外另一个核心协议了. TCP 协议是网络传输中至关重要的一个协议,它位于传输层. ...

  4. 一个斜杠引发的CDN资源回源请求量飙升

    背景 一个安静的晚上,突然接到小伙伴电话线上CDN回源异常,具体表现为请求量飙升,且伴有少量请求404,其中回源请求量飙升已经持续两天但一直未被发现,直到最近404请求触发了告警后分析log才同时发现 ...

  5. R语言网络数据爬取

    现在大家对爬虫的兴趣不断高涨,R和PYTHON是两个非常有力的爬虫工具.Python倾向于做大型爬虫,与R相比,语法相对复杂,因此Python爬虫的学习曲线会相对陡峭.对于那些时间宝贵,又想从网上获取 ...

  6. [Linux]常用命令之【nl/sed/awk/wc/xargs/perl】

    nl nl : 在linux系统中用来计算文件中行号. nl 可以将输出的文件内容自动的加上行号!其默认的结果与 cat -n 有点不太一样, nl 可以将行号做比较多的显示设计,包括位数与是否自动补 ...

  7. Go 语言切片是如何扩容的?

    原文链接: Go 语言切片是如何扩容的? 在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice). 切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装.它非常灵活,支 ...

  8. Nvidia Tensor Core初探

    1 背景 在基于深度学习卷积网络的图像处理领域,作为计算密集型的卷积算子一直都是工程优化的重点,而卷积计算一般转化为矩阵乘运算,所以优化矩阵乘运算自然成为深度学习框架最为关心的优化方向之一.鉴于此,N ...

  9. 简单的了解下 Fetch API 的工作原理

    一.简介 Fetch API是一种现代的Web API,提供了一种异步获取网络资源的方法.由于其简单性.灵活性和一致性,它已经成为Web应用程序中获取数据和资源的流行选择.在本文中,我们将深入探讨Fe ...

  10. 如何玩转国产神器:接口一体化协作平台Apifox!

    前言:Apifox是什么? 简介: 简单来说,Apifox = swagger + mock + postman+Jmeter,是API 文档.API 调试.API Mock.API 自动化测试一体化 ...