摘要:该网络通信系统基于Unity实现,包含以下几个核心模块:
- 协议配置:通过XML定义枚举(如玩家/英雄类型)、数据结构(如PlayerData)及消息协议(如PlayerMsg),支持基础类型、数组、字典等复杂结构。
- 代码生成工具:解析XML自动生成C#脚本,包括枚举类、可序列化的数据结构类(实现字节计算、序列化/反序列化)、消息类及消息池,减少手动编码。
- 网络管理器:采用异步Socket实现TCP通信,处理连接、心跳包(间隔2秒)、消息收发及粘包/分包问题,通过消息池动态映射ID与消息类型,结合队列机制解耦网络层与业务逻辑。
- 扩展性:支持多命名空间、自动目录生成,预留C++/Java接口,确保协议修改后代码自动同步,提升开发效率。
整体设计实现了高内聚、低耦合的网络通信框架,适用于游戏等实时交互场景。
<?xml version="1.0" encoding="UTF-8"?>
<messages><!--枚举配置规则--><enum name="E_PLAYER_TYPE" namespace="GamePlayer"><field name="MAIN">1</field><field name="OTHER"/></enum><enum name="E_HERO_TYPE" namespace="GamePlayer"><field name="MAIN"/><field name="OTHER"/></enum><enum name="E_MONSTER_TYPE" namespace="GameMonster"><field name="NORMAL">2</field><field name="BOSS"/></enum><!--数据结构类配置规则--><data name="PlayerData" namespace="GamePlayer"><field type="int" name="id"/><field type="float" name="atk"/><field type="bool" name="sex"/><field type="long" name="lev"/><field type="array" data="int" name="arrays"/><field type="list" T="int" name="list"/><field type="dic" Tkey="int" Tvalue="string" name="dic"/><field type="enum" data="E_HERO_TYPE" name="heroType"/></data><!--消息类类配置规则--><message id="1001" name="PlayerMsg" namespace="GamePlayer"><field type="int" name="playerID"/><field type="PlayerData" name="data"/></message><message id="1002" name="HeartMsg" namespace="GameSystem"/><message id="1003" name="QuitMsg" namespace="GameSystem"/>
</messages>
该配置文件通过枚举、数据结构和消息定义,构建了游戏中玩家、怪物和系统交互的基础模型。枚举确保类型统一,数据结构支持复杂数据建模,消息机制实现模块间通信,整体设计符合游戏开发中数据配置的典型范式。
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using UnityEditor;
using UnityEngine;public class ProtocolTool
{//配置文件所在路径private static string PROTO_INFO_PATH = Application.dataPath + "/Editor/ProtocolTool/ProtocolInfo.xml";private static GenerateCSharp generateCSharp = new GenerateCSharp();[MenuItem("ProtocolTool/生成C#脚本")]private static void GenerateCSharp(){//1.读取xml相关的信息//XmlNodeList list = GetNodes("enum");//2.根据这些信息 去拼接字符串 生成对应的脚本//生成对应的枚举脚本generateCSharp.GenerateEnum(GetNodes("enum"));//生成对应的数据结构类脚本generateCSharp.GenerateData(GetNodes("data"));//生成对应的消息类脚本generateCSharp.GenerateMsg(GetNodes("message"));//生成消息池generateCSharp.GenerateMsgPool(GetNodes("message"));//刷新编辑器界面 让我们可以看到生成的内容 不需要手动进行刷新了AssetDatabase.Refresh();}[MenuItem("ProtocolTool/生成C++脚本")]private static void GenerateC(){Debug.Log("生成C++代码");}[MenuItem("ProtocolTool/生成Java脚本")]private static void GenerateJava(){Debug.Log("生成Java代码");}/// <summary>/// 获取指定名字的所有子节点 的 List/// </summary>/// <param name="nodeName"></param>/// <returns></returns>private static XmlNodeList GetNodes(string nodeName){XmlDocument xml = new XmlDocument();xml.Load(PROTO_INFO_PATH);XmlNode root = xml.SelectSingleNode("messages");return root.SelectNodes(nodeName);}
}
这段代码是一个 Unity 编辑器扩展工具,用于根据 XML 配置文件自动生成多语言协议代码,主要功能如下:
-
配置解析:读取 XML 配置文件(如用户提供的协议定义),提取枚举、数据结构和消息定义。
-
代码生成:
- 通过菜单命令(ProtocolTool / 生成 C# 脚本)触发,生成 C# 协议类文件
- 支持生成:枚举类型、数据结构类、消息类、消息池管理类
- 预留了 C++ 和 Java 代码生成接口(仅打印日志)
-
工具集成:
- 在 Unity 编辑器菜单中添加功能入口
- 生成后自动刷新项目视图,无需手动操作
-
核心逻辑:
- 使用
GenerateCSharp
类处理代码生成逻辑 - 通过 XPath 查询 XML 节点,提取协议定义信息
- 使用
这个工具的设计目标是简化游戏网络协议开发流程,将配置文件自动转换为各语言的代码实现,提高开发效率并减少手动编码错误。
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using UnityEngine;public class GenerateCSharp
{//协议保存路径private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";//生成枚举public void GenerateEnum(XmlNodeList nodes){//生成枚举脚本的逻辑string namespaceStr = "";string enumNameStr = "";string fieldStr = "";foreach (XmlNode enumNode in nodes){//获取命名空间配置信息namespaceStr = enumNode.Attributes["namespace"].Value;//获取枚举名配置信息enumNameStr = enumNode.Attributes["name"].Value;//获取所有的字段节点 然后进行字符串拼接XmlNodeList enumFields = enumNode.SelectNodes("field");//一个新的枚举 需要清空一次上一次拼接的字段字符串fieldStr = "";foreach (XmlNode enumField in enumFields){fieldStr += "\t\t" + enumField.Attributes["name"].Value;if (enumField.InnerText != "")fieldStr += " = " + enumField.InnerText;fieldStr += ",\r\n";}//对所有可变的内容进行拼接string enumStr = $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic enum {enumNameStr}\r\n" +"\t{\r\n" +$"{fieldStr}" +"\t}\r\n" +"}";//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Enum/";//如果不存在这个文件夹 则创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为枚举脚本文件File.WriteAllText(path + enumNameStr + ".cs", enumStr);}Debug.Log("枚举生成结束");}//生成数据结构类public void GenerateData(XmlNodeList nodes){string namespaceStr = "";string classNameStr = "";string fieldStr = "";string getBytesNumStr = "";string writingStr = "";string readingStr = "";foreach (XmlNode dataNode in nodes){//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getBytesNumStr = GetGetBytesNumStr(fields);//通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果writingStr = GetWritingStr(fields);//通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果readingStr = GetReadingStr(fields);string dataStr = "using System;\r\n" +"using System.Collections.Generic;\r\n" +"using System.Text;\r\n" + $"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseData\r\n" +"\t{\r\n" +$"{fieldStr}" +"\t\tpublic override int GetBytesNum()\r\n" +"\t\t{\r\n" +"\t\t\tint num = 0;\r\n" +$"{getBytesNumStr}" +"\t\t\treturn num;\r\n" +"\t\t}\r\n" +"\t\tpublic override byte[] Writing()\r\n" +"\t\t{\r\n" +"\t\t\tint index = 0;\r\n"+"\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +$"{writingStr}" +"\t\t\treturn bytes;\r\n" +"\t\t}\r\n" +"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +"\t\t{\r\n" +"\t\t\tint index = beginIndex;\r\n" +$"{readingStr}" +"\t\t\treturn index - beginIndex;\r\n" +"\t\t}\r\n" +"\t}\r\n" +"}";//保存为 脚本文件//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Data/";//如果不存在这个文件夹 则创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为枚举脚本文件File.WriteAllText(path + classNameStr + ".cs", dataStr);}Debug.Log("数据结构类生成结束");}//生成消息类public void GenerateMsg(XmlNodeList nodes){string idStr = "";string namespaceStr = "";string classNameStr = "";string fieldStr = "";string getBytesNumStr = "";string writingStr = "";string readingStr = "";foreach (XmlNode dataNode in nodes){//消息IDidStr = dataNode.Attributes["id"].Value;//命名空间namespaceStr = dataNode.Attributes["namespace"].Value;//类名classNameStr = dataNode.Attributes["name"].Value;//读取所有字段节点XmlNodeList fields = dataNode.SelectNodes("field");//通过这个方法进行成员变量声明的拼接 返回拼接结果fieldStr = GetFieldStr(fields);//通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果getBytesNumStr = GetGetBytesNumStr(fields);//通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果writingStr = GetWritingStr(fields);//通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果readingStr = GetReadingStr(fields);string dataStr = "using System;\r\n" +"using System.Collections.Generic;\r\n" +"using System.Text;\r\n" +$"namespace {namespaceStr}\r\n" +"{\r\n" +$"\tpublic class {classNameStr} : BaseMsg\r\n" +"\t{\r\n" +$"{fieldStr}" +"\t\tpublic override int GetBytesNum()\r\n" +"\t\t{\r\n" +"\t\t\tint num = 8;\r\n" +//这个8代表的是 消息ID的4个字节 + 消息体长度的4个字节$"{getBytesNumStr}" +"\t\t\treturn num;\r\n" +"\t\t}\r\n" +"\t\tpublic override byte[] Writing()\r\n" +"\t\t{\r\n" +"\t\t\tint index = 0;\r\n" +"\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +"\t\t\tWriteInt(bytes, GetID(), ref index);\r\n" +"\t\t\tWriteInt(bytes, bytes.Length - 8, ref index);\r\n" +$"{writingStr}" +"\t\t\treturn bytes;\r\n" +"\t\t}\r\n" +"\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +"\t\t{\r\n" +"\t\t\tint index = beginIndex;\r\n" +$"{readingStr}" +"\t\t\treturn index - beginIndex;\r\n" +"\t\t}\r\n" +"\t\tpublic override int GetID()\r\n" +"\t\t{\r\n" +"\t\t\treturn " + idStr + ";\r\n" +"\t\t}\r\n" +"\t}\r\n" +"}";//保存为 脚本文件//保存文件的路径string path = SAVE_PATH + namespaceStr + "/Msg/";//如果不存在这个文件夹 则创建if (!Directory.Exists(path))Directory.CreateDirectory(path);//字符串保存 存储为枚举脚本文件File.WriteAllText(path + classNameStr + ".cs", dataStr);//生成处理器脚本//判断处理器脚本是否存在 如果存在就不要覆盖 避免把写过的逻辑处理代码覆盖了//如果想要改变,就把没用的脚本删了,再生成就会是新的if (File.Exists(path + classNameStr + "Handler.cs"))continue;string handlerStr = $"namespace {namespaceStr}\r\n" +"{\r\n" + $"\tpublic class {classNameStr}Handler : BaseHandler"+"\t{\r\n"+"\t\tpublic override void MsgHandler()\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和消息类型以及消息处理器类型的对应关系public void GenerateMsgPool(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){//记录所有消息的IDstring id = dataNode.Attributes["id"].Value;if (!ids.Contains(id))ids.Add(id);elseDebug.LogError("存在相同ID的消息" + id);string name = dataNode.Attributes["name"].Value;if (!names.Contains(name))names.Add(name);elseDebug.LogError("存在同名的消息" + name + ",建议即使在不同的命名空间下也使用不同的消息名字");string msgNameSpace = dataNode.Attributes["namespace"].Value;if (!nameSpaces.Contains(msgNameSpace))nameSpaces.Add(msgNameSpace);}//获取所有需要引用的命名空间 拼接好string nameSpaceStr = "";for (int i = 0; i < nameSpaces.Count; i++)nameSpaceStr += $"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" +nameSpaceStr +"public class MsgPool\r\n" +"{\r\n" +"\tprivate Dictionary<int, Type> message = new Dictionary<int, Type>();\r\n" +"\tprivate Dictionary<int, Type> handlers = new Dictionary<int, Type>();\r\n" +"\tpublic MsgPool ()\r\n" +"\t{\r\n" +registerStr +"\t}\r\n" +"\tprivate void Register(int id,Type messageType,Type handlerType)\r\n" +"\t{\r\n" +"\t\tmessage.Add(id, messageType);\r\n" +"\t\thandlers.Add(id, handlerType);\r\n" +"\t}\r\n" +"\tpublic BaseMsg GetMessage(int id)\r\n" +"\t{\r\n" +"\t\tif (!message.ContainsKey(id))\r\n" +"\t\t\treturn null;\r\n" +"\t\treturn Activator.CreateInstance(message[id]) as BaseMsg;\r\n" +"\t}\r\n" +"\tpublic BaseHandler GetHandler(int id)\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" +"}\r\n";string path = SAVE_PATH + "/Pool/";if (!Directory.Exists(path))Directory.CreateDirectory(path);File.WriteAllText(path + "MsgPool.cs", msgPoolStr);}/// <summary>/// 获取成员变量声明内容/// </summary>/// <param name="fields"></param>/// <returns></returns>private string GetFieldStr(XmlNodeList fields){string fieldStr = "";foreach (XmlNode field in fields){//变量类型string type = field.Attributes["type"].Value;//变量名string fieldName = field.Attributes["name"].Value;if(type == "list"){string T = field.Attributes["T"].Value;fieldStr += "\t\tpublic List<" + T + "> ";}else if(type == "array"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + "[] ";}else if(type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;fieldStr += "\t\tpublic Dictionary<" + Tkey + ", " + Tvalue + "> ";}else if(type == "enum"){string data = field.Attributes["data"].Value;fieldStr += "\t\tpublic " + data + " ";}else{fieldStr += "\t\tpublic " + type + " ";}fieldStr += fieldName + ";\r\n";}return fieldStr;}//拼接 GetBytesNum函数的方法private string GetGetBytesNumStr(XmlNodeList fields){string bytesNumStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息bytesNumStr += "\t\t\tfor (int i = 0; i < " + name + ".Count; ++i)\r\n";//这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(T, name + "[i]") + ";\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息bytesNumStr += "\t\t\tfor (int i = 0; i < " + name + ".Length; ++i)\r\n";//这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(data, name + "[i]") + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息bytesNumStr += "\t\t\tforeach (" + Tkey + " key in " + name + ".Keys)\r\n";bytesNumStr += "\t\t\t{\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tkey, "key") + ";\r\n";bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";bytesNumStr += "\t\t\t}\r\n";}elsebytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";}return bytesNumStr;}//获取 指定类型的字节数private string GetValueBytesNum(string type, string name){//这里我没有写全 所有的常用变量类型 你可以根据需求去添加switch (type){case "int":case "float":case "enum":return "4";case "long":return "8";case "byte":case "bool":return "1";case "short":return "2";case "string":return "4 + Encoding.UTF8.GetByteCount(" + name + ")";default:return name + ".GetBytesNum()";}}//拼接 Writing函数的方法private string GetWritingStr(XmlNodeList fields){string writingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if(type == "list"){string T = field.Attributes["T"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tfor (int i = 0; i < " + name + ".Count; ++i)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(T, name + "[i]") + "\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Length, ref index);\r\n";writingStr += "\t\t\tfor (int i = 0; i < " + name + ".Length; ++i)\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(data, name + "[i]") + "\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";writingStr += "\t\t\tforeach (" + Tkey + " key in " + name + ".Keys)\r\n";writingStr += "\t\t\t{\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tkey, "key") + "\r\n";writingStr += "\t\t\t\t" + GetFieldWritingStr(Tvalue, name + "[key]") + "\r\n";writingStr += "\t\t\t}\r\n";}else{writingStr += "\t\t\t" + GetFieldWritingStr(type, name) + "\r\n";}}return writingStr;}private string GetFieldWritingStr(string type, string name){switch (type){case "byte":return "WriteByte(bytes, " + name + ", ref index);";case "int":return "WriteInt(bytes, " + name + ", ref index);";case "short":return "WriteShort(bytes, " + name + ", ref index);";case "long":return "WriteLong(bytes, " + name + ", ref index);";case "float":return "WriteFloat(bytes, " + name + ", ref index);";case "bool":return "WriteBool(bytes, " + name + ", ref index);";case "string":return "WriteString(bytes, " + name + ", ref index);";case "enum":return "WriteInt(bytes, Convert.ToInt32(" + name + "), ref index);";default:return "WriteData(bytes, " + name + ", ref index);";}}private string GetReadingStr(XmlNodeList fields){string readingStr = "";string type = "";string name = "";foreach (XmlNode field in fields){type = field.Attributes["type"].Value;name = field.Attributes["name"].Value;if (type == "list"){string T = field.Attributes["T"].Value;readingStr += "\t\t\t" + name + " = new List<" + T + ">();\r\n";readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\tfor (int i = 0; i < " + name + "Count; ++i)\r\n";readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(T) + ");\r\n";}else if (type == "array"){string data = field.Attributes["data"].Value;readingStr += "\t\t\tshort " + name + "Length = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\t" + name + " = new " + data + "["+ name + "Length];\r\n";readingStr += "\t\t\tfor (int i = 0; i < " + name + "Length; ++i)\r\n";readingStr += "\t\t\t\t" + name + "[i] = " + GetFieldReadingStr(data) + ";\r\n";}else if (type == "dic"){string Tkey = field.Attributes["Tkey"].Value;string Tvalue = field.Attributes["Tvalue"].Value;readingStr += "\t\t\t" + name + " = new Dictionary<" + Tkey + ", " + Tvalue + ">();\r\n";readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";readingStr += "\t\t\tfor (int i = 0; i < " + name + "Count; ++i)\r\n";readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(Tkey) + ", " +GetFieldReadingStr(Tvalue) + ");\r\n";}else if (type == "enum"){string data = field.Attributes["data"].Value;readingStr += "\t\t\t" + name + " = (" + data + ")ReadInt(bytes, ref index);\r\n";}elsereadingStr += "\t\t\t" + name + " = " + GetFieldReadingStr(type) + ";\r\n";}return readingStr;}private string GetFieldReadingStr(string type){switch (type){case "byte":return "ReadByte(bytes, ref index)";case "int":return "ReadInt(bytes, ref index)";case "short":return "ReadShort(bytes, ref index)";case "long":return "ReadLong(bytes, ref index)";case "float":return "ReadFloat(bytes, ref index)";case "bool":return "ReadBool(bytes, ref index)";case "string":return "ReadString(bytes, ref index)";default:return "ReadData<" + type + ">(bytes, ref index)";}}
}
这段代码是 Unity 中用于自动生成 C# 协议相关脚本的工具类,核心功能是解析 XML 配置文件并生成对应的枚举、数据结构、消息类及消息池管理代码,具体作用如下:
1. 代码生成核心逻辑
1.1 枚举生成(GenerateEnum
)
- 输入:XML 中所有
<enum>
节点(如玩家类型、怪物类型等)。 - 处理:
- 提取命名空间(
namespace
)、枚举名(name
)和字段(field
)。 - 自动拼接枚举代码字符串(包含字段名和值),例如:
csharp
namespace GamePlayer { public enum E_PLAYER_TYPE { MAIN = 1, OTHER } }
- 提取命名空间(
- 输出:按命名空间分层存储的枚举脚本(如
GamePlayer/Enum/E_PLAYER_TYPE.cs
)。
1.2 数据结构类生成(GenerateData
)
- 输入:XML 中所有
<data>
节点(如PlayerData
)。 - 处理:
- 解析字段类型(基础类型、数组、列表、字典、枚举),生成对应的成员变量声明。
- 自动实现
BaseData
抽象类的GetBytesNum
(计算字节长度)、Writing
(序列化)、Reading
(反序列化)方法。 - 例如,数组 / 列表会先写入长度(
short
类型),再循环写入元素;枚举类型会转换为整数存储。
- 输出:数据结构类脚本(如
GamePlayer/Data/PlayerData.cs
),支持网络传输的数据序列化 / 反序列化。
1.3 消息类生成(GenerateMsg
)
- 输入:XML 中所有
<message>
节点(如PlayerMsg
)。 - 处理:
- 生成消息类(继承
BaseMsg
),包含消息 ID(GetID
方法)、字段序列化 / 反序列化逻辑。 - 自动生成消息处理器脚本(如
PlayerMsgHandler.cs
),用于处理消息逻辑(需手动补充业务代码)。 - 消息协议格式:前 8 字节固定为消息 ID(4 字节)和消息体长度(4 字节),后续为具体字段数据。
- 生成消息类(继承
- 输出:消息类脚本(如
GamePlayer/Msg/PlayerMsg.cs
)和处理器脚本。
1.4 消息池生成(GenerateMsgPool
)
- 功能:创建
MsgPool
类,维护消息 ID 与消息类型、处理器类型的映射关系。 - 处理:
- 从 XML 中提取所有消息 ID、名称和命名空间,生成注册代码(
Register
方法)。 - 提供
GetMessage
和GetHandler
方法,通过反射创建消息实例和处理器。
- 从 XML 中提取所有消息 ID、名称和命名空间,生成注册代码(
- 输出:消息池管理脚本(
Pool/MsgPool.cs
),用于统一管理消息的创建和分发。
2. 辅助工具方法
2.1 字段解析(GetFieldStr
)
- 作用:根据字段类型(
list
/array
/dic
/enum
/ 基础类型)生成对应的成员变量声明。- 例如:
list<int>
生成public List<int> list;
,dic<int, string>
生成public Dictionary<int, string> dic;
。
- 例如:
2.2 字节计算与序列化 / 反序列化(GetGetBytesNumStr
/GetWritingStr
/GetReadingStr
)
- 字节计算:
- 基础类型直接返回固定字节数(如
int=4
,string
需计算 UTF8 字节长度)。 - 容器类型(列表 / 数组 / 字典)先写入长度(
short
,2 字节),再递归计算元素字节数。
- 基础类型直接返回固定字节数(如
- 序列化(
Writing
):- 通过
WriteInt
/WriteShort
等方法将数据写入字节数组,容器类型循环写入元素。
- 通过
- 反序列化(
Reading
):- 通过
ReadInt
/ReadShort
等方法从字节数组读取数据,容器类型先读取长度再循环读取元素,枚举类型通过强制转换还原。
- 通过
3. 目录结构与文件管理
- 输出路径:
- 枚举:
Assets/Scripts/Protocol/[命名空间]/Enum/
- 数据结构:
Assets/Scripts/Protocol/[命名空间]/Data/
- 消息类:
Assets/Scripts/Protocol/[命名空间]/Msg/
- 消息池:
Assets/Scripts/Protocol/Pool/
- 枚举:
- 自动创建目录:若路径不存在,自动创建文件夹(如
GamePlayer/Enum
)。 - 避免覆盖:消息处理器脚本若已存在则跳过生成,防止覆盖手动编写的逻辑。
4. 工具集成与使用
- 触发方式:通过 Unity 编辑器菜单
ProtocolTool/生成C#脚本
调用,自动解析 XML 并生成代码。 - 依赖项:需提前定义
BaseData
和BaseMsg
抽象类,以及序列化工具方法(如WriteInt
/ReadInt
)。 - 扩展能力:预留了生成 C++/Java 代码的接口(当前仅打印日志,需进一步实现)。
总结
该工具通过解析 XML 配置文件,自动化生成游戏开发中所需的协议相关 C# 代码,涵盖枚举定义、数据结构序列化、消息通信和消息池管理,显著减少手动编码工作量,提高开发效率,尤其适用于需要频繁修改协议的网络通信场景。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using GamePlayer;
using GameSystem;
using UnityEngine;public class NetAsyncMgr : MonoBehaviour
{private static NetAsyncMgr instance;public static NetAsyncMgr Instance => instance;//和服务器进行连接的 Socketprivate Socket socket;//接受消息用的 缓存容器private byte[] cacheBytes = new byte[1024 * 1024];private int cacheNum = 0;private Queue<BaseHandler> receiveQueue = new Queue<BaseHandler>();//发送心跳消息的间隔时间private int SEND_HEART_MSG_TIME = 2;private HeartMsg hearMsg = new HeartMsg();//消息池对象 用于快速获取消息和处理消息处理类对象private MsgPool msgPool = new MsgPool();// Start is called before the first frame updatevoid Awake(){instance = this;//过场景不移除DontDestroyOnLoad(this.gameObject);//客户端循环定时给服务端发送心跳消息InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);}private void SendHeartMsg(){if (socket != null && socket.Connected)Send(hearMsg);}// Update is called once per framevoid Update(){if (receiveQueue.Count > 0){//目标二:不要每次添加了新消息 就在这里去处理对应消息的逻辑//更加自动化的去处理他们 并且不要在网络层这来处理//通过消息处理者基类对象 调用处理方法 以后无论添加多少消息 都不用修改了receiveQueue.Dequeue().MsgHandler();}}//连接服务器的代码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);}else{print("连接失败" + args.SocketError);}};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);elseClose();}else{print("接受消息出错" + args.SocketError);//关闭客户端连接Close();}}public void Close(bool isSelf=false){if(socket != null){QuitMsg msg = new QuitMsg();socket.Send(msg.Writing());socket.Shutdown(SocketShutdown.Both);socket.Disconnect(false);socket.Close();socket = null;}//不是自己主动断开连接的if(!isSelf){//短线重连,弹出一个面板}}public void SendTest(byte[] bytes){SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};this.socket.SendAsync(args);}public void Send(BaseMsg msg){if(this.socket != null && this.socket.Connected){byte[] bytes = msg.Writing();SocketAsyncEventArgs args = new SocketAsyncEventArgs();args.SetBuffer(bytes, 0, bytes.Length);args.Completed += (socket, args) =>{if (args.SocketError != SocketError.Success){print("发送消息失败" + args.SocketError);Close();}};this.socket.SendAsync(args);}else{Close();}}//处理接受消息 分包、黏包问题的方法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){//解析IDmsgID = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;//解析长度msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);nowIndex += 4;}if (cacheNum - nowIndex >= msgLength && msgLength != -1){//解析消息体//BaseMsg baseMsg = null;//BaseHandler handler = null;//目标一:不需要每次手动的去添加代码//添加了消息后 根据这个ID 就能自动的去根据ID得到对应的消息类 来进行反序列化//switch (msgID)//{// case 1001:// baseMsg = new PlayerMsg();// handler = new PlayerMsgHandler();// baseMsg.Reading(cacheBytes, nowIndex);// handler.message = baseMsg;// break;//}//if (baseMsg != null)// receiveQueue.Enqueue(handler);//得到一个指定ID的消息类对象 只不过是用父类装子类BaseMsg baseMsg = msgPool.GetMessage(msgID);if(baseMsg !=null){//反序列化baseMsg.Reading(cacheBytes, nowIndex);BaseHandler baseHandler = msgPool.GetHandler(msgID);baseHandler.message = 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;}}}private void OnDestroy(){Close(true);}
}
这段代码是 Unity 中实现的异步网络通信管理器,用于处理客户端与服务器的 TCP 连接、消息收发及消息处理,核心功能如下:
1. 单例模式与初始化
- 单例实例:通过
Awake
方法确保全局唯一实例,跨场景保持连接状态。 - 心跳机制:通过
InvokeRepeating
定时发送心跳消息(HeartMsg
),维持长连接。 - 消息池依赖:使用
MsgPool
管理消息实例和处理器,实现消息类型与 ID 的动态映射。
2. 网络连接管理
2.1 连接服务器
- 异步连接:通过
SocketAsyncEventArgs
实现非阻塞连接,连接成功后立即注册异步接收回调(ReceiveCallBack
)。 - 参数配置:支持传入 IP 和端口,创建 TCP 流式套接字(
SocketType.Stream
)。
2.2 关闭连接
- 优雅断开:发送退出消息(
QuitMsg
)后关闭套接字,处理重连逻辑(预留扩展)。 - 错误处理:连接 / 收发失败时自动关闭连接,触发可能的重连机制。
3. 消息收发与序列化
3.1 发送消息
- 通用接口:
Send
方法接受BaseMsg
子类(如PlayerMsg
),自动调用序列化逻辑(Writing
)生成字节数组。 - 异步发送:通过
SocketAsyncEventArgs
实现非阻塞发送,发送失败时关闭连接。
3.2 接收消息
- 异步接收:使用
SocketAsyncEventArgs
循环接收数据,存入缓存数组cacheBytes
。 - 粘包 / 分包处理:
- 先读取前 8 字节(4 字节消息 ID + 4 字节消息体长度)。
- 根据长度读取完整消息体,剩余数据缓存至下次解析。
- 通过
MsgPool
根据 ID 动态创建消息实例(如PlayerMsg
),调用反序列化方法(Reading
)。
4. 消息处理流程
- 队列解耦:接收的消息处理器(
BaseHandler
)存入receiveQueue
,通过Update
帧循环处理,避免阻塞网络线程。 - 动态分发:通过消息池获取处理器(如
PlayerMsgHandler
),将消息实例注入处理器,实现业务逻辑与网络层分离。
5. 核心组件与依赖
MsgPool
:维护消息 ID 与类型的映射,通过反射创建实例,避免硬编码switch-case
。- 协议基类:
BaseMsg
:定义消息序列化(Writing
)、反序列化(Reading
)、获取 ID(GetID
)接口。BaseHandler
:消息处理器基类,持有消息实例(message
属性),子类实现MsgHandler
具体逻辑。
- 工具方法:使用
BitConverter
解析消息 ID 和长度,确保字节序一致性(默认本地字节序,需根据服务器调整)。
6. 扩展与优化点
- 线程安全:
receiveQueue
需考虑多线程访问安全(当前仅主线程操作,无需锁)。 - 加密与压缩:可在
Writing
/Reading
中添加数据加密(如 AES)或压缩(如 Zlib)逻辑。 - 连接重试:
Close
方法中预留的 “断线重连” 逻辑需补充具体实现。 - 日志系统:当前仅用
print
输出,可集成更完善的日志记录(如错误等级、消息统计)。
总结
该代码实现了基于 TCP 的异步网络通信框架,具备连接管理、心跳维持、自动序列化 / 反序列化、消息分发解耦等功能,适用于实时性要求较高的游戏或应用场景。通过消息池和基类设计,降低了协议扩展的复杂度,开发者只需新增 XML 配置和处理器逻辑,即可快速支持新消息类型。