《Unity 3D遊戲客戶端基礎框架》 protobuf網路框架
前言:
protobuf是google的一個開源專案,主要的用途是:
1.資料儲存(序列化和反序列化),這個功能類似xml和json等;
2.製作網路通訊協議;
一、資源下載:
二、資料儲存:
C#語言方式的導表和解析過程,在之前的篇章中已經有詳細的闡述:Unity —— protobuf 導excel表格資料,建議在看後續的操作之前先看一下這篇文件,因為後面設計到得一些操作與導表中是一致的,而且在理解了導表過程之後,能夠快速地理解協議資料序列化和反序列化的過程。
三、網路協議:
1.設計思想:
有兩個必要的資料:協議號和協議型別,將這兩個資料分別儲存起來
- 當客戶端向伺服器傳送資料時,會根據協議型別加上協議號,然後使用protobuf序列化之後再發送給伺服器;
- 當伺服器傳送資料給客戶端時,根據協議號,用protobuf根據協議型別反序列化資料,並呼叫相應回撥方法。
由於資料在傳輸過程中,都是以資料流的形式存在的,而進行解析時無法單從protobuf資料中得知使用哪個解析類進行資料反序列化,這就要求我們在傳輸protobuf資料的同時,攜帶一個協議號,通過協議號和協議型別(解析類)之間的對應關係來確定進行資料反序列化的解析類。
此處協議號的作用就是用來確定用於解析資料的解析類,所以也可能稱之為協議型別名,可以是string和int型別的資料。
2.特點分析:
使用protobuf作為網路通訊的資料載體,具有幾個優點:
- 通過序列化之後資料量比較小;
- 而且以key-value的方式儲存資料,這對於訊息的版本相容比較強;
- 此外,由於protobuf提供的多語言支援,所以使用protobuf作為資料載體定製的網路協議具有很強的跨語言特性。
四、樣例實現:
1.協議定義:
在之前導表的時候,我們得到了.proto的解析類,這是protobuf提供的一種特殊的指令碼,具有格式簡單、可讀性強和方便拓展的特點,所以接下來我們就是使用proto指令碼來定義我們的協議。例如:
上述例子中,Item相當於定義了一個數據結構或者是類,而ItemList是一個列表,列表中的每個元素都是一個Item物件。注意結構關鍵詞:// 物品 message Item { required int32 Type = 1; //遊戲物品大類 optional int32 SubType = 2; //遊戲物品小類 required int32 num = 3; //遊戲物品數量 } // 物品列表 message ItemList { repeated Item item = 1; //物品列表 }
- required:必有的屬性
- optional:可選屬性
- repeated:陣列
- 使用protoc.exe將.proto檔案轉化為.protodesc中間格式;
- 使用protogen.exe將中間格式為.protodesc生成指定的高階語言類,我們在Unity中使用的是C#,所以結果是.cs類
package cs;
message CSLoginInfo
{
required string UserName = 1;//賬號
required string Password = 2;//密碼
}
//傳送登入請求
message CSLoginReq
{
required CSLoginInfo LoginInfo = 1;
}
//登入請求回包資料
message CSLoginRes
{
required uint32 result_code = 1;
}
package關鍵字後面的名稱為.proto轉為.cs之後的名稱空間namespace的值,用message可以定義類,這裡定義了一個CSLoginInfo的資料類,該類包含了賬號和密碼兩個字串型別的屬性。然後定義了兩個訊息結構:- CSLoginReq登入請求訊息,攜帶的資料是一個CSLoginInfo型別的物件資料;
- CSLoginRes登入請求伺服器返回的資料型別,返回結果是一個uint32無符號的整型資料,即結果碼。
package cs;
enum EnmCmdID
{
CS_LOGIN_REQ = 10001;//登入請求協議號
CS_LOGIN_RES = 10002;//登入請求回包協議號
}
使用protoc.exe和protogen.exe將這兩個protobuf指令碼得到C#類,具體步驟參考導表使用的操作,這裡我直接給出自動化導表使用的批處理檔案general_all.bat內容,具體檔案目錄可以根據自己放置情況進行調整:::---------------------------------------------------
::第二步:把proto翻譯成protodesc
::---------------------------------------------------
call proto2cs\protoc protos\cs_login.proto --descriptor_set_out=cs_login.protodesc
call proto2cs\protoc protos\cs_enum.proto --descriptor_set_out=cs_enum.protodesc
::---------------------------------------------------
::第二步:把protodesc翻譯成cs
::---------------------------------------------------
call proto2cs\ProtoGen\protogen -i:cs_login.protodesc -o:cs_login.cs
call proto2cs\ProtoGen\protogen -i:cs_enum.protodesc -o:cs_enum.cs
::---------------------------------------------------
::第二步:把protodesc檔案刪除
::---------------------------------------------------
del *.protodesc
pause
轉換結束後,我們的得到了兩個.cs檔案分別是:cs_enum.cs和cs_login.cs,將其放入到我們的Unity專案中,以便於接下來序列化和反序列化資料的使用。2.協議資料構建:
直接在專案程式碼中通過usingcs引入協議解析類的名稱空間,然後建立訊息物件,並對物件的屬性進行賦值,即可得到協議資料物件,例如登入請求物件的建立如下:
CSLoginInfo mLoginInfo = new CSLoginInfo();
mLoginInfo.UserName = "linshuhe";
mLoginInfo.Password = "123456";
CSLoginReq mReq = new CSLoginReq();
mReq.LoginInfo = mLoginInfo;
從上述程式碼,可以得到登入請求物件mReq,裡面包含了一個CSLoginInfo物件mLoginInfo,再次列舉物件中找到與此協議型別對應的協議號,即:EnmCmdID.CS_LOGIN_REQ3.資料的序列化和反序列化:
資料傳送的時候必須以資料流的形式進行,所以這裡我們需要考慮如何將要傳送的protobuf物件資料進行序列化,轉化為byte[]位元組陣列,這就需要藉助ProtoBuf庫為我們提供的Serializer類的Serialize方法來完成,而反序列化則需藉助Deserialize方法,將這兩個方法封裝到PackCodec類中:
using UnityEngine;
using System.Collections;
using System.IO;
using System;
using ProtoBuf;
/// <summary>
/// 網路協議資料打包和解包類
/// </summary>
public class PackCodec{
/// <summary>
/// 序列化
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="msg"></param>
/// <returns></returns>
static public byte[] Serialize<T>(T msg)
{
byte[] result = null;
if (msg != null)
{
using (var stream = new MemoryStream())
{
Serializer.Serialize<T>(stream, msg);
result = stream.ToArray();
}
}
return result;
}
/// <summary>
/// 反序列化
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="message"></param>
/// <returns></returns>
static public T Deserialize<T>(byte[] message)
{
T result = default(T);
if (message != null)
{
using (var stream = new MemoryStream(message))
{
result = Serializer.Deserialize<T>(stream);
}
}
return result;
}
}
使用方法很簡單,直接傳入一個數據物件即可得到位元組陣列: byte[] buf = PackCodec.Serialize(mReq);
為了檢驗打包和解包是否匹配,我們可以直接做一次本地測試:將打包後的資料直接解包,看看資料是否與原來的一致:
using UnityEngine;
using System.Collections;
using System;
using cs;
using ProtoBuf;
using System.IO;
public class TestProtoNet : MonoBehaviour {
// Use this for initialization
void Start () {
CSLoginInfo mLoginInfo = new CSLoginInfo();
mLoginInfo.UserName = "linshuhe";
mLoginInfo.Password = "123456";
CSLoginReq mReq = new CSLoginReq();
mReq.LoginInfo = mLoginInfo;
byte[] pbdata = PackCodec.Serialize(mReq);
CSLoginReq pReq = PackCodec.Deserialize<CSLoginReq>(pbdata);
Debug.Log("UserName = " + pReq.LoginInfo.UserName + ", Password = " + pReq.LoginInfo.Password);
}
// Update is called once per frame
void Update () {
}
}
將此指令碼綁到場景中的相機上,執行得到以下結果,則說明打包和解包完全匹配:
4.資料傳送和接收:
這裡我們使用的網路通訊方式是Socket的強聯網方式,關於如何在Unity中使用Socket進行通訊,可以參考我之前的文章:Unity —— Socket通訊(C#),Unity客戶端需要複製此專案的ClientSocket.cs和ByteBuffer.cs兩個類到當前專案中。
此外,伺服器可以參照之前的方式搭建,唯一不同的是RecieveMessage(object clientSocket)方法解析資料的過程需要進行修改,因為需要使用protobuf-net.dll進行資料解包,所以需要參考客戶端的做法,把protobuf-net.dll複製到伺服器專案中的Protobuf_net目錄下:
假如由於直接使用原始碼而不用.dll會出現不安全儲存,需要在Visual Studio中設定允許不安全程式碼,具體步驟為:在“解決方案”中選中工程,右鍵“資料”,選擇“生成”頁籤,勾選“允許不安全程式碼”:
當然,解析資料所用的解析類和協議號兩個指令碼cs_login.cs和cs_enum.cs也應該新增到伺服器專案中,保證客戶端和伺服器一直,此外PackCodec.cs也需要新增到伺服器程式碼中但是要把其中的using UnityEngine給去掉防止報錯,最終伺服器目錄結構如下:
5.完整協議資料的封裝:
從之前說過的設計思路分析,我們在傳送資料的時候除了要傳送關鍵的protobuf資料之外,還需要帶上兩個附件的資料:協議頭(用於進行通訊檢驗)和協議號(用於確定解析類)。假設我們的是:
協議頭:用於表示後面資料的長度,一個short型別的資料:
/// <summary>
/// 資料轉換,網路傳送需要兩部分資料,一是資料長度,二是主體資料
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
private static byte[] WriteMessage(byte[] message)
{
MemoryStream ms = null;
using (ms = new MemoryStream())
{
ms.Position = 0;
BinaryWriter writer = new BinaryWriter(ms);
ushort msglen = (ushort)message.Length;
writer.Write(msglen);
writer.Write(message);
writer.Flush();
return ms.ToArray();
}
}
協議號:用於對應解析類,這裡我們使用的是int型別的資料:
private byte[] CreateData(int typeId,IExtensible pbuf)
{
byte[] pbdata = PackCodec.Serialize(pbuf);
ByteBuffer buff = new ByteBuffer();
buff.WriteInt(typeId);
buff.WriteBytes(pbdata);
return buff.ToBytes();
}
客戶端傳送登入資料時測試指令碼TestProtoNet如下,測試需要將此指令碼繫結到當前場景的相機上:using UnityEngine;
using System.Collections;
using System;
using cs;
using Net;
using ProtoBuf;
using System.IO;
public class TestProtoNet : MonoBehaviour {
// Use this for initialization
void Start () {
CSLoginInfo mLoginInfo = new CSLoginInfo();
mLoginInfo.UserName = "linshuhe";
mLoginInfo.Password = "123456";
CSLoginReq mReq = new CSLoginReq();
mReq.LoginInfo = mLoginInfo;
byte[] data = CreateData((int)EnmCmdID.CS_LOGIN_REQ, mReq);
ClientSocket mSocket = new ClientSocket();
mSocket.ConnectServer("127.0.0.1", 8088);
mSocket.SendMessage(data);
}
private byte[] CreateData(int typeId,IExtensible pbuf)
{
byte[] pbdata = PackCodec.Serialize(pbuf);
ByteBuffer buff = new ByteBuffer();
buff.WriteInt(typeId);
buff.WriteBytes(pbdata);
return WriteMessage(buff.ToBytes());
}
/// <summary>
/// 資料轉換,網路傳送需要兩部分資料,一是資料長度,二是主體資料
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
private static byte[] WriteMessage(byte[] message)
{
MemoryStream ms = null;
using (ms = new MemoryStream())
{
ms.Position = 0;
BinaryWriter writer = new BinaryWriter(ms);
ushort msglen = (ushort)message.Length;
writer.Write(msglen);
writer.Write(message);
writer.Flush();
return ms.ToArray();
}
}
// Update is called once per frame
void Update () {
}
}
伺服器接受資料解包過程參考打包資料的格式,在RecieveMessage(object clientSocket)中,解析資料的核心程式碼如下: ByteBuffer buff = new ByteBuffer(result);
int datalength = buff.ReadShort();
int typeId = buff.ReadInt();
byte[] pbdata = buff.ReadBytes();
//通過協議號判斷選擇的解析類
if(typeId == (int)EnmCmdID.CS_LOGIN_REQ)
{
CSLoginReq clientReq = PackCodec.Deserialize<CSLoginReq>(pbdata);
string user_name = clientReq.LoginInfo.UserName;
string pass_word = clientReq.LoginInfo.Password;
Console.WriteLine("資料內容:UserName={0},Password={1}", user_name, pass_word);
}
}
上面通過typeId來找到匹配的資料解析類,協議少的時候可以使用這種簡單的使用if語句分支判斷來實現,但是假如協議型別多了,則需要進一步封裝查詢方法,常用的方法有:定義一個Dictionary<int,Type>字典來存放協議號(int)和協議型別(Type)的對應關係。6.執行結果:
啟動伺服器,然後執行Unity中的客戶端,得到正確的結果應該如下: