《爐石傳說》架構設計賞析(7):使用Google.ProtocolBuffers處理網路訊息,
這段時間琢磨了一下Unity3D網路遊戲開發中的網路訊息處理。網路遊戲的服務端一般都是自主開發的,所以對應的網路訊息處理也要自己開發。用戶端/服務端之間的訊息傳到目前使用JSON和Google.ProtocolBuffers是兩種常見的做法。開啟爐石的代碼看了看它的處理方式,感覺代碼寫的還是很好的,把它的思路分析一下,與大家分享。
整體機制描述我們想要達到的目標大概是這樣的:
- 有N個網路訊息,每個訊息對應一個Proto中的message描述;
- 每個訊息對應一個數字ID;
- 底層在收到訊息是,將其解析成為Google.ProtocolBuffers.IMessage對象,這個對象的具體類型應該是前面那個message產生的程式碼;
- 發送訊息就簡單了,因為知道其類型,可以直接執行序列化;
爐石使用Google.ProtocolBuffers類庫,可以看這裡:http://www.nuget.org/packages/Google.ProtocolBuffers/
訊息發送發送的機制很簡單,首先使用ProtocolBuffer產生的message類構造一個訊息對象,例如:ConnectAPI.SendPing()
public static void SendPing(){ Ping.Builder body = Ping.CreateBuilder(); QueueGamePacket(0x73, body); s_lastGameServerPacketSentTime = DateTime.Now;}底層會構造一個“PegasusPacket”資料包對象,添加到發送隊列之中,這個資料包對象主要包含3部分:訊息ID,訊息大小,具體訊息資料。詳見PegasusPacket.Encode()函數:
public override byte[] Encode(){ if (!(this.Body is IMessageLite)) { return null; } IMessageLite body = (IMessageLite) this.Body; this.Size = body.SerializedSize; byte[] destinationArray = new byte[8 + this.Size]; Array.Copy(BitConverter.GetBytes(this.Type), 0, destinationArray, 0, 4); Array.Copy(BitConverter.GetBytes(this.Size), 0, destinationArray, 4, 4); body.WriteTo(CodedOutputStream.CreateInstance(destinationArray, 8, this.Size)); return destinationArray;}訊息接收與解析接下來我們重點看一下訊息的接收與解析機制。首先因為TCP是流式的,所以底層應該檢測資料包頭,並收集到一個完整的資料包,然後再發送到上層解析,這部分邏輯是在”ClientConnection<PacketType>.BytesReceived()“中實現的。當收到完整資料包時,會在主線程中觸發”OnPacketCompleted“事件,實際上會調用到”ConnectAPI.PacketReceived()“,其內部主要是調用了”ConnectAPI.QueuePacketReceived()“,這個函數負責將TCP層接收到的byte[]解析成對應的IMessage對象。
重點來了!由於網路層發過來的資料包,只包含一個訊息ID,那麼用戶端就需要解決從ID找到相應的訊息Type的問題。想象中無非有兩種方式去做:1是手動記錄每個ID對應的Type;2是搞一個中間的對應關係的類,附加上自訂的Attribute,然後在使用反射機制自動收集這些類,其實和前者也差不多。爐石採用了第一種方式。整體機制是這樣的:
最後我們看一下,Decoder模板的實現技巧。首先訊息解析的具體操作是有Google.ProtocolBuffers產生的程式碼去實現的,所以具體操作流程是完全一致的,這些寫到基類的的靜態模板函數中:
public abstract class PacketDecoder{ // Methods public abstract PegasusPacket HandlePacket(PegasusPacket p); public static PegasusPacket HandleProtoBuf<TMessage, TBuilder>(PegasusPacket p) where TMessage: IMessageLite<TMessage, TBuilder> where TBuilder: IBuilderLite<TMessage, TBuilder>, new() { byte[] body = (byte[]) p.Body; TBuilder local2 = default(TBuilder); TBuilder local = (local2 == null) ? Activator.CreateInstance<TBuilder>() : default(TBuilder); p.Body = local.MergeFrom(body).Build(); return p; }}其次,使用一個模板衍生類別,實現HandlePacket()這個虛函數,主要的目的只是把TMessage和TBuilder這兩個類型傳給那個靜態函數而已:
public class DefaultProtobufPacketDecoder<TMessage, TBuilder> : ConnectAPI.PacketDecoder where TMessage: IMessageLite<TMessage, TBuilder> where TBuilder: IBuilderLite<TMessage, TBuilder>, new(){ // Methods public override PegasusPacket HandlePacket(PegasusPacket p) { return ConnectAPI.PacketDecoder.HandleProtoBuf<TMessage, TBuilder>(p); }}
OK,爐石是使用使用ProtocolBuffers來處理網路訊息的機制就是這樣,是不是已經很清晰啦!