内存比特流-序列化比特大小的数据
前几周硬啃UE4同步源码还是极其费劲的,其中关于比特流的序列化和反序列化部分更是头大。最近看的一本书《网络多人游戏架构与编程》对MMO游戏的设计都了很好的解释。其中的设计思路就有UE4采用的。因此看了这本书绝对可以让你在学习UE4同步源码的时候事半功倍。
这片文章整理了内存比特流写入和读取。
计算机内部按字节为单位存储,以比特为单位写入缓冲区的最大问题是如果缓冲区的某个字节本次没写满,例如一字节8位比特中只写了6个比特,那么下次再写5个比特的时候,如何利用好上一次剩余的2个比特。
举个粒子,我要往缓冲区中分别写入数字13和数字52,以之前的最简单的按字节写缓冲区的做法,两个int数据类型用64位写入缓冲区也太浪费空间了。聪明一点的做法是因为13和52也就占了2个字节,因此我只用两个字节写入缓冲区,但是更高效省空间的做法是用11个比特位写入缓冲区。这种比特流的做法使得缓冲区内每一个比特都富有意义。
比特流写入缓冲区:
具体的以比特写缓冲区的做法是:先写数字13,这个时候缓冲区第一个字节如下:
写一次再写数字52的时候,因为上一个字节还有高位3个比特的空闲位置,所以数字52的低3位写到这里,剩余的高3位写到缓冲区的下一个字节:
从比特流缓冲区读出:
读出占据5比特的数字13,这时候mBitHead就等于5。下一次读占据6比特的数字52的时候就比较麻烦。需要将第一个字节剩余的3个比特和第二个字节的低3个比特拼出来。就是上图绿色箭头逆过来。
代码实现:
/* * MemoryBitStream.h * * Created on: 2020年9月6日 * Author: Administrator */ #ifndef MEMORYBITSTREAM_H_ #define MEMORYBITSTREAM_H_ #include <cstdint> #include <cstdlib> #include <string> class OutputMemoryBitStream { public: OutputMemoryBitStream() { ReallocBuffer(256); } ~OutputMemoryBitStream() { std::free(mBuffer); } void WriteBits(uint8_t inData, uint32_t inBitCount); void WriteBits(const void* inData, uint32_t inBitCount); const char* GetBufferPtr() const {return mBuffer;} uint32_t GetBitLength() const { //返回缓冲区中一共有多少有效比特 return mBitHead; } uint32_t GetByteLength() const { //返回缓冲区中的所有比特一共占据了多少字节 //+7效果为向上取整,再右移3位(右移3位等同于除以8) return (mBitHead+7) >> 3; } void WriteBytes(const void* inData, uint32_t inByteCount) {WriteBits(inData, inByteCount<<3);} //提供模板使得支持对所有POD数据类型的比特流写入 template<typename T> void Write(T inData, unsigned int inBitCount = sizeof(T) * 8) { static_assert(std::is_arithmetic<T>::value || std::is_enum<T>::value, "Only supports primitive data types"); WriteBits(&inData, inBitCount); } //提供bool类型的重载使和模板的参数区分 void Write(bool inData) { WriteBits(&inData, 1); } private: void ReallocBuffer(uint32_t inNewBitCapacity); private: char *mBuffer; uint32_t mBitHead; uint32_t mBitCapacity; }; class InputMemoryBitStream { public: InputMemoryBitStream(char *inBuffer, uint32_t inBitCount): mBuffer(inBuffer), mBitCapacity(inBitCount), mBitHead(0) { } ~InputMemoryBitStream() { free(mBuffer);} const char* GetBufferPtr() const { return mBuffer; } uint32_t GetRemainingBitCount() const { return mBitCapacity - mBitHead;} void ReadBits(uint8_t &outData, uint32_t inBitCount); void ReadBits(void *outData, uint32_t inBitCount); void ReadBytes(void *outData, uint32_t inByteCount) { ReadBits(outData, inByteCount << 3);} //提供模板供使得可以从流中读取所有的POD数据类型 template<typename T> void Read(T &inData, uint32_t inBitCount = sizeof(T)*8) { static_assert(std::is_arithmetic<T>::value || std::is_enum<T>::value, "Only support POD data"); ReadBits(&inData, inBitCount); } private: char* mBuffer; uint32_t mBitHead; uint32_t mBitCapacity; }; #endif /* MEMORYBITSTREAM_H_ */
/* * MemoryBitStream.cpp * * Created on: 2020年9月6日 * Author: Administrator */ #include "MemoryBitStream.h" void OutputMemoryBitStream::WriteBits(uint8_t inData, uint32_t inBitCount) { uint32_t nextBitHead = mBitHead + inBitCount; if (nextBitHead > mBitCapacity) { ReallocBuffer(std::max(nextBitHead, mBitCapacity*2)); } uint32_t byteOffset = mBitHead >> 3; //&7的效果等同于除以8求余数,这里是拿到总的比特数在最后一个字节内占据了多少位 uint32_t bitOffset = mBitHead & 0x7; //currentMask用来将最后一个字节没被占用的位置0,清除脏位数据 uint8_t currentMask = ~(0xff << bitOffset); //(mBuffer[byteOffset] & currentMask):清除位上的脏数据 //(inData << bitOffset):bitOffset这几个位上已经是有效数据了,因此将待写入缓冲区的inData左移空出来这几个位,再写于缓冲区。 mBuffer[byteOffset] = (mBuffer[byteOffset] & currentMask) | (inData << bitOffset); //mBuffer[byteOffset]的一个字节内如果已经有部分有效位,那么就存在待写入的inData还有部分未写入缓冲区的情况 //因此计算还有多少位未写入,将这些位写入到下一个字节内 //因为inData的写入缓冲区都是先写低位,因此计算未写入的方法是:计算写入了多少位,再将inData右移多少位写入mBuffer的下一个空闲字节 uint32_t bitsFreeThisByte = 8 - bitOffset; if (bitsFreeThisByte < inBitCount) { mBuffer[byteOffset + 1] = inData >> bitsFreeThisByte; } mBitHead = nextBitHead; } void OutputMemoryBitStream::WriteBits(const void* inData, uint32_t inBitCount) { const char *srcByte = static_cast<const char*>(inData); while(inBitCount > 8) { WriteBits(*srcByte, 8); //注意不是srcByte+=8, 此处指针的++就是跳了8位 srcByte++; inBitCount -= 8; } if (inBitCount > 0) { WriteBits(*srcByte, inBitCount); } } void InputMemoryBitStream::ReadBits(uint8_t &outData, uint32_t inBitCount) { uint32_t byteOffset = mBitHead >> 8; uint32_t bitOffset = mBitHead & 7; //依据mBitHead拿到读取比特位的起点 //假设mBitHead=10 //0000 0111 0000 0111 1111 1111 // |------>从这里往高位开始读,再第二个字节中,最多还能读6位,如果inBitCount大于6,那么就需要到下一个字节读了 outData = static_cast<uint8_t>(mBuffer[byteOffset]) >> bitOffset; uint32_t bitsFreeThisByte = 8 - bitOffset; if (inBitCount > bitsFreeThisByte) { //由于存的缘故,读下一个字节剩余的比特位是放在outData的高位 //bitsFreeThisByte是在上一个mBuffer[byteOffset]字节中读的位放到了outData的低位,那么从下一个字节中读的位就要放到outData的高位 //最好的办法就是将mBuffer[byteOffset+1]左移或上outData,然后将不需要读的位置0 outData |= static_cast<uint8_t>(mBuffer[byteOffset+1]) << bitsFreeThisByte; } //将outData不是从mBuffer里面读的位置为0 outData &= (~(0x00ff << inBitCount)); mBitHead += inBitCount; } void InputMemoryBitStream::ReadBits(void* outData, uint32_t inBitCount) { uint8_t *destByte = reinterpret_cast<uint8_t*>(outData); while(inBitCount < 8) { ReadBits(*destByte, 8); ++destByte; inBitCount -= 8; } if (inBitCount > 0) { ReadBits(*destByte, inBitCount); } }