注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

BCB-DG's Blog

...

 
 
 

日志

 
 

Raknet(3)——Creating Packets  

2015-02-13 12:24:23|  分类: Socket |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
//转

如何将你的数据编码到一个数据包中?

运行RakNet的系统通过人们所熟知的数据包进行通讯,实际上所有在Internet上运行的系统都如此。更准确的说,在UDP协议下,它用的是数据报。每一个通过RakNet创建的数据报中都包含了一条或者多条信息。消息可以是通过你创建的,例如位置信息,血量信息,或者其他通过RakNet内部创建的,例如ping消息。按照惯例,消息的第一个字节包含了一个0-255之间的数字标示符,它被用来标识消息的类型。RakNet已经在自己内部或者为插件使用了大量消息,这些可以在MessageIdentifiers.h文件中看到。

这个例子中,我们放置了一个定时炸弹在游戏里,我们需要以下的数据:

1.炸弹的位置信息,包含三个浮点数,x,y,z,你可能有自己定义的可以替代三个浮点数的向量。

2.一些所有系统都可以访问炸弹的方法。NetworkIDObject类是一个非常好的方法。让我们假设一个炸弹类继承自NetworkIDObject类。然后我们需要存储炸弹的NetworkID(更多的信息请查看Receiving Packets, Sending Packets, 后面会讲到)

3.谁拥有这个炸弹。这样我们就知道有人踩中了它,该给谁积分。创建引用到玩家身上,最好是系统内存地址,这样就可以通过GetExternalID()来获得改内存地址,也就是拥有者。

4.当这个炸弹被放置后,这个炸弹会在我们倒计时10秒钟后自动爆炸,因此获得正确的时间是非常重要的,这样就不会出现在不同的电脑上爆炸时间不同的问题。幸好RakNet已经有了TimeStamping来处理这个问题。

 使用一个结构体或者字节流?

任何你想发送的数据最终都会变成字节流发送,将你的数据变成字节流有两种简单的方法。一种是创建一个结构体然后将它转化为(char*),另一种方法是使用内建的BitStream类。

第 一种方法的优点是改变结构体非常容易,同时你也可以确切的看到你想要发送的数据。由于发送者和接收者都可以共享源文件中定义的结构体,所以可以避免转化错 误。同样也没有让数据乱序,也不会出现使用错误的数据类型。缺点是你经常不得不修改结构体并且重编译许多文件。这样你就失去了可以总是用字节流类来自动执 行的便利,同时Raknet不能自动为结构体成员转化字节序。(译者注:网络传输字节序与内存字节序是相反的,所以发送的时候需要转化一下。)

第二种方法的优点是你不需要改变任何外部文件。简单的创建字节流,写入你想要发送的任何排序的数据,然后发送它。可以使用“压缩”版本的读写方法来写入较少的比特数据,例如它写入bool值,只需要一个比特位。当某些情况下是true或者false,你可以动态的写入数据。使用Serialize(), Write()或者Read()写入,BitStream可以自动将数据成员转化为网络字节序。BitStream的缺点是你现在使用它很容易犯错误,比如读写的方法不完全相同,错误的排序,错误的数据类型,或者其他错误。

我们将要使用两种方法来创建数据包。

 用结构体创建数据包

正如我可能前面提到的,RakNet有一个标识数据包类型的约定惯例。数据段的第一个字节是一个单字节枚举,它标识了数据包的类型,接下来是数据传输。数据包中包含了一个时间戳,第一个字节包含了ID_TIMESTAMP,接下来的4个字节是真正的时间戳值,然后下一个字节是数据包类型的标识,接下来才是真正传输的数据。

没有时间戳的情况

#pragma pack(push, 1)
struct structName
{
unsigned char typeId; // 数据类型
// 你的数据
};
#pragma pack(pop)

注意 #pragma pack( push, 1 ) #pragma pack( pop ),他强制你的编译器(在本例子中是VC++),将结构体按照1字节对齐。检查你的编译器文档学习更多。

 有时间戳的情况

#pragma pack(push, 1)
struct structName
{
unsigned char useTimeStamp; // 赋值ID_TIMESTAMP给它

RakNet::Time timeStamp; // 通过RakNet::GetTime()获得系统时间或者其他返回类似值的函数

unsigned char typeId; // 数据包类型

// 你的数据
};
#pragma pack(pop)

注意:当发送数据时,RakNet假设时间戳是网络字节序的。你应该使用BitStream::EndianSwapBytes()函数将时间戳数据进行转序。在接受时间戳数据的系统上,使用

if ( bitSteam->DoEndianSwap() )

bitSteam->ReverseBytes( timeStamp, sizeof( timeStamp ) );

如果使用的是BitSteam这个步骤可以省略。

填充数据包,对于我们的定时炸弹,我们想试用有时间戳的方法。因此最终的结果看起来应该如下所示:

#pragma pack(push, 1)
struct structName
{
unsigned char useTimeStamp; // 赋值ID_TIMESTAMP
RakNet::Time timeStamp; // 通过RakNet::GetTime()获得系统时间
unsigned char typeId; // 一个自定义的枚举类型,该枚举定义在 MessageIdentifiers.h最后,例如ID_SET_TIMED_MINE
float x,y,z; // 炸弹的坐标
NetworkID networkId; // 炸弹的网络ID,作为一种通用的方法来制定不同计算机上的炸弹
SystemAddress systemAddress; // 拥有该炸弹玩家的系统地址
};
#pragma pack(pop)

像我上面的注释写到,我们必须定义一个自己数据包的枚举,当数据流到达接收函数时,我们就知道我们关注的数据包是哪一个了。你应该从ID_USER_PACKET_ENUM开始定义枚举,就像下面的例子:

//定义你自己的枚举
enum {
ID_SET_TIMED_MINE = ID_USER_PACKET_ENUM,
// 更多的枚举
};

注意:结构体中不应该直接或者间接包含指针。

结构体或者类中包含指针貌似是一个普遍的错误,人们认为指向数据的指针应该可以在网络中被发送。也不是没有这种情况,它会被当做一个指针地址被发送出去的。

 嵌套结构体

使用嵌套结构体没有任何问题,不过请保持第一个字节总是决定了数据包的类型。

#pragma pack(push, 1)
struct A
{
unsigned char typeId; // ID_A
};
struct B
{
unsigned char typeId; // ID_A
};
struct C // Struct C is of type ID_A
{
A a;
B b;
}
struct D // Struct D is of type ID_B
{
B b;
A a;
}
#pragma pack(pop)

使用字节流来创建数据包

我们继续使用上面的炸弹例子,使用字节流将他发送出去,我们使用与前面相同的数据。

MessageID useTimeStamp; // 将ID_TIMESTAMP赋值给它
RakNet::Time timeStamp; // 使用RakNet::GetTime()获得的系统时间
MessageID typeId; // 这个赋值给一个类型,在ID_USER_PACKET_ENUM后添加,让我们叫它ID_SET_TIMED_MINE
useTimeStamp = ID_TIMESTAMP;
timeStamp = RakNet::GetTime();
typeId=ID_SET_TIMED_MINE;
Bitstream myBitStream;
myBitStream.Write(useTimeStamp);
myBitStream.Write(timeStamp);
myBitStream.Write(typeId);
// 假设我们已经有了一个Mine* mine对象
myBitStream.Write(mine->GetPosition().x);
myBitStream.Write(mine->GetPosition().y);
myBitStream.Write(mine->GetPosition().z);
myBitStream.Write(mine->GetNetworkID()); // 这个是结构体中的NetworkID networkId
myBitStream.Write(mine->GetOwner()); // 这个是结构体中的系统地址

如果哦我们想将myBitStream发送到RakPeerInterface::Send,这时候它会在内部被转化为结构体。现在让我们试着做一点改进。因为一些原因让我们假设定时炸弹的坐标为0,0,0。我们可以替换为下面的。

MessageID useTimeStamp; // 将ID_TIMESTAMP赋值给它
RakNet::Time timeStamp; // 使用RakNet::GetTime()获得的系统时间
MessageID typeId; // 这个赋值给一个类型,在ID_USER_PACKET_ENUM后添加,让我们叫它ID_SET_TIMED_MINE

useTimeStamp = ID_TIMESTAMP;
timeStamp = RakNet::GetTime();
typeId=ID_SET_TIMED_MINE;
Bitstream myBitStream;
myBitStream.Write(useTimeStamp);
myBitStream.Write(timeStamp);
myBitStream.Write(typeId);
// 假设我们已经有了一个Mine* mine对象
// 如果炸弹的坐标是0,0,0,使用一位数据来代表这个情况
if (mine->GetPosition().x==0.0f && mine->GetPosition().y==0.0f && mine->GetPosition().z==0.0f)
{
myBitStream.Write(true);
}
else
{
myBitStream.Write(false);
myBitStream.Write(mine->GetPosition().x);
myBitStream.Write(mine->GetPosition().y);
myBitStream.Write(mine->GetPosition().z);
}
myBitStream.Write(mine->GetNetworkID()); // 这个是结构体中的NetworkID networkId
myBitStream.Write(mine->GetOwner()); // 这个是结构体中的系统地址

这个方法在网络传输中可以节省3float,而是用1位数据代替。

 通常的错误

当用bitstream写第一个字节的时候,必须转化为MessageID或者 unsigned char类型,如果你仅是直接写入枚举数据类型,那将会是一个整数(4字节)

正确的方法是:

bitStream->Write((MessageID)ID_SET_TIMED_MINE);

错误的方法是:

bitStream->Write(ID_SET_TIMED_MINE);

第二种情况下,RakNet中读取出来的第一个字节是0,这个是为ID_INTERNAL_PING保留的,千万记住。

 写入字符串

可以使用BitStream的数组来写入字符串。一种是先写入长度,然后再写入数据,例如:

void WriteStringToBitStream(char *myString, BitStream *output)
{
output->Write((unsigned short) strlen(myString));
output->Write(myString, strlen(myString);
}

解码是类似的,然而,效率却不高。RakNet存在一个内建的StringCompressor函数…stringCompressor。它是一个全局实例,使用它写入字串变成了:

void WriteStringToBitStream(char *myString, BitStream *output)
{
stringCompressor->EncodeString(myString, 256, output);
}

不仅是字符串编码,所以数据包嗅探器不是很容易读取字符串,而且它压缩了字符串。解码一个字串可以使用:

void WriteBitStreamToString(char *myString, BitStream *input)
{
stringCompressor->DecodeString(myString, 256, input);
}

在这个例子里,256是读写的最大长度。在EncodeString中,如果你的字串长度小于256,他将会写入整个字串。如果超过256个字符,它会被截短,它将会被当成一个256个字符的数据进行解码,包含结束符。

RakNet还有一个字符串类,RakNet::RakString,在RakString.h中可以找到

RakNet::RakString rakString("The value is %i", myInt);

bitStream->write(rakString);

RakStringstd::string约快3倍。

RakString支持Unicode

 程序员们请注意:

1.可以直接将结构体写入BitsStream,只需要简单的将他转化为(char*)。使用memcpy拷贝你的结构体。在结构体中,因为不能包含指针,所以不允许将指针写入bitstream

如果你经常使用字符串,你可以使用StringTable代替。它和StringCompressor类似,但是可以发送代表一个已知字串的两字节数据。

  评论这张
 
阅读(1802)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017