UN6L1——消息池类和消息处理类
要解决的问题
在目前的练习题内实现的客户端网络管理器里,我们是将消息的识别,和消息的处理逻辑在网络层实现的
并且用switch...case...来实现消息的识别,缩略代码如下(省略断联、发消息等逻辑):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
   | private Queue<BaseMessage> receiveQueue = new Queue<BaseHandler>();
 
  public void Connect(string ip, int port) {     if (socket != null && socket.Connected)         return;          IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);     socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);          SocketAsyncEventArgs args = new SocketAsyncEventArgs();     args.RemoteEndPoint = ipPoint;     args.Completed += (socket, args) =>     {         if (args.SocketError == SocketError.Success)         {             print("连接成功");                          SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();             receiveArgs.SetBuffer(cacheBytes, 0, cacheBytes.Length);             receiveArgs.Completed += ReceiveCallBack;             this.socket.ReceiveAsync(receiveArgs);         }     };     socket.ConnectAsync(args); }
 
  private void ReceiveCallBack(object obj, SocketAsyncEventArgs args) {     if (args.SocketError == SocketError.Success)     {         HandleReceiveMsg(args.BytesTransferred);         args.SetBuffer(cacheNum, args.Buffer.Length - cacheNum);         if (this.socket != null && this.socket.Connected)                         socket.ReceiveAsync(args);     }     else         print("接受消息出错" + args.SocketError); }
 
  private void HandleReceiveMsg(int receiveNum) {     int msgID = 0;     int msgLength = 0;     int nowIndex = 0;
      cacheNum += receiveNum;
      while (true)     {                  msgLength = -1;                  if (cacheNum - nowIndex >= 8)         {                          msgID = BitConverter.ToInt32(cacheBytes, nowIndex);             nowIndex += 4;                          msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);             nowIndex += 4;         }
          if (cacheNum - nowIndex >= msgLength && msgLength != -1)         {                          BaseMsg baseMsg = null;             switch (msgID)             {                 case 1001:                     baseMsg = new PlayerMsg();                     baseMsg.Reading(cacheBytes, nowIndex);                     break;             }             if (baseMsg != null)                                  receiveQueue.Enqueue(baseMsg);             nowIndex += msgLength;             if (nowIndex == cacheNum)             {                 cacheNum = 0;                 break;             }         }         else         {             if (msgLength != -1)                 nowIndex -= 8;                          Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);             cacheNum = cacheNum - nowIndex;             break;         }     } }
  void Update() {     if (receiveQueue.Count > 0)     {                  BaseMsg baseMsg = receiveQueue.Dequeue();         switch (baseMsg)         {             case PlayerMsg msg:                 print(msg.playerID);                 break;         }     } }
   | 
 
我们可以发现,在HandleReceiveMsg和Update方法内分别涉及到消息反序列化和消息的处理
其中消息的识别是根据消息ID使用switch...case...来分别执行不同的消息体反序列化和处理逻辑的
消息的具体如何处理也在网络层内实现
随着项目的推进,消息类将不可避免的越来越多,如果将所有的消息的识别和处理都在网络层上实现,
势必会频繁修改网络层代码,工作量和网络层代码量也会越来越大,这不利于我们维护网络层代码
因此我们需要将消息的识别和处理从网络层上剥离开。
解决方案 —— 消息处理类 BaseHandler
我们可以为每个消息都声明一个消息处理类,消息处理类有共同的父类BaseHandler,
BaseHandler类中声明消息类变量message,用来装载反序列化出来的消息对象本身,网络层反序列化对象时将消息赋值给该变量
再声明消息处理方法MsgHandle(),将message内的消息对象进行处理,由网络层调用
所有的子消息处理类都必须要重写这个方法,其中就可以实现独立的消息处理逻辑
网络层在接收到数据并反序列化出消息后,只需要将消息赋值给对应的消息处理类的变量message内
然后,在处理消息时只需要调用装载子类的BaseHandler变量的处理方法MsgHandle(),即可执行子类的处理方法处理消息
1 2 3 4 5 6 7 8 9 10
   | 
 
  public abstract class BaseHandler {          public BaseMessage message;          public abstract void MsgHandle(); }
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13
   | using GamePlayer; using UnityEngine;
  public class PlayerMessageHandler : BaseHandler {     public override void MsgHandle()     {         PlayerMessage msg = message as PlayerMessage;                           Debug.Log(msg.playerID);     } }
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
   | private Queue<BaseHandler> receiveQueue = new Queue<BaseHandler>();
  private void HandleReceiveMsg(int receiveNum) {     int msgID = 0;     int msgLength = 0;     int nowIndex = 0;
      cacheNum += receiveNum;
      while (true)     {                  msgLength = -1;                  if (cacheNum - nowIndex >= 8)         {                          msgID = BitConverter.ToInt32(cacheBytes, nowIndex);             nowIndex += 4;                          msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);             nowIndex += 4;         }
          if (cacheNum - nowIndex >= msgLength && msgLength != -1)         {                          BaseMessage baseMsg = null;             BaseHandler handler = null;             switch (msgID)             {                 case 1001:                     baseMsg = new PlayerMessage();                     baseMsg.Reading(cacheBytes, nowIndex);                     handler = new PlayerMessageHandler();                     handler.message = baseMsg;                     break;             }             if (handler != null)                 receiveQueue.Enqueue(handler);             nowIndex += msgLength;             if (nowIndex == cacheNum)             {                 cacheNum = 0;                 break;             }         }         else         {             if (msgLength != -1)                 nowIndex -= 8;                          Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);             cacheNum = cacheNum - nowIndex;             break;         }     } }
  void Update() {     if (receiveQueue.Count > 0)         receiveQueue.Dequeue().MsgHandle(); }
   | 
 
可以发现,反序列化消息后将消息赋值给对应的消息处理类,然后将消息处理类压入到待处理的BaseHandler队列内
处理消息时直接取出一个BaseHandler类对象执行处理方法即可,由于父类装子类,执行的实际上是子类的方法
因此,网络层不再需要再实现如何处理各种消息,也不再需要在处理消息时识别消息
解决方案 —— 消息池类 MessagePool
在使用BaseHandler解决了网络层与消息的处理耦合问题后,
网络层反序列化消息时,还需要通过消息ID识别消息的类型,调用对应的反序列化方法
调用不同的反序列化逻辑实际上还是使用的是switch...case...,因此我们依然需要想办法优化这里的逻辑
我们可以使用消息池类 MessagePool,里面声明两个字典,建立消息ID和消息类与消息处理类的映射关系
然后在网络层里实例化一个消息池类对象,实例化时,我们需要注册所有的消息ID与消息类和消息映射类的映射关系
网络层通过将反序列化出来的消息ID传入到消息池类,就可以获取到对应的消息类与消息处理类
网络层可以用它们的父类BaseMessage和BaseHandler类型变量来装载这些返回的对象
然后执行BaseMessage的反序列化方法反序列化数据,再将消息类对象传入到BaseHandler对象内,
将BaseHandler对象压入到待处理的BaseHandler队列内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
   | using GamePlayer; using System; using System.Collections.Generic;
 
 
 
 
  public class MessagePool {     private Dictionary<int, Type> messages = new Dictionary<int, Type>();     private Dictionary<int, Type> handlers = new Dictionary<int, Type>();
      public MessagePool()     {                  Register(1001, typeof(PlayerMessage), typeof(PlayerMessageHandler));     }
      private void Register(int id, Type messageType, Type handlerType)     {         messages.Add(id, messageType);         handlers.Add(id, handlerType);     }
                               public BaseMessage GetMessage(int id)     {         if (!messages.ContainsKey(id))             return null;         return Activator.CreateInstance(messages[id]) as BaseMessage;     }
                               public BaseHandler GetHandler(int id)     {         if (!handlers.ContainsKey(id))             return null;         return Activator.CreateInstance(handlers[id]) as BaseHandler;     } }
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
   | private Queue<BaseHandler> receiveQueue = new Queue<BaseHandler>();
  private void HandleReceiveMsg(int receiveNum) {     int msgID = 0;     int msgLength = 0;     int nowIndex = 0;
      cacheNum += receiveNum;
      while (true)     {                  msgLength = -1;                  if (cacheNum - nowIndex >= 8)         {                          msgID = BitConverter.ToInt32(cacheBytes, nowIndex);             nowIndex += 4;                          msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);             nowIndex += 4;         }
          if (cacheNum - nowIndex >= msgLength && msgLength != -1)         {             BaseMessage message = messagePool.GetMessage(msgID);             if (message != null)             {                 message.Reading(cacheBytes, nowIndex);                 BaseHandler handler = messagePool.GetHandler(msgID);                 handler.message = message;                 receiveQueue.Enqueue(handler);             }                      nowIndex += msgLength;             if (nowIndex == cacheNum)             {                 cacheNum = 0;                 break;             }         }         else         {             if (msgLength != -1)                 nowIndex -= 8;                          Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);             cacheNum = cacheNum - nowIndex;             break;         }     } }
  void Update() {     if (receiveQueue.Count > 0)         receiveQueue.Dequeue().MsgHandle(); }
 
   | 
 
可以发现,网络层在反序列化后消息ID后,就可以通过消息ID获取到对应的消息类和消息处理类,而不需要关心如何识别它们
因此,网络层不再需要在反序列化时识别消息,消息类的变动不再能够影响网络层
工具生成Handler
每个消息类都会有一个对应的Handler类,而消息类是通过配置文件自动生成的,
很明显,我们可以在生成消息类时同时生成消息处理类的模板,省去我们创建脚本编写重复内容的工作
让我们的精力可以更加放在编写处理逻辑上
假设我们要生成这样的消息处理类模板,我们在自动声明的方法内写逻辑即可
1 2 3 4 5 6 7 8 9 10 11
   | namespace GamePlayer {     public class PlayerMessageHandler : BaseHandler     {         public override void MsgHandle()         {             PlayerMessage msg = message as PlayerMessage;                      }     } }
   | 
 
我们只需要在之前的生成消息类的基础上,再同时自动生成消息处理类,放在同一文件夹下
值得一提的是,由于我们生成的是代码文件模板,我们之后会写逻辑,如果此时不加以判断就生成问可能会覆盖掉我们写的逻辑
因此在追加新的消息处理类文件时,我们必须要检查消息处理类脚本文件是否已存在,如果存在,就不要生成这个代码逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
   | public void GenerateMessage(XmlNodeList nodes) {     string namespaceStr = "";     string classNameStr = "";     string idStr = "";     string fieldStr = "";     string getBytesNumStr = "";     string writingStr = "";     string readingStr = "";
      foreach (XmlNode dataNode in nodes)     {         namespaceStr = dataNode.Attributes["namespace"].Value;         classNameStr = dataNode.Attributes["name"].Value;
          
                   if (File.Exists(path + classNameStr + "Handler.cs"))             continue;                  string HandlerStr = $"namespace {namespaceStr}\r\n" +                               "{\r\n" +                             $"\tpublic class {classNameStr}Handler : BaseHandler\r\n" +                              "\t{\r\n" +                              "\t\tpublic override void MsgHandle()\r\n" +                              "\t\t{\r\n" +                             $"\t\t\t{classNameStr} msg = message as {classNameStr};\r\n" +                              "\t\t}\r\n" +                              "\t}\r\n" +                              "}\r\n";                  File.WriteAllText(path + classNameStr + "Handler.cs", HandlerStr);     }     Debug.Log("消息类生成结束"); }
   | 
 
工具生成消息池类
对于消息池类,每次我们追加了新的消息类和消息处理类时,构造函数内的注册逻辑就需要添加内容,可能还要引用新的命名空间
因此,我们完全可以在生成消息类和消息处理类的时候顺便生成消息池类
根据协议配置文件内的消息类节点的多少,就注册多少个消息ID与消息类和消息映射类的映射关系
同时根据消息类节点的命名空间,在消息池脚本内引用对应的命名空间
假设我们要生成这样的消息池脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
   | using System; using System.Collections.Generic; using GamePlayer; using GameSystem;
  public class MessagePool {     private Dictionary<int, Type> messages = new Dictionary<int, Type>();     private Dictionary<int, Type> handlers = new Dictionary<int, Type>();
      public MessagePool()     {         Register(1001, typeof(PlayerMessage), typeof(PlayerMessageHandler));         Register(1002, typeof(HeartMessage), typeof(HeartMessageHandler));         Register(1003, typeof(QuitMessage), typeof(QuitMessageHandler));     }
      private void Register(int id, Type messageType, Type handlerType)
      {         messages.Add(id, messageType);         handlers.Add(id, handlerType);     }
      public BaseMessage GetMessage(int id)
      {         if (!messages.ContainsKey(id))             return null;         return Activator.CreateInstance(messages[id]) as BaseMessage;     }
      public BaseHandler GetHandler(int id)
      {         if (!handlers.ContainsKey(id))             return null;         return Activator.CreateInstance(handlers[id]) as BaseHandler;     } }
   | 
 
我们在GenerateCSharp内再声明一个生成消息池脚本的方法GenerateMessagePool(),在生成C#脚本时调用,传入所有的消息类节点
1 2 3 4 5 6 7 8 9
   | [MenuItem("ProtocolTool/生成C#脚本")] private static void GenerateCSharp() {     generateCSharp.GenerateEnum(GetNodes("enum"));     generateCSharp.GenerateData(GetNodes("data"));     generateCSharp.GenerateMessage(GetNodes("message"));     generateCSharp.GenerateMessagePool(GetNodes("message"));     AssetDatabase.Refresh(); }
  | 
 
它的可变内容是消息类命名空间的引用,以及构造函数内的注册消息ID与消息类和消息映射类的映射关系语句
- 遍历所有的消息类节点的命名空间属性,获取它们的命名空间,引用命名空间
 
- 遍历所有的消息类节点的ID、名字属性,在构造函数内生成注册消息ID和消息类与消息处理类的映射关系语句
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
   | public void GenerateMessagePool(XmlNodeList nodes) {     List<string> ids = new List<string>();     List<string> names = new List<string>();     List<string> nameSpaces = new List<string>();     foreach (XmlNode dataNode in nodes)     {                  string id = dataNode.Attributes["id"].Value;         if (!ids.Contains(id))             ids.Add(id);         else             Debug.LogError("存在相同ID的消息" + id);                  string name = dataNode.Attributes["name"].Value;         if (!names.Contains(name))             names.Add(name);         else             Debug.LogError("存在同名的消息" + name + ", 建议即使不在同一命名空间内也不要有同名消息");                  string msgNamespace = dataNode.Attributes["namespace"].Value;         if (!nameSpaces.Contains(msgNamespace))             nameSpaces.Add(msgNamespace);     }
           string nameSpacesStr = "";     for (int i = 0; i < nameSpaces.Count; i++)         nameSpacesStr += $"using {nameSpaces[i]};\r\n";          string registerStr = "";     for (int i = 0; i < ids.Count; i++)     {         registerStr += $"\t\tRegister({ids[i]}, typeof({names[i]}), typeof({names[i]}Handler));\r\n";     }
      string msgPoolStr = "using System;\r\n" +                         "using System.Collections.Generic;\r\n" +                         nameSpacesStr + "\r\n" +                                                  "public class MessagePool\r\n" +                         "{\r\n" +                                                  "\tprivate Dictionary<int, Type> messages = new Dictionary<int, Type>();\r\n" +                         "\tprivate Dictionary<int, Type> handlers = new Dictionary<int, Type>();\r\n\r\n" +                                                  "\tpublic MessagePool()\r\n" +                         "\t{\r\n" +                         registerStr +                         "\t}\r\n\r\n" +                                                  "\tprivate void Register(int id, Type messageType, Type handlerType)\r\n\r\n" +                         "\t{\r\n" +                         "\t\tmessages.Add(id, messageType);\r\n" +                         "\t\thandlers.Add(id, handlerType);\r\n" +                         "\t}\r\n\r\n" +                                                  "\tpublic BaseMessage GetMessage(int id)\r\n\r\n" +                         "\t{\r\n" +                         "\t\tif (!messages.ContainsKey(id))\r\n" +                         "\t\t\treturn null;\r\n" +                         "\t\treturn Activator.CreateInstance(messages[id]) as BaseMessage;\r\n" +                         "\t}\r\n\r\n" +                                                  "\tpublic BaseHandler GetHandler(int id)\r\n\r\n" +                         "\t{\r\n" +                         "\t\tif (!handlers.ContainsKey(id))\r\n" +                         "\t\t\treturn null;\r\n" +                         "\t\treturn Activator.CreateInstance(handlers[id]) as BaseHandler;\r\n" +                         "\t}\r\n" +                         "}";
      string path = SAVE_PATH + "/Pool/";     if (!Directory.Exists(path))         Directory.CreateDirectory(path);          File.WriteAllText(path + "MessagePool.cs", msgPoolStr);     Debug.Log("生成消息池成功!"); }
   |