在游戏开发过程中,经常会涉及到前后端数据的交互。以二进制数据流进行通信是一种常用的方式。然而由于游戏通信的数据复杂多样,结构也相对不一,往往会造成数据的序列化繁琐。现在介绍一种相对简单的方式。
首先描述一下内存结构:
|-------------------------------------------|----------------------------------------------.................
|<-----------------1--------------------->|<------------------------------2------------------>
编号1:标识数据流的长度信息等,也就是我们所说的数据包头(msg head),这里一般是四个字节或八个字节的固定长度。
编号2:真正的有交数据流二进制数据。
基于以上内存互形式,我们来看看相关序列化的代码:
#ifndef _BINARY_WRITE_STREAM_H_
#define _BINARY_WRITE_STREAM_H_
#include <iostream>
#include <vector>
#include <string>
using namespace std;
enum
{
STREAM_LEN_HEAD_SIZE = 4,
STREAM_DEF_LEN = 10240,
};
class BinaryWriteStream
{
public:
BinaryWriteStream()
{
//!预留STREAM_LEN_HEAD_SIZE个长度表示此数据流的总长度
unsigned int len = 0;
m_data.append((char*)&len, STREAM_LEN_HEAD_SIZE);
}
~BinaryWriteStream(void){}
template<typename T>
BinaryWriteStream& operator <<(const T& val_);
template<typename T>
BinaryWriteStream& operator <<(const std::vector<T>& val_);
template<>
BinaryWriteStream& operator <<(const char& val_);
template<>
BinaryWriteStream& operator <<(const int& val_);
template<>
BinaryWriteStream& operator <<(const unsigned int& val_);
template<>
BinaryWriteStream& operator <<(const float& val_);
template<>
BinaryWriteStream& operator <<(const double& val_);
template<>
BinaryWriteStream& operator <<(const string& val_);
const string& data()
{
return m_data;
}
private:
BinaryWriteStream& append(const char* data_, unsigned int size_);
void flush();
private:
std::string m_data;
};
template<typename T>
BinaryWriteStream& BinaryWriteStream::operator <<(const T& val_)
{
val_.pack(*this);
return *this;
}
template<typename T>
BinaryWriteStream& BinaryWriteStream::operator <<(const std::vector<T>& val_)
{
unsigned int size = val_.size();
*this << size;
typename std::vector<T>::const_iterator itEnd = val_.end();
for(typename std::vector<T>::const_iterator it = val_.begin(); it != itEnd; ++it)
{
*this << *it;
}
return *this;
}
inline BinaryWriteStream& BinaryWriteStream::append(const char* data_, unsigned int size_)
{
m_data.append(data_, size_);
flush();
return *this;
}
inline void BinaryWriteStream::flush()
{
unsigned int size = m_data.size();
m_data.replace(m_data.begin(), m_data.begin() + STREAM_LEN_HEAD_SIZE, (char*)&size, STREAM_LEN_HEAD_SIZE);
}
template<>
BinaryWriteStream& BinaryWriteStream::operator <<(const char& val_)
{
return append((const char*)&val_, sizeof(char));
}
template<>
BinaryWriteStream& BinaryWriteStream::operator <<(const int& val_)
{
return append((const char*)&val_, sizeof(int));
}
template<>
BinaryWriteStream& BinaryWriteStream::operator <<(const unsigned int& val_)
{
return append((const char*)&val_, sizeof(unsigned int));
}
template<>
BinaryWriteStream& BinaryWriteStream::operator <<(const float& val_)
{
return append((const char*)&val_, sizeof(float));
}
template<>
BinaryWriteStream& BinaryWriteStream::operator <<(const double& val_)
{
return append((const char*)&val_, sizeof(double));
}
template<>
BinaryWriteStream& BinaryWriteStream::operator <<(const string& val_)
{
unsigned int size = val_.size();
append((const char*)&size, sizeof(unsigned int));
return append(val_.c_str(), size);
}
#endif
由代码可见:
1》在构造函数中我预留了一个指定长度,也就是上面所述的编号1。
2》我定义了一个标准模版函数,里面调用了类型的pack函数,也就是说:所有自定义的类型要进行序列化,都必须有一个pack函数来定义自身的序列化方式。
那么对常用基本数据类型,这里我进行了一个特化,直接进行内存数据拷贝。
3》每次数据进行序列化后,数据流长度变化,我都会调用flush函数更新预留区的值。
接下来,我们一起来看自定义类型的定义形式,如上述,必须有一个pack的函数,这里我们举个例子如下:
struct test
{
int a;
BinaryWriteStream& pack(BinaryWriteStream& stream) const
{
return stream << a;
}
};
显然上面的类是可以进行序列化的。所以,为了方便起见,我定义了一些宏,部分代码如下:
#ifndef _STRUCT_TEMPLATE_H_
#define _STRUCT_TEMPLATE_H_
class BinaryWriteStream;
class BinaryReadStream;
#define DEFINE_STRUCT_1(STRUCT_NAME, T1, V1) \
struct STRUCT_NAME \
{ \
T1 V1; \
BinaryWriteStream& pack(BinaryWriteStream& stream) const \
{ \
return stream << V1; \
} \
BinaryReadStream& unpack(BinaryReadStream& stream) \
{ \
return stream >> V1; \
} \
};
#define DEFINE_STRUCT_2(STRUCT_NAME, T1, V1, T2, V2) \
struct STRUCT_NAME \
{ \
T1 V1; \
T2 V2; \
BinaryWriteStream& pack(BinaryWriteStream& stream) const \
{ \
return stream << V1 << V2; \
} \
BinaryReadStream& unpack(BinaryReadStream& stream) \
{ \
return stream >> V1 >> V2; \
} \
};
#define DEFINE_MSG_1(MSG_NAME, MSGID_NAME, T1, V1) \
struct MSG_NAME \
{ \
int msgid; \
T1 V1; \
MSG_NAME(){msgid = MSGID_NAME;} \
BinaryWriteStream& pack(BinaryWriteStream& stream) const \
{ \
return stream << msgid << V1; \
} \
BinaryReadStream& unpack(BinaryReadStream& stream) \
{ \
return stream >> msgid >> V1; \
} \
};
#define DEFINE_MSG_2(MSG_NAME, MSGID_NAME, T1, V1, T2, V2) \
struct MSG_NAME \
{ \
int msgid; \
T1 V1; \
T2 V2; \
MSG_NAME(){msgid = MSGID_NAME;} \
BinaryWriteStream& pack(BinaryWriteStream& stream) const \
{ \
return stream << msgid << V1 << V2; \
} \
BinaryReadStream& unpack(BinaryReadStream& stream) \
{ \
return stream >> msgid >> V1 >> V2; \
} \
};
#endif
所以上面的示例test类可以直接表示成:DEFINE_STRUCT_1(test, int, a);
同样的思想,反序列化的代码如下:
#ifndef _BINARY_READ_STREAM_H_
#define _BINARY_READ_STREAM_H_
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class BinaryReadStream
{
public:
BinaryReadStream(const char* data_, unsigned int len_)
m_data(data_),m_len(len_),m_cur(data_)
{
m_cur += STREAM_LEN_HEAD_SIZE;
}
~BinaryReadStream(void){}
template<typename T>
BinaryReadStream& operator >>(T& val_);
template<typename T>
BinaryReadStream& operator >>(std::vector<T>& val_);
template<>
BinaryReadStream& operator >>(char& val_);
template<>
BinaryReadStream& operator >>(int& val_);
template<>
BinaryReadStream& operator >>(unsigned int& val_);
template<>
BinaryReadStream& operator >>(float& val_);
template<>
BinaryReadStream& operator >>(double& val_);
template<>
BinaryReadStream& operator >>(string& val_);
private:
unsigned int remainLen()
{
return m_len - (m_cur - m_data);
}
BinaryReadStream& readData(char* data_, unsigned int len_)
{
if(remainLen() < len_)
{
throw exception("size no enough");
}
::memcpy(data_, m_cur, len_);
m_cur += len_;
return *this;
}
private:
const char* const m_data;
const unsigned int m_len;
const char* m_cur;
};
template<typename T>
inline BinaryReadStream& BinaryReadStream::operator >>(T& val_)
{
val_.unpack(*this);
return *this;
}
template<typename T>
inline BinaryReadStream& BinaryReadStream::operator >>(std::vector<T>& val_)
{
unsigned int len = 0;
readData((char*)&len, sizeof(unsigned int));
for(unsigned int i = 0; i < len; ++i)
{
T temp;
*this >> temp;
val_.push_back(temp);
}
return *this;
}
template<>
inline BinaryReadStream& BinaryReadStream::operator >>(char& val_)
{
return readData((char*)&val_, sizeof(char));
}
template<>
inline BinaryReadStream& BinaryReadStream::operator >>(int& val_)
{
return readData((char*)&val_, sizeof(int));
}
template<>
inline BinaryReadStream& BinaryReadStream::operator >>(unsigned int& val_)
{
return readData((char*)&val_, sizeof(unsigned int));
}
template<>
inline BinaryReadStream& BinaryReadStream::operator >>(float& val_)
{
return readData((char*)&val_, sizeof(float));
}
template<>
inline BinaryReadStream& BinaryReadStream::operator >>(double& val_)
{
return readData((char*)&val_, sizeof(double));
}
template<>
inline BinaryReadStream& BinaryReadStream::operator >>(string& val_)
{
unsigned int len;
readData((char*)&len, sizeof(unsigned int));
return readData((char*)&val_, len);
}
#endif
测试代码如下:
// stream.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
using namespace std;
#include <vector>
#include "BinaryWriteStream.h"
#include "BinaryReadStream.h"
#include "structTemplate.h"
DEFINE_STRUCT_2(test_struct, int, a, std::vector<int>, b);
DEFINE_MSG_2(test_msg2, 2, test_struct, st, int, a);
DEFINE_MSG_1(test_msg, 2, test_struct, st);
#ifdef WIN32_SYSTEM
int _tmain(int argc, _TCHAR* argv[])
#else
int main(int argc, char* argv[])
#endif
{
int a = 0;
int b = 3;
int c = 4;
std::vector<int> d;
d.push_back(a);
d.push_back(b);
d.push_back(c);
test_struct st;
st.a = b;
st.b = d;
test_msg ms;
ms.st = st;
BinaryWriteStream stream;
stream << ms;
try
{
const string& data = stream.data();
BinaryReadStream readStream(data.c_str(), data.size());
test_msg ds;
readStream >> ds;
}
catch (exception& e)
{
cout << "exception:" << e.what() << endl;
}
getchar();
return 0;
}
以上只介绍了核心思想,不足和完善的地方日后有时间在整理。