最近接触的项目,是一个棋牌游戏,棋牌游戏需要在前端进行一些操作,然后向服务器发送数据包,在服务器接收到客户端的数据后,会在服务端进行一系列的判断之后,然后发送给客户端一个返回数据,客户端接收到这个返回数据后,需要作出一系列的响应。那么,就针对于这一整个服务器<--->客户端的通讯过程,看看是如何来实现的。
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接口方可。
最后附上一张关系图: