当前位置: 首页 > 工具软件 > Raknet > 使用案例 >

RakNet学习(11) -- Creating Packets

滑令
2023-12-01

创建数据包

 

如何将游戏数据编码到数据包中

       运行RakNet的系统,事实上所有在因特网上的系统,都是通过人们所熟知的数据包进行通信。或更加准确点在UDP下,它用的是数据报。每一个数据报由RakNet创建,并且包含了一条或多条消息。消息可以由你创建,例如位置或健康(health这个词确实不知道如何翻译好),或者有时由RakNet内部创建的数据,例如pings。按照惯例,消息的第一个字节包含了一个从0到255的数字标识符,它用于表明消息的类型。RakNet有一大组内部使用的消息,或者插件使用的标识符。这些可以在文件MessageIdentifiers.h查看到详细信息。

       这个例子,我们在游戏中设置一个定时炸弹。我们需要下面的数据:

       1. 地雷的位置,需要3个floats值,float x,float y,float z。可以定义自己使用的向量类型。

       2. 定义一些所有系统都一致可以访问“地雷”的方法。NetworkIDObject类是非常好的一个方法。我们假设类Mine从NetworkIDObject派生。我们要存储的就是获取地雷的NetworkID。(更多信息参考Receiving Packets,Sending Packets)。

       3. 谁拥有这个地雷。如果有人猜到了地雷,我们要知道积分应该给谁。创建到player,SystemAddress的索引是非常好的,可以使用GetExternalID()方法获取SystemAddress。

       4. 地雷爆炸的时间。我们假设10秒之后地雷自动爆炸,那么获取准确的时间就非常重要了,否则炸弹会出现不同电脑上的爆炸时间不同。幸运的是,RakNet内建了这种功能,可以使用Timestamping来实现。

 

使用结构体或位流?

       最后,任何时候发送数据都是发送一个字符流。有两种很容易的方法将数据编码成为这种格式。一种是创建一种结构体,然后将它转化为(char *),另外一种就是使用内置的BitStream类。

       创建结构体进行转化的优点是很容易修改结构,并且可以看到你事实上正在发送的数据。由于发送者和接收者能够共享定义了结构体的文件,避免了转化的错误。也没有让数据乱序,或者使用错误类型的危险。创建结构体的不足就是常常不得不改变和重新编译文件。并且丧失了使用Bitstream类进行自动压缩的便利。并且RakNet不能自动转换结构体成员的字节序。

       使用Bitstream的优点是不需要改变任何外部文件。仅仅需要一个bitstream,在其中写入你想要写入的数据,然后发送即可。可以使用“Compressed”版本的read和write方法写入相对较少的数据,例如使用它写入bool类型,仅仅需要一位。可以动态写入数据,在某些确定情况下写的值是true或者false。使用Serialize(),Write(), Read()等方法写的数据,Bitstream会自动进行网络字节序的转换。Bitstream的不足就是很容易出现数据处理错误。读取数据的方式与写入的方式不完全相同-错误的序列,或者一个字节的错误数据,或者其他的错误。

下面会介绍两种方法创建数据包。

使用结构体创建数据包

       我早先已经提到了,RakNet标识数据包类型的方法是一种惯例。数据域的第一个字节是一个单字节的枚举类型数据,它指明了数据的类型,紧跟着的是传输的数据。在包含了时间戳的数据包中,第一个字节包含了ID_TIMESTAMP,接下来的四个字节是实际的时间戳值,再向后的这个字节才是数据包数据类型的标识,它之后才是传输的实际数据。

       没有时间戳的情况

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

       注意#prama pack(push, 1)和#prama pack(pop),他们强制编译器(在VC++下)按照字节对齐的方式填充数据结构体。检查你自己的编译器,获得更多的相关配置信息。

 

带有时间戳

        #pragma pack(push, 1)
        struct structName
        {
        unsigned char useTimeStamp; // 赋值 ID_TIMESTAMP值
                        RakNet::Time timeStamp; // 将由RakNet::GetTime()返回的系统时间值或其他方式返回的类似值
                unsigned char typeId; // 你的类型放到这里
                // 这里放数据
        };
        #pragma pack(pop)

       注意发送数据的时候,RakNet假设timeStamp是网络字节序。必须使用timeStamp域的函数BitStream::EndianSwapBytes()实现字节序的变换。在接收系统上读取时间戳,使用if (bitStream->DoEndianSwap()) bitStream->ReverseBytes(timeStamp, sizeof(timeStamp)获得时间戳。如果使用的是BitStream这一步就不需要了。

       填充数据包。对于我们的定时地雷,我们需要使用timeStamping的格式。最终的结果应该如下这种形式…

        #pragma pack(push, 1)
        struct structName
        {
                unsigned char useTimeStamp; // 对它赋值 ID_TIMESTAMP 
                RakNet::Time timeStamp; // 将RakNet::GetTime()返回的系统时间值放到这里
                unsigned char typeId; // 应该将你定义的一个枚举类型数据放到这里,例如ID_SET_TIMED_MINE
                float x,y,z; // 地雷的位置
                NetworkID networkId; // 地雷的NetworkID, 用于当做一个方法来指向不同电脑上的地雷

                SystemAddress systemAddress; // 拥有该地雷的SystenAddress 
        };
        #pragma pack(pop)

         像上面的注释中写的,我们必须要为我们自己的数据定义枚举类型,那么当数据到达了接收函数中,我们就知道该如何处理了。你应该定义你自己的枚举类型,格式可以参考下面的格式:

        // 定义自己用的数据包ID 
        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)

 

使用BitsStreams创建数据包

        使用bitstream写入更少的数据

       我们还是以上述的地雷的例子作为例子,在这里使用bitstream将它发送出去。数据与前面是相同的。

 

        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()); // 在结构体中这里是SystemAddress systemAddress

 

        如果我们将myBitStream发送到RakPeerInterface::Send(),它会内在地在这块转换为结构体。现在进行一些改进。由于有些原因,假设大多数的地雷是在0,0,0。然后可以这样发送数据。

 

        unsigned char useTimeStamp; //赋值为 ID_TIMESTAMP
        RakNet::Time timeStamp; // 将RakNet::GetTime()返回的系统时间值放到这里
        unsigned char 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, 可以使用1位代替
        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()); //在结构体中此处为SystemAddress systemAddress

 

这里可以节省3个floats类型数据大小,而仅仅需要1位。

 

共同的错误:

       在写入第一个字节的时候,确保将它转换为(MessageID)或(unsigned char)。如果你仅仅直接写入枚举类型数据,会写入一个整数(四个字节)。

       正确:

       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。它是一个全局的实例。使用它将字符串写入BitStream如下:

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

       它不仅编码字符串,sniffers无法很容易读取字符串,而且压缩了字符串。解码字符串可以按照如下的方法。

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

        256是读取和写入的最大的字节数。在EncodeString中,如果字符串少于256,它会写入整个字符串。如果大于256个字符,将截断字符串,那么将解码为256个字符的数组,包括结束符。

       RakNet也包含一个字符串类,RakNet::RakString,可以在RakString找到。

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

       bitStream->write(rakString);

 

       RakString比std::string的速度快3倍。

       RakString支持Unicode。

 

编程人员注意:

       1. 可以直接将结构体写入BitsStream,只需要将结构体转化为(char *)。它会使用内存拷贝memcpy拷贝结构体。使用了结构体,就会将指针废弃,因此不要将指针写入bitstream。

       2. 如果使用string非常频繁,可以使用StringTable类来代替,它和StringCompressor类类似,但是可以使用两个字节来代替一个一直字符串。

 类似资料: