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;
//设置服务器IP和端口号,创建socket
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)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
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
/// <summary>
/// 消息处理器基类,主要用于处理消息的逻辑的
/// </summary>
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)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
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;

/// <summary>
/// 消息池,主要是用于注册ID和消息类型以及消息处理器类型的映射关系
/// 方便网络层获取对象,进行反序列化和消息处理
/// </summary>
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);
}

/// <summary>
/// 根据ID 得到一个指定的消息类对象
/// </summary>
/// <param name="id">消息ID</param>
/// <returns>消息类对象</returns>
public BaseMessage GetMessage(int id)
{
if (!messages.ContainsKey(id))
return null;
return Activator.CreateInstance(messages[id]) as BaseMessage;
}

/// <summary>
/// 根据ID 得到一个指定的消息处理类对象
/// </summary>
/// <param name="id">消息ID</param>
/// <returns>消息处理类对象</returns>
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)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
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)
{
//记录所有消息的ID
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("生成消息池成功!");
}