1. 程式人生 > >[網路開發]RakNet文件翻譯(3)

[網路開發]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.當這個炸彈被放置後,這個炸彈會在我們倒計時

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類似,但是可以傳送代表一個已知字串的兩位元組資料。