[从零开始的Unity网络同步] 附1.网络消息包的封装(Packet)

发表于2018-11-15
评论4 4.6k浏览

1.原因

因为使用UnityEngine.Networking,发送或接受的消息都要继承自MessageBase,而MessageBase的读写操作,使用的是(读)NetworkReader,(写)NetworkWriter,查看文档会发现,写入操作都是直接按照基本数据类型所占字节长度来整个写入的,读取也是如此.比如:一个int类型,占4个字节,但是如果这个int变量是10的话,二进制表示为1010,只需要写入4个位(半个字节)就行了,为了节约网络消息包的大小(毕竟带宽有限~),有的时候完全没必要写入全部的字节长度.所以需要对网络消息包进行一下封装,实现按照自己的需要读写指定长度网络数据.

2.思路

计算机系统中一切数据的本质都是0和1,一个0或1表示1个位(bit),每8个位表示1个字节(byte),基本的数据类型所占位数
>bool = 1位
float = 32位
double = 64位
short, ushort = 16位
int, uint = 32位
long, ulong = 64位
string = 1个ASCII字符占8位,中文字符占16位

另外还涉及到浮点数(科学计数法)与负数(最高位为1)0的二进制表示形式,网上有很多讲解,在此不再赘述.
既然要实现按己所需读写指定长度的数据,那么就需要一个类似游标(指针)的变量来标记整个数据包写到哪了,数据包读到哪了.

3.编码

Packet类

public class Packet : IDisposable
{ 
    public int ptr;                //数据读/写的游标(指针)
    public int length;             //所占位数
    public byte[] data;            //字节数组,读写的数据都在这

    public Packet(byte[] array) : this(array, array.Length)
    {    }
    public Packet(byte[] array, int size)
    {
        ptr = 0;
        data = array;
        length = size << 3;
    }
}

每个数组类型的写入其实都可以当成是将一个字节按照指定位数从某个位置开始写

    /// <summary>
    /// 逐位写入 
    /// </summary>
    public void WriteByteAtPtr(byte value, int bits)
    {
        if (bits > 0)
        {
            value = (byte)((int)value & 255 >> 8 - bits);
            int byteIndex= ptr >> 3;                               //准备写入的byteIndex
            int num = ptr & 7;                                    //byteIndex对应的byte已经写了多少位
            int num2 = 8 - num;                                   //byteIndex对应的byte剩余多少位可写
            int num3 = num2 - bits;                                //写完以后剩余位数
            if (num3 >= 0)
            {
                //如果空间足够,就在data[num]写入value
                int num4 = 255 >> num2 | 255 << 8 - num3;
                data[byteIndex] = (byte)(((int)data[byteIndex] & num4) | (int)value << num);
            }
            else
            {
                //如果空间不够,就将value拆分,放入两个字节中
                data[byteIndex] = (byte)(((int)data[byteIndex] & 255 >> num2) | (int)value << num);
                data[byteIndex+ 1] = (byte)(((int)data[byteIndex + 1] & 255 << bits - num2) | value >> num2);
            }
        }
        ptr += bits;          //ptr增加
    }

有了这个最基本的实现方法,那么其他的基本类型都很好写了:

public bool WriteBool(bool value)
{
    WriteByteAtPtr((byte)(value ? 1 : 0), 1);
    return value;
}

public void WriteByte(byte value, int bits = 8)
{
    WriteByteAtPtr(value, bits);
}

public void WriteUShort(ushort value, int bits = 16)
{
    if (bits <= 8)
    {
        WriteByteAtPtr((byte)(value & 255), bits);
    }
    else
    {
        WriteByteAtPtr((byte)(value & 255), 8);
        WriteByteAtPtr((byte)(value >> 8), bits - 8);
    }
}

其他类型的写入方式大体相似,就不一一举例了.对应的读取方式,最根本的方法:

private byte ReadByteAtPtr(int bits)
{
    byte result;
    if (bits <= 0)
    {
        result = 0;
    }
    else
    {
        int num = ptr >> 3;
        int num2 = ptr % 8;
        byte b;
        if (num2 == 0 && bits == 8)
        {
             b = data[num];
        }
        else
        {
            int num3 = data[num] >> num2;
            int num4 = bits - (8 - num2);
            if (num4 < 1)
            {
                b = (byte)(num3 & 255 >> 8 - bits);
            }
            else
            {
                int num5 = (int)data[num + 1] & 255 >> 8 - num4;
                b = (byte)(num3 | num5 << bits - num4);
             }
         }
        ptr += bits;
        result = b;
     }
     return result;
}

与写入方式类似,其他基本类型的读取,就将需要读的位数传入,拿到返回的字节以后,组合成对应的数据类型即可.

4.结语

这样,对网络消息包的封装基本就完成了,使用这个类,可以更灵活的构造网络传输中的数据,包括数据压缩,解压;检测数据溢出;截断数据流等等都可以很方便了.

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引