[網路開發]RakNet文件翻譯(3)
如何將你的資料編碼到一個數據包中?
執行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.當這個炸彈被放置後,這個炸彈會在我們倒計時
使用一個結構體或者位元組流?
任何你想傳送的資料最終都會變成位元組流傳送,將你的資料變成位元組流有兩種簡單的方法。一種是建立一個結構體然後將它轉化為(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()); // 這個是結構體中的系統地址
這個方法在網路傳輸中可以節省3個float,而是用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);
RakString比std::string約快3倍。
RakString支援Unicode
程式設計師們請注意:
1.可以直接將結構體寫入BitsStream,只需要簡單的將他轉化為(char*)。使用memcpy拷貝你的結構體。在結構體中,因為不能包含指標,所以不允許將指標寫入bitstream。
如果你經常使用字串,你可以使用StringTable代替。它和StringCompressor類似,但是可以傳送代表一個已知字串的兩位元組資料。