最近接觸的項目,是一個棋牌游戲,棋牌游戲需要在前端進行一些操作,然后向服務器發送數據包,在服務器接收到客戶端的數據后,會在服務端進行一系列的判斷之后,然后發送給客戶端一個返回數據,客戶端接收到這個返回數據后,需要作出一系列的響應。那么,就針對於這一整個服務器<--->客戶端的通訊過程,看看是如何來實現的。
1. 報文(消息結構體)的商定。
客戶端向服務器發送消息,客戶端接收服務器的消息,都是一系列的數據,那么這些數據到底是代表了什么,應該以什么樣的方式解讀?這些東西,在剛開始開發的時候,客戶端與服務器就需要定好相關的內容,這個東西呢,也就是消息報文啦!在消息報文中,會定義一些常用的東西:
a、游戲狀態
b、游戲通訊協議定義
c、游戲具體的數據包結構
1 /// <summary> 2 /// 游戲狀態定義 3 /// </summary> 4 public enum GameState : byte 5 { 6 GS_WAIT_SETGAME = 0, 7 8 GS_WAIT_AGREE = 1, 9 10 GS_NOTE_STATE = 20, 11 } 12 13 /// <summary> 14 /// 游戲通訊協議定義 15 /// </summary> 16 public struct ProtocolID 17 { 18 public const int S_C_IS_SUPER_USER = 78;//超端用戶消息 19 public const int C_S_SUPER_SET = 79;//超端設置 20 public const int S_C_SUPER_SET_RESULT = 80;//超端設置結果 21 } 22 23 /// <summary> 24 ///游戲基礎數據 25 /// </summary> 26 [System.Serializable] 27 [StructLayout(LayoutKind.Sequential, Pack = ClientPlat.PlatFormConfig.HallPack)] 28 public struct GameStationBase 29 { 30 /// <summary> 31 ///上庄列表 32 /// </summary> 33 [MarshalAs(UnmanagedType.ByValArray, SizeConst = GameConst.PlayerCount)] 34 public byte[] byZhuangList; 35 /// <summary> 36 /// 路子信息 37 /// </summary> 38 [MarshalAs(UnmanagedType.ByValArray, SizeConst = GameConst.GameMaxCount)] 39 public LuziData[] TLuziData; 40 41 public int iXiaZhuTime; /// 下注時間 42 public int iKaiPaiTime; /// 開牌時間 43 public int iFreeTime; /// 空閑時間 44 public int iShowWinTime; /// 顯示中獎時間 45 public int iNtStation; //庄家位置 46 public int iNtPlayCount; //庄家坐庄次數 47 public long i64NtMoney; //庄家金幣 48 public long i64NtWinMoney; //庄家輸贏情況 49 public long i64UserWin; //個人輸贏 50 public long i64MyMoney; //個人金幣數 -從服務端發送過去 51 public long i64UserMaxNote; ///玩家最大下注數 52 public long i64ShangZhuangLimit; /// 上庄需要的最少金幣 53 54 }
說明:消息報文需要嚴格遵循前后關系,如果客戶端與服務器消息報文順序不一致,那么會導致數據混亂。
2. 消息報文確定以后,需要根據消息報文腳本中確定的ProtocolID來定義對應的事件,而且這些事件都會實現某一個接口,方便事件中心觸發。
1 public class GameStationBase_Event : IEvent 2 { 3 public Enum EventType 4 { 5 get 6 { 7 return DataEventID.GameStationBase; 8 } 9 } 10 11 public GameStationBase data; 12 13 public GameStationBase_Event(GameStationBase _data) 14 { 15 data = _data; 16 } 17 18 public virtual string ToSring() 19 { 20 string msg = string.Format("EventType:{0}", DataEventID.GameStationBase.ToString()); 21 return msg; 22 } 23 24 public void DestroySelf() 25 { 26 } 27 }
a、從上面的代碼可以看到,這個GameStationBase_Event 類實現了接口IEvent,並且實現了它的DestroySelf方法,這樣的好處后面點兒會講到。
b、GameStationBase_Event 有一個public的枚舉類型EventType 變量,並且直接返回的就是 消息報文中的“游戲通訊協議定義”,這樣一來,GameStationBase_Event 就與“游戲通訊協議定義”中的每一個枚舉一一對應。
c、GameStationBase_Event還有一個 GameStationBase 類型的變量data,同時還有一個有參的構造器,直接初始化變量data,這樣一來,GameStationBase_Event 就與“游戲具體的數據包結構”一一對應了。
廢話了這么多,其實也就是一句話,一個 事件類,對應着一個游戲通訊協議與一個消息結構體,並且實現了一個通用的接口IEvent。
3、既然消息該如何發送,該如何接受解析已經定義好了,那么接下來是不是就該發送接受消息了呢?沒錯,接下來,就開始定義我們向服務器發送消息時該做些什么,接收到服務器的消息時,我們又該做些什么。
接下來,頂一個消息處理腳本,叫做 GameLogicTable,至於為什么叫這個,我也不知道 - -!
d、其它的東西,比如來個靜態的實例啊什么的,見代碼:
1 public class GameLogicTable : GameLogicBase 2 { 3 4 //聲明一個靜態實例 5 private static GameLogicTable instance; 6 //公有的靜態實例獲取方法 7 public static GameLogicTable Instance 8 { 9 get 10 { 11 if (instance == null) 12 { 13 instance = new GameLogicTable(0, 180); 14 } 15 return instance; 16 } 17 18 } 19 20 /// <summary> 21 /// 通訊協議(服務器==》客戶端) 22 /// </summary> 23 /// <param name="data"></param> 24 /// <param name="bAssistantID"></param> 25 private void ProcessGameLogic(byte[] data, uint bAssistantID) 26 { 27 //Debug.LogError("========數據接受處理=======>>" + bAssistantID.ToString()); 28 switch (bAssistantID) 29 { 30 case ProtocolID.S_C_APPLY_ZHUANG_RESULT: 31 { 32 S_C_ApplyZhuangResult ndata = new S_C_ApplyZhuangResult(); 33 ndata = (S_C_ApplyZhuangResult)HNNetToolKit.Instance.BytesToStruct(data, ndata.GetType()); 34 ApplyZhuangResult_Event evt = new ApplyZhuangResult_Event(ndata); 35 EventCenter.Instance.TriggerEvent(evt); 36 } 37 break; 38 default: 39 break; 40 } 41 } 42 /// <summary> 43 /// 初始化游戲 44 /// </summary> 45 /// <param name="data">Data.</param> 46 public void InitGameState(byte[] data) 47 { 48 49 GameState gs = (GameState)base.GameStatus; 50 //Debug.LogError("=========數據接收=========>>"+ gs.ToString()); 51 52 53 switch (gs) 54 { 55 case GameState.GS_WAIT_SETGAME://無庄等待狀態 56 case GameState.GS_WAIT_AGREE: 57 case GameState.GS_WAIT_NEXT: 58 { 59 GameStationBase pState = new GameStationBase(); 60 if (!HNNetToolKit.Instance.AssertSize(data.Length, pState.GetType())) return; 61 pState = (GameStationBase)HNNetToolKit.Instance.BytesToStruct(data, pState.GetType()); 62 GameStationBase_Event evt = new GameStationBase_Event(pState); 63 EventCenter.Instance.TriggerEvent(evt); 64 } 65 break; 66 } 67 } 68 /// <summary> 69 /// 通訊協議(客戶端==》服務端) 70 /// </summary> 71 #region 72 73 //超端設置 74 public void SuperSet(SuperUserSetData node) 75 { 76 SendData(ProtocolID.C_S_SUPER_SET,node); 77 }
a、客戶端--->服務端:向客戶端發送一個消息ID,然后附上對應結構的數據node。
b、服務端--->客戶端:根據接收到的消息ID,來做出對應的處理:
1.新建一個對應消息ID的數據報文 ndata;
2.將接受到的消息data的內容賦值給ndata;
3.新建一個對應消息ID的 事件類,並且將ndata作為參數傳遞給構造器。
4.使用事件中心,觸發事件。(為什么不在這里處理,還要添加事件呢?)
4. 現在雖然定義好了收到消息與發送消息的行為,那么現在還差一個東西,那就是接受到服務器消息時,觸發事件的EventCenter,這個EventCenter的作用很簡單,就是讓需要監聽服務器消息的地方可以注冊監聽,然后當服務器發送對應消息時,就觸發事件即可。那么這個EventCenter是如何設計的呢?
a、首先,增加一個Dictionary<enum,IEventHandlerManger> 類型的 eventHandlers,用來保存消息ID與對應 要觸發的 方法。
b、然后,增加一個HNSafeQueue<IEvent> 類型eventQueue,用來保存需要觸發的事件列表。
c、寫一個public的Addlistener(enum,IEventHandlerManger)方法,用於事件的注冊
d、寫一個public的TriggerEvent(IEvent eve)方法,用於觸發事件。在這個TriggerEvent方法中,其實就是將eve傳入到eventQueue隊列中,等待被觸發。
e、寫一個BroadcastEvent()方法,該方法就是將eventQueue隊列中的eve事件一個個取出,然后調用封裝好的事件觸發方法觸發事件回調。
e、在該腳本的Updata() 方法中,一直調用BroadcastEvent()方法。
f、其它一些邏輯處理,例如初始化啦、清空啦,刪除事件注冊啦等等,這里就不一一介紹了。
1 public class EventCenter : HNBehaviourSingleton<EventCenter>, IEventCenter 2 { 3 /// <summary> 4 /// 保存事件監聽處理列表,對應說明a 5 /// </summary> 6 private Dictionary<Enum, IEventHandlerManger> eventHandlers = new Dictionary<Enum, IEventHandlerManger>(); 7 8 /// <summary> 9 /// 事件隊列,對應說明b 10 /// </summary> 11 private HNSafeQueue<IEvent> eventQueue = new HNSafeQueue<IEvent>(); 12 13 //對應說明f 14 public void Update() 15 { 16 BoardCastEvent(); 17 } 18 19 //廣播事件方法,對應說明中的e 20 public void BoardCastEvent() 21 { 22 if ( eventQueue.Count < 1 ) 23 { 24 return; 25 } 26 IEvent evt = eventQueue.Dequeue(); 27 BoardCastEvent(evt); 28 } 29 //觸發事件方法,對應說明中的d 30 public void TriggerEvent(IEvent evt) 31 { 32 string msg = string.Format("TriggerEvent :{0}", evt.ToSring()); 33 HNLogger.Log(msg); 34 this.eventQueue.Enqueue(evt); 35 } 36 37 //注冊事件監聽方法 38 public bool AddEventListener(Enum eventType, EventHandle eventHandle, bool isPermanently = false) 39 { 40 bool flag = false; 41 if ( !this.eventHandlers.ContainsKey(eventType) ) 42 { 43 this.eventHandlers[eventType] = new EventReceiver(); 44 } 45 flag = this.eventHandlers[eventType].AddHandler(eventHandle); 46 } 47 return flag; 48 }
5.到了這里,其實就可以知道為什么在 定義事件類的時候,需要讓事件類來實現IEvent接口了,因為當接收到服務器消息時,會通過EventCenter的 TriggerEvent(IEvent eve)方法將接受到的數據轉化為對應的事件類,然后加入到eventQueue隊列中,而加入
eventQueue隊列,則需要是IEvent接口類型的方可,所以定義事件類型的時候必須要實現IEvent接口方可。
最后附上一張關系圖: