自我总结本篇概要
- 读取数据的时候要特别小心, 因为可能会有攻击者发送过来的恶意的数据包以及错误的包, 在写入数据的时候你可能会轻松很多,因为如果有任何事情出错了,那几乎肯定是你自己导致的错误
统一的数据包序列化功能 :诀窍在于让流类型的序列化函数模板化。在我的系统中有两个流类型:ReadStream类和WriteStream类。每个类都有相同的一套方法,但实际上它们没有任何关系。一个类负责从比特流读取值到变量中,另外一个类负责把变量的值写到流中。
在模板里类似这样写, 通过Stream::IsWriting
和Stream::IsReading
模板会自动区分,然后帮你生产你想要的代码, 简洁漂亮1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38class WriteStream
{
public:
enum { IsWriting = 1 };
enum { IsReading = 0 };
// ...
};
class ReadStream
{
public:
enum { IsWriting = 0 };
enum { IsReading = 1 };
// ..
}
template <typename Stream>
bool serialize( Stream & stream, float & value )
{
union FloatInt
{
float float_value;
uint32_t int_value;
};
FloatInt tmp;
if ( Stream::IsWriting )
tmp.float_value = value;
bool result = stream.SerializeBits( tmp.int_value, 32 );
if ( Stream::IsReading )
value = tmp.float_value;
return result;
}边界检查和终止读取 : 把允许的大小范围也传给序列化函数而不仅仅是所需的比特数量。
序列化浮点数和向量 : 计算机根本不知道内存中的这个32位的值到底是一个整数还是一个浮点数还是一个字符串的部分。它知道的就是这仅仅是一个32位的值。代码如下(可以通过一个联合体来访问看上去是整数的浮点数).
有些时候,你并不想把一个完整精度的浮点数进行传递。那么该如何压缩这个浮点值?第一步是将它的值限制在某个确定的范围内然后用一个整数表示方式来将它量化。
举个例子来说,如果你知道一个浮点类型的值是在区间[-10,+10],对于这个值来说可以接受的精确度是0.01,那么你可以把这个浮点数乘以100.0让它的值在区间[-1000,+1000]并在网络上将其作为一个整数进行序列化。而在接收的那一端,仅仅需要将它除以100.0来得到最初的浮点值.1
2
3
4
5
6
7
8
9union FloatInt
{
float float_value;
uint32_t int_value;
};
FloatInt tmp;
tmp.float_value= 10.0f;
printf(“float value as an integer: %x\n”, tmp.int_value );序列化字符串和数组 : 为什么要费那么大精力把一个字节数组按比特打包到你的比特流里?为什么不在序列化写入之前进行按字节进行对齐?Why not align to byte so you can memcpy the array of bytes directly into the packet?
如何将比特流按字节对齐?只需要在流的当前位置做些计算就可以了,找出还差写入多少个比特就能让当前比特流的比特数量被8整除,然后按照这个数字插入填充比特(比如当前比特流的比特数量是323,那么323+5才能被8整除,所以需要插入5个填充比特)。对于填充比特来说,填充的比特值都是0,这样当你序列化读取的时候你可以进行检测,如果检测的结果是正确的,那么就确实是在读取填充的部分,并且填充的部分确实是0。一直读取到下一个完整字节的比特起始位置(可以被8整除的位置)。如果检测的结果是在应该填充的地方发现了非0的比特值,那么就中止序列化读取并丢弃这个数据包。- 序列化数组的子集 : 当实现一个游戏网络协议的时候,或早或晚总会需要序列化一个对象数组然后在网络上传递。比如说服务器也许需要把所有的物体发送给客户端,或者有时候需要发送一组事件或者消息。如果你要发送所有的物体到客户端,这是相当简单直观的,但是如果你只是想发送一个数组的一个子集怎么办?
最先想到也是最容易的办法是遍历数组的所有物体然后序列化一个bool数组,这个bool数组标记的是对应的物体是否通过网络发送。如果bool值为1那么后面会跟着物体的数据,否则就会被忽略然后下一个物体的bool值取决于流的下一个值。
如果有大量的物体需要发送,举个例子来说,整个场景中有4000个物体,有一半的物体也就是2000个需要通过网络进行发送。每个物体需要一个序号,那么就需要2000个序号,每个序号需要12比特。。。。这就是说数据包里面24000比特或者说接近30000比特(几乎是30000,不是严格是,译注:原文如此)的数据被序号浪费掉了.
可以把序号的编码方式修改下来节省数据,序号不再是全局序号,而是相对上一个物体的相对序号。 - 如何应对恶意数据包和错误包 : 如果某些人发送一些包含随机信息的恶意数据包给你的服务器。你会不会在解析的时候把服务器弄崩溃掉?
有三种技术应对 :- 协议ID : 在你的数据包里面包含协议ID。一般典型的做法是,头4个字节你可以设定一些比较罕见而且独特的值,你可以通过这32比特的数据判断出来根本就不是你的应用程序的包,然后就可以直接丢弃了。
- CRC32 : 对你的数据包整体做一个CRC32的校验,并把这个校验码放到数据包的包头。可以不发送这个协议ID,但是发送方和接收方提前确认过这个协议ID是什么,并在计算数据包CRC32值的时候装作这个数据包带上了这个协议ID的前缀来参与计算。这样如果发送方使用的协议ID与接收方不一致的时候,CRC32的校验就会失败,这将为每个数据包节省4个字节.
- 序列化检测 : 是在包的中间,在一段复杂的序列化写入之前或者之后写上一个已知的32比特整数,并在另外一端序列化读取的时候用相同的值进行检测判断。如果序列化检查值是不正确的,那么就中止序列化读取并丢弃这个数据包。
. . .