首页 > 代码库 > 设计一个字节数组缓存类

设计一个字节数组缓存类

版权所有,转载须注明出处!

1、为什么要

在做网络通信的时候,经常需要用到:
  • 读:就是我们需要从网络流里面读取字节数据,并且由于分包的原因,我们需要自己缓存这些数据,而不是读完立刻丢掉。
  • 写:我们需要把各种类型的数据变成字节写入。比如把int、string、short等变成字节数组写入流。

2、需要什么

我们需要设计一个类来实现:
  • 支持可以不停地往这个类中添加字节
  • 支持写入int、string、short等基础数据类型
  • 支持从这个类中获取可读的字节

3、怎么做

    1. 支持可以不停地往这个类中添加字节

    这个实现你可以用一个List<byte>来实现(本身List就支持无限往里面添加元素)。不过由于这个类比较特殊。处于网络最底层,使用比较频繁。因此我们还是自己用byte[]来处理。

    2.支持写入int、string、short等基础数据类型

    这个简单,在实现了上一步添加字节数组的基础上,剩下的只需要把各种数据类型编码成byte数组而已。比如一个int变成用4个byte表示。

    3.支持从这个类中获取可读的字节

    比如你写入了10个byte,那么肯定需要能从里面读出这10个byte。不然写入的数据就没意义了。

4、开始动手写代码

using System;
using System.Text;
 
namespace com.duoyu001.net
{
    namespace buffer
    {
        /* ==============================================================================
             * 功能描述:字节缓冲类
             * 创 建 者:cjunhong
             * 主    页:http://blog.csdn.net/kakashi8841
             * 邮    箱:[url=mailto:john.cha@qq.com]john.cha@qq.com[/url]
             * 创建日期:2014/12/02 16:22:09
             * ==============================================================================*/
 
        public class ByteBuffer
        {
            //增加的容量
            public const short CAPACITY_INCREASEMENT = 128;
            public const ushort USHORT_8 = (ushort) 8;
            public const short SHORT_8 = (short) 8;
            //字节数组
            private byte[] buffers;
            //读取索引
            private int readerIndex;
            //写的索引
            private int writerIndex;
            //上次备份的reader索引
            private int readerIndexBak;
            //字符数组 空字符串
            public static byte[] NULL_STRING = new byte[] {(byte) 0, (byte) 0};
 
            public ByteBuffer()
                : this(8)
            {
            }
 
            /// <summary>
            /// 带参构造函数 初始化字节数组
            /// </summary>
            /// <param name="initCapacity">初始容量</param>
            public ByteBuffer(int initCapacity)
            {
                buffers = new byte[initCapacity];
            }
 
            /// <summary>
            /// 带参构造函数 向字节数组中 写字节
            /// </summary>
            /// <param name="buffers">字节数组</param>
            public ByteBuffer(byte[] buffers)
                : this(buffers.Length)
            {
                writeBytes(buffers);
            }
 
            public void writeBytes(byte[] data, int dataOffset, int dataSize)
            {
                ensureWritable(dataSize);
                Array.Copy(data, dataOffset, buffers, writerIndex, dataSize);
                writerIndex += dataSize;
            }
 
            public void writeBytes(byte[] data)
            {
                writeBytes(data, 0, data.Length);
            }
 
            public void writeByte(byte data)
            {
                writeBytes(new byte[] {data});
            }
 
            public void writeByte(int data)
            {
                writeBytes(new byte[] {(byte) data});
            }
 
            public void writeShort(int data)
            {
                writeBytes(new byte[] {(byte) (data >> 8), (byte) data});
            }
 
            public void writeInt(int data)
            {
                writeBytes(new byte[]
                    {
                        (byte) (data >> 24),
                        (byte) (data >> 16),
                        (byte) (data >> 8),
                        (byte) data
                    });
            }
 
            public void writeString(string data)
            {
                writeString(data, Encoding.UTF8);
            }
 
            public void writeString(string data, Encoding encoding)
            {
                if (data =http://www.mamicode.com/= null)>

5、怎么用

1、怎么写数据

    比如你想写入一个int。那么只要调用writeInt方法。想写入short、byte、string等 只要调用相应的writeShort、writeByte、writeString等方法即可。

2、怎么读数据

    比如你想读出一个int。那么只要调用readInt方法。相应读取short、byte、string等 也只要调用readShort、readByte、readString等方法。

3、怎么记录上次读取位置,并重置当前的位置到上次位置

    比如你想读取一个数据,然后判断数据是否符合期望值,如果不符合则返回到读取前状态。(这个在处理分包的时候经常需要用到,因为你需要确认本次想读取的数据是否已经全部接受完,如果还没接受完,那么你就需要等到下次接受完整再来读取)那么只要这么调用:
        saveReaderIndex
        readXXX
        loadReaderIndex



6、内部设计说明

1、关于writeXXX

    可以看到。writeInt、writeShort、writeByte、writeString等方法,都是调用writeBytes(byte[] data)。没错,正如上面说的,各种写方法,只是把指定的数据类型编码成byte数组,然后添加到里面而已。

       1.1、怎么编码

        比如writeInt,其实只是简单用移位获取它每个8位的的数据(一个int是由4个byte组成的嘛-_-)。然后就writeBytes(byte[] data)

       1.2、writeBytes做了啥

public void writeBytes(byte[] data, int dataOffset, int dataSize)
{
    ensureWritable(dataSize);
    Array.Copy(data, dataOffset, buffers, writerIndex, dataSize);
    writerIndex += dataSize;
}
上面说了,我们要支持能无限往里面写数据。因此,第一行代码ensureWritable就是来确定当前是否能写入指定长度的数据。如果判断不行,则会进行扩容(怎样判断具体的思路我们在后面会说)。
        第二行代码则是把写入的数据复制到我们这个缓存对象上的指定位置。
        第三行则是把写指针writeIndex往后移。
        细心的读者应该会发现ensureWritable中有关于readerIndex、writerIndex这些参数的一些计算。接着看下面。


2、读写指针readerIndex与writerIndex

    我们使用byte[]数组来保存数据,那么我们怎么知道如果追加数据的时候,应该把新数据加入到数组中的哪个位置?怎么知道当前有多少数据可以读?有多少空间可以写入数据?

        2.1、writerIndex

        我们使用writerIndex来记录当前数组中哪个位置可以开始写入数据。最开始这个值为0,每当写入一个byte的时候,这个值加1。(请看上面1.2中writeBytes的第三行代码)

        2.2、readerIndex

        缓冲区的数据是读取之后就会丢弃的,但是如果每次读取就要重建数组来实现丢弃,这样的开销就太大了。因此,我们可以使用readerIndex来记录当前读取到数组中哪个位置,那么下次读取就会从这个位置开始读取数据了。每当读取一个byte,readerIndex加1。你可以先看看下面这段readInt的代码,读取了一个int(4个byte),那么readerIndex会增加4。
public int readInt()
{
    int i = buffers[readerIndex] << 24 | buffers[readerIndex + 1] << 16 | buffers[readerIndex + 2] << 8 |
            buffers[readerIndex + 3];
    readerIndex += 4;
    return i;
}

        2.3、writerIndex和readerIndex的关系

        怎样判断当前有多少数据可以读?根据上面对这两个参数的解释,我们可以轻易得出问题的答案是:writerIndex-readerIndex。这也正是readableBytes方法中的实现。
public int readableBytes()
{
    return writerIndex - readerIndex;
}

       2.4、怎样判断当前有没有足够的空间来写入数据

        最简单的方法是:判断数据剩余写入空间是否大于要写入的数据长度。剩余写入空间即:buffers.Length - writerIndex
        当时如果上面判断出的剩余写入空间比要写入的数据长度小时,是否就要重建一个更大的数组呢?不一定,因为还可以回收一些已经读取过的空间来使用。具体代码:
private void ensureWritable(int dataSize)
{
    int leftCapacity = buffers.Length - writerIndex;
    if (leftCapacity < dataSize)
    {
        int oldReaderIndex = readerIndex;
        int oldWriterIndex = writerIndex;
        writerIndex = readableBytes();
        readerIndex = 0;
        if (buffers.Length - writerIndex >= dataSize)
        {
            Array.Copy(buffers, oldReaderIndex, buffers, 0, oldWriterIndex - oldReaderIndex);
        }
        else
        {
            byte[] newBuffers = new byte[buffers.Length + CAPACITY_INCREASEMENT];
            Array.Copy(buffers, oldReaderIndex, newBuffers, 0, oldWriterIndex - oldReaderIndex);
            buffers = newBuffers;
        }
    }
}
       可以看到,当满足buffers.Length - writerIndex >= dataSize条件时,是没有重建数组的。因为这时候说明你前面有一些读取过的数据,因此你只需要把那部分读取过的数据丢弃掉,就有更多的空间来容纳要写入的数据了。

        2.5、这个类还有个readerIndexBak的又是干嘛的?  

       前面说了,你有时候需要进行尝试数据读取,但是当发现没到读取时间的时候,想要恢复读取状态,就可以通过在读取前保存读取指针,后面可以恢复读取指针。


基本关于这个类在Socket通信中使用已经足够满足大部分应用场景的需要了。
至于你说你用Protobuf或什么之类的协议。和这个类是无关的。比如:读取的时候只要这个对象中读取byte字节,然后反序列化成Protobuf数据即可。
反正这个类是通信中最基础的一个数据类。有需要的就拿去吧~


大家有什么建议或意见可以在这里和我交流。

设计一个字节数组缓存类