Unity网络开发实践项目

news2025/6/2 12:30:50

摘要:该网络通信系统基于Unity实现,包含以下几个核心模块:

  1. 协议配置:通过XML定义枚举(如玩家/英雄类型)、数据结构(如PlayerData)及消息协议(如PlayerMsg),支持基础类型、数组、字典等复杂结构。
  2. 代码生成工具:解析XML自动生成C#脚本,包括枚举类、可序列化的数据结构类(实现字节计算、序列化/反序列化)、消息类及消息池,减少手动编码。
  3. 网络管理器:采用异步Socket实现TCP通信,处理连接、心跳包(间隔2秒)、消息收发及粘包/分包问题,通过消息池动态映射ID与消息类型,结合队列机制解耦网络层与业务逻辑。
  4. 扩展性:支持多命名空间、自动目录生成,预留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 配置文件自动生成多语言协议代码,主要功能如下:

  1. 配置解析:读取 XML 配置文件(如用户提供的协议定义),提取枚举、数据结构和消息定义。

  2. 代码生成

    • 通过菜单命令(ProtocolTool / 生成 C# 脚本)触发,生成 C# 协议类文件
    • 支持生成:枚举类型、数据结构类、消息类、消息池管理类
    • 预留了 C++ 和 Java 代码生成接口(仅打印日志)
  3. 工具集成

    • 在 Unity 编辑器菜单中添加功能入口
    • 生成后自动刷新项目视图,无需手动操作
  4. 核心逻辑

    • 使用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)
        {
            //消息ID
            idStr = 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)
        {
            //记录所有消息的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 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";
            }
            else
                bytesNumStr += "\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";
            }
            else
                readingStr += "\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方法)。
    • 提供GetMessageGetHandler方法,通过反射创建消息实例和处理器。
  • 输出:消息池管理脚本(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=4string需计算 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 并生成代码。
  • 依赖项:需提前定义BaseDataBaseMsg抽象类,以及序列化工具方法(如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;

    //和服务器进行连接的 Socket
    private 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 update
    void 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 frame
    void 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);
            else
                Close();
        }
        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)
            {
                //解析ID
                msgID = 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
  • 粘包 / 分包处理
    1. 先读取前 8 字节(4 字节消息 ID + 4 字节消息体长度)。
    2. 根据长度读取完整消息体,剩余数据缓存至下次解析。
    3. 通过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 配置和处理器逻辑,即可快速支持新消息类型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394254.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jetson Orin Nano - SONY imx415 camera驱动开发

目录 前言: 调试准备工作: 修改内核默认打印等级 一、imx415驱动开发 1、硬件接线 2、设备树修改 2.1 创建 tegra234-p3767-camera-p3768-imx415-C-4lane.dtsi 文件 2.2 tegra234-p3767-camera-p3768-imx415-C-4lane.dtsi 添加到设备树 2.3 编译设备树 3、imx415驱动…

word为跨页表格新加表头和表名

问题&#xff1a; 当表格过长需要跨页时&#xff08;如下图所示&#xff09;&#xff0c;某些格式要求需要转页接排加续表。 方法一&#xff1a; 1、选中表格&#xff0c;在“表布局”区域点开“自动调整”&#xff0c;选择“固定列宽”&#xff08;防止后续拆分表格后表格变…

测试用例篇章

本节概要&#xff1a; 测试⽤例的概念 设计测试⽤例的万能思路 设计测试⽤例的⽅法 一、测试用例 1.1 概念 什么是测试用例&#xff1f; 测试⽤例&#xff08;Test Case&#xff09;是为了实施测试⽽向被测试的系统提供的⼀组集合&#xff0c;这组集合包含&#xff1a;测…

2025年北京市职工职业技能大赛第六届信息通信行业网络安全技能大赛复赛CTF部分WP-哥斯拉流量分析

2025年北京市职工职业技能大赛第六届信息通信行业网络安全技能大赛复赛CTF部分WP-哥斯拉流量分析 一、流量分析 题目没有任何提示,附件gzl.pcap 解题哥斯拉流量300多KB包很多,没啥经验只能挨个看回来之后又狠狠得撸了一把哥斯拉流量分析我这里用的是哥斯拉4.0.1 测试链接…

Django ToDoWeb 服务

我们的任务是使用 Django 创建一个简单的 ToDo 应用程序,允许用户添加、查看和删除笔记。我们将通过设置 Django 项目、创建 Todo 模型、设计表单和视图来处理用户输入以及创建模板来显示任务来构建它。我们将逐步实现核心功能以有效地管理 todo 项。 Django ToDoWeb 服务 …

各种数据库,行式、列式、文档型、KV、时序、向量、图究竟怎么选?

慕然回首&#xff0c;发现这些年来涌现出了许多类型的数据库&#xff0c;今天抽空简单回顾一下&#xff0c;以便于后面用到时能快速选择。 1. 关系型数据库(行式) 关系型数据库&#xff08;RDBMS&#xff09;&#xff0c;我们常说的数据库就是指的关系型数据库。 它的全称是关…

全志科技携飞凌嵌入式T527核心板亮相OpenHarmony开发者大会

近日&#xff0c;OpenHarmony开发者大会2025&#xff08;OHDC.2025&#xff0c;以下简称“大会”&#xff09;在深圳举办&#xff0c;全志科技作为OpenHarmony生态的重要合作伙伴受邀参会&#xff0c;并进行了《全志科技行业智能芯片OpenHarmony方案适配与认证经验分享》的主题…

事件驱动架构入门

主要参考资料&#xff1a; 软件架构-事件驱动架构: https://blog.csdn.net/liuxinghao/article/details/113923639 目录 简介事件队列事件日志事件收集器响应队列读事件 vs. 写事件 简介 事件驱动架构是一种系统或组件之间通过发送事件和响应事件彼此交互的架构风格。当某个事…

基于Web的濒危野生动物保护信息管理系统设计(源码+定制+开发)濒危野生动物监测与保护平台开发 面向公众参与的野生动物保护与预警信息系统

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

leetcode hot100刷题日记——30.两数之和

解答&#xff1a; 方法一&#xff1a;迭代 迭代大致过程就是&#xff1a; 算两条链表的当前位的和&#xff0c;加上上一位留下来的进位&#xff0c;就是新链表的当前位的数字。计算当前的进位。 这样&#xff0c;我们迭代需要的东西是&#xff1a;链表1&#xff0c;链表2&…

Fastapi 学习使用

Fastapi 学习使用 Fastapi 可以用来快速搭建 Web 应用来进行接口的搭建。 参考文章&#xff1a;https://blog.csdn.net/liudadaxuexi/article/details/141062582 参考文章&#xff1a;https://blog.csdn.net/jcgeneral/article/details/146505880 参考文章&#xff1a;http…

rtpinsertsound:语音注入攻击!全参数详细教程!Kali Linux教程!

简介 2006年8月至9月期间&#xff0c;我们创建了一个用于将音频插入指定音频&#xff08;即RTP&#xff09;流的工具。该工具名为rtpinsertsound。 该工具已在Linux Red Hat Fedora Core 4平台&#xff08;奔腾IV&#xff0c;2.5 GHz&#xff09;上进行了测试&#xff0c;但预…

【C++】入门基础知识(1.5w字详解)

本篇博客给大家带来的是一些C基础知识&#xff01;包含函数栈帧的详解&#xff01; &#x1f41f;&#x1f41f;文章专栏&#xff1a;C &#x1f680;&#x1f680;若有问题评论区下讨论&#xff0c;我会及时回答 ❤❤欢迎大家点赞、收藏、分享&#xff01; 今日思想&#xff1…

Photoshop2025(PS2025)软件及安装教程

在数字图像编辑领域&#xff0c;Adobe Photoshop 一直是无可争议的王者。如今&#xff0c;Photoshop 2025 重磅登场&#xff0c;再次为我们带来了惊喜与变革&#xff0c;进一步巩固了它在行业中的领先地位。 Photoshop 2025 在人工智能方面的升级令人瞩目。其全新的 “Magic Se…

AI赋能开源:如何借助MCP快速解锁开源项目并提交你的首个PR

引子 很多同学都梦想为开源项目贡献力量&#xff0c;然而现实往往是——面对庞大复杂的项目&#xff0c;从入门到提交第一个有实质性代码的PR&#xff0c;时间跨度可能长达数年。传统路径通常是先从文档贡献开始&#xff0c;逐步深入理解项目架构&#xff0c;最终才能进行代码…

R 语言科研绘图第 52 期 --- 网络图-分组

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

姜老师的MBTI课程:MBTI是可以转变的

我们先来看内向和外向这条轴&#xff0c;I和E内向和外向受先天遗传因素的影响还是比较大的&#xff0c;因为它事关到了你的硬件&#xff0c;也就是大脑的模型。但是我们在大五人格的排雷避坑和这套课程里面都强调了一个观点&#xff0c;内向和外向各有优势&#xff0c;也各有不…

Django【应用 02】第一个Django应用开发流程图

第 1 部分 安装 Django创建项目初始化应用配置视图、路由 第 2 部分 数据库配置语言和时区配置应用设置表初始化模型创建、激活、表创建管理员账号创建应用加入管理页面 第 3 部分 更多视图&#xff08;添加模板及模板调用、render、get_object_or_404、去除模板里的硬编码…

python打卡训练营打卡记录day41

知识回顾 数据增强卷积神经网络定义的写法batch归一化&#xff1a;调整一个批次的分布&#xff0c;常用与图像数据特征图&#xff1a;只有卷积操作输出的才叫特征图调度器&#xff1a;直接修改基础学习率 卷积操作常见流程如下&#xff1a; 1. 输入 → 卷积层 → Batch归一化层…

GD32F103系列工程模版创建记录

准备条件&#xff1a; 1&#xff1a;首先需要下载GD32F103的官方库 2&#xff1a;GD32F103的软件包 3&#xff1a;KEIL5软件 4&#xff1a;单片机GD32F103C8T6 本文已经默认KEIL5已将安装好GD32F103的软件包了 步骤一 基本模版创建 1 打开KEIL5软件&#xff0c;新建工程&am…