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("生成消息池成功!"); }
|