C++实现DataInputStream/DataOutputStream

DataInputStream/DataOutputStream

我们知道Java中,可以用DataInputStream/DataOutputStream分别从字节流中读取数据以及向流中保存数据。它们分别实现了以下接口(不完整):
DataOutputStream:

void write(byte b[]) throws IOException;

void writeBoolean(boolean v) throws IOException;

void writeByte(int v) throws IOException;

void writeShort(int v) throws IOException;

void writeChar(int v) throws IOException;

void writeInt(int v) throws IOException;

void writeLong(long v) throws IOException;

void writeFloat(float v) throws IOException;

DataInputStream:

void readFully(byte b[]) throws IOException;

int skipBytes(int n) throws IOException;

boolean readBoolean() throws IOException;

byte readByte() throws IOException;

short readShort() throws IOException;

char readChar() throws IOException;

int readInt() throws IOException;

long readLong() throws IOException;

float readFloat() throws IOException;

但是在C++中,并没有如此方便的接口(至少标准库中没有,Boost库中倒是有Serialization接口支持序列化(即将数据结构保存为字节流)与反序列化(从字节流中读取数据以重建数据结构),但是引入Boost库的开销是比较庞大的)。因此,考虑自己实现字节流。

字节流的保存与读取

首先,用于存储流的数据结构当然得是byte数组,这里直接用vector方便扩容

typedef unsigned char byte;
std::vector<byte> byteStream; // 用unsigned char数组来保存字节流

将各种类型的数据以字节的形式保存到字节流中,可以直接抽象为通用的模板:

template<typename T>
void WriteValue(T &val) // Only for POD data
{
    byteStream.insert(byteStream.end(), reinterpret_cast<byte*>(&val), reinterpret_cast<byte*>(&val) + sizeof(T));
}

这样,在实现WriteInt等接口时,可以省去大量重复的代码:

...
void WriteFloat(float val)
{
    WriteValue(val);
}

void WriteInt(int val)
{
    WriteValue(val);
}
...

同样,读取字节流也可以用模板来实现:

template<typename T>
T ReadValue() // Only for POD data
{
    T val = *(reinterpret_cast<T*>(byteStream.data() + streamIter));
    streamIter += sizeof(T);
    return val;
}

由于字节流的读取是单向的,之前读取的数据在当前流中将无法再次读取,所以用streamIter来记录当前光标在流中的位置(用erase删掉已经读过的数据也是可以的,但是效率不如指针,开销也大)。
这样,读取不同类型的数据就可以按如下方式编码:

...
float ReadFloat()
{
    return ReadValue<float>();
}

int ReadInt()
{
    return ReadValue<int>();
}
...

保存与读取字节数组的实现如下:

void Write(const byte* buff, unsigned int bufSize)
{
    if (buff == nullptr) {
        // 打印异常日志
        return;
    }
    byteStream.insert(byteStream.end(), buff, buff + bufSize);
}

int Read(byte* buff, unsigned int bufSize)
{
    std::copy(byteStream.begin() + streamIter, byteStream.begin() + streamIter + bufSize, buff);
    streamIter += bufSize;
    return 0; // 这里没有考虑异常场景
}

需要注意,一个字节流应该只用来保存或者读取数据。建议将WriteInt、WriteFloat等写数据的接口都封装到类似DataOutputStream的类里,用来支持数据结构的序列化;将ReadInt、ReadFloat等读数据的接口都封装到类似DataInputStream的类里,用来支持数据结构的反序列化。

大小端转换

上面的代码已经基本能满足数据结构的序列化与反序列化需求,唯一的问题是,生成的字节流是小端模式存储在内存里的。由于当前的CPU(Intel、ARM等)大多是以小端模式存储的,我们按照上面的方式生成字节流并读取并不会有问题。然而,由于大端模式更符合人类的读写习惯,网络字节序与文件字节序都是以大端模式存储的。这样就会有这样一个问题,假如我们需要反序列化一个其他应用生成的字节流(它很大概率是以大端模式保存的),那样我们读出来的数据与原始数据差别就会非常大。同样,当我们把我们的字节流交给其他应用时,大概率也是以大端模式来解析。所以,我们在序列化时,要将数据做大端转换,而反序列化时要将读取的数据转换回小端模式。
于是,考虑将数据转换为大端保存,保存POD数据的模板修改如下:

template<typename T>
void WriteValue(T &val) // Only for POD data
{
    // 转换为大端模式存,需要从数据的末位向高位存
    auto *p = reinterpret_cast<byte*>(&val);
    for (int i = sizeof(T) - 1; i >= 0; i--) {
        byteStream.emplace_back(p[i]);
    }
}

这样WriteInt、WriteFloat等接口就无需修改了。
很好,改动不大。那么ReadValue的模板也如法炮制吧:

// 下面的代码无法通过编译
template<typename T>
T ReadValue() // Only for POD data
{
    T val;
    byte *p = byteStream.data() + streamIter;
    for (int i = sizeof(T) - 1; i >= 0; i--) {
        val |= (p[i] << (i * 8));
    }
    return val;
}

理想很丰满,现实很骨感,上面的代码是有问题的。val |= (p[i] << (i * 8))这种写法是不可行的,因为|、 &、^这些操作符只能用于整型(int、short、long、long long、uint32_t等。char也算,它其实就是int8_t),不能用于浮点数和自定义类型等,对于模板中T类型的变量val,在执行|=运算时是不能确保它 的类型是整型的,编译会报错。那么就没办法用模板了。
那么,对于长度不同的整型数据,分别提供下面的接口:

// byte即unsigned char,即uint8_t
uint8_t Read8Bits()
{
    byte* p = byteStream.data() + streamIter;
    streamIter += sizeof(uint8_t);
    return p[0];
}

uint16_t Read16Bits()
{
    byte* p = byteStream.data() + streamIter;
    streamIter += sizeof(uint16_t);
    return p[0] << 8 | p[1];
}

uint32_t Read32Bits()
{
    byte* p = byteStream.data() + streamIter;
    streamIter += sizeof(uint32_t);
    return p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
}

uint64_t Read64Bits()
{
    uint64_t high = Read32Bits();
    uint64_t low = Read32Bits();
    return high << 32 | low;
}

这样,ReadInt,ReadLong等接口就可以根据返回值长度调用上面不同的接口了:

...
uint32_t ReadUInt()
{
    return Read32Bits();
}

int ReadInt()
{
    return Read32Bits();
}
...

unsigned int与int虽然可以表示的范围不同,但在内存中都是4个字节,按字节读取出来再隐式转换为对应的类型即可。其它无符号整型也是一样的。
对于浮点数,我们该怎么处理呢?浮点数没办法用|操作符来连接呀。这里要用到union来处理,以ReadFloat为例,具体实现如下(相关原理这里不赘述了):

union FloatConvert {
    uint32_t intVal;
    float floatVal;
};

float ReadFloat()
{
    FloatConvert converter;
    converter.intVal = Read32Bits();
    return converter.floatVal;
}

对于Read与Write接口,由于是一个字节一个字节顺序保存的,读取时也是按字节读,大小端模式不会造成歧义。

后话

实现好上面的接口,字节流的存取基本就没问题了。还有一些额外的细节,比如读到字节流尾、读出数据过大等异常处理就留给各位读者自己实现了。
最后再把字节流存到文件以及从文件中读取字节流的代码贴一下,方便各位取用。本人用的是fstream:

bool ReadFile(const std::string &filePath)
{
    std::ifstream ifs(filePath, std::ios::binary | std::ios::in);
    if (!ifs.is_open()) {
        std::cerr << "in file is not exist:" << filePath << std::endl;
        return false;
    }
    ifs.seekg(0, std::ios::end);
    int fileLength = ifs.tellg();
    ifs.seekg(0, std::ios::beg);
    byteStream.resize(fileLength);
    ifs.read(reinterpret_cast<char*>(byteStream.data()), fileLength);
    ifs.close();
    std::cout << "file read success." << std::endl;
    return true;
}

bool WriteFile(const std::string &filePath)
{
    std::ofstream ofs(filePath, std::ios::binary | std::ios::out);
    if (!ofs.is_open()) {
        std::cerr << "out file is not exist:" << filePath << std::endl;
        return false;
    }
    ofs.seekp(0, std::ios::beg);
    ofs.write(reinterpret_cast<char*>(byteStream.data()), byteStream.size());
    ofs.close();
    std::cout << "file write success." << std::endl;
    return true;
}

欢迎各位读者进行交流与指导!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值