当前位置: 首页 > news >正文

ProtoEditor - 如何在Unity中实现一个Protobuf通信协议类编辑器

文章目录

  • 简介
    • Protobuf 语法规则
    • Proto Editor
  • 实现
    • 创建窗口
    • 定义类、字段
    • 增删类
    • 编辑字段
    • 导入、导出Json文件
    • 生成.proto文件
    • 生成.bat文件


简介

Socket网络编程中,假如使用Protobuf作为网络通信协议,需要了解Protobuf语法规则、编写.proto文件并通过编译指令将.proto文件转化为.cs脚本文件,本文介绍如何在Unity中实现一个编辑器工具来使开发人员不再需要关注这些语法规则、编译指令,以及更便捷的编辑和修改.proto文件内容。工具已上传至SKFramework框架Package Manager中:

SKFramework PackageManager

Protobuf 语法规则

在介绍工具之前先简单介绍protobuf的语法规则,以便更好的理解工具的作用,下面是一个proto文件的示例:

message AvatarProperty
{required string userId = 1;required float posX = 2;required float posY = 3;required float posZ = 4;required float rotX = 5;required float rotY = 6;required float rotZ = 7;required float speed = 8;
}
  • 类通过message来声明,后面是类的命名
  • 字段修饰符包含三种类型:
    • required : 不可增加或删除的字段,必须初始化
    • optional : 可选字段,可删除,可以不初始化
    • repeated : 可重复字段(对应C#里面的List)
  • 与C#的字段类型对应关系如下,查阅自官网
.proto TypeC# TypeNotes
doubledouble
floatfloat
int32intUses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.
int64longUses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.
uint32uintUses variable-length encoding.
uint64ulongUses variable-length encoding.
sint32intUses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.
sint64longUses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.
fixed32uintAlways four bytes. More efficient than uint32 if values are often greater than 228.
fixed64ulongAlways eight bytes. More efficient than uint64 if values are often greater than 256.
sfixed32intAlways four bytes.
sfixed64longAlways eight bytes.
boolbool
stringstringA string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.
bytesByteStringMay contain any arbitrary sequence of bytes no longer than 232.
  • 标识号:示例中的1-8表示每个字段的标识号,并不是赋值。

每个字段都有唯一的标识号,这些标识符是用来在消息的二进制格式中识别各个字段的。[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。:要为将来有可能添加的、频繁出现的标识号预留一些标识号,不可以使用其中的[19000-19999]标识号,Protobuf协议实现中对这些进行了预留。

Proto Editor

Proto Editor

如图所示,工具包含以下功能:

  • New、Clear Message:增加、删除message类;

New、Clear Message

  • 增加、删除、编辑fields字段(修饰符、类型、命名、分配标识号);

增删字段

  • Import、Export Json File:导入、导出json文件(假如要修改一个已有的通信协议类,导入之前导出的Json文件再次编辑即可);

Import Json File

  • Generate Proto File:生成.proto文件;
  • Create .bat:生成.bat文件(不再需要手动编辑编译指令)。

生成的.proto & .bat文件

实现

创建窗口

  • 继承Editor Window编辑器窗口类;
  • Menu Item添加打开窗口的菜单;
public class ProtoEditor : EditorWindow
{[MenuItem("Multiplayer/Proto Editor")]public static void Open(){GetWindow<ProtoEditor>("Proto Editor").Show();}
}

定义类、字段

/// <summary>
/// 类
/// </summary>
public class Message
{/// <summary>/// 类名/// </summary>public string name = "New Message";/// <summary>/// 所有字段/// </summary>public List<Fields> fieldsList = new List<Fields>(0);
}
/// <summary>
/// 字段
/// </summary>
public class Fields
{public ModifierType modifier;public FieldsType type;public string typeName;public string name;public int flag;
}
  • Modifer Type:修饰符类型
/// <summary>
/// 修饰符类型
/// </summary>
public enum ModifierType
{/// <summary>/// 必需字段/// </summary>Required,/// <summary>/// 可选字段/// </summary>Optional,/// <summary>/// 可重复字段/// </summary>Repeated
}
  • Fields Type:字段类型

这里只定义了我常用的几种类型,Custom用于自定义类型:

/// <summary>
/// 字段类型
/// </summary>
public enum FieldsType
{Double,Float,Int,Long,Bool,String,Custom,
}

增删类

  • 声明一个列表存储所有类
//存储所有类
private List<Message> messages = new List<Message>();
  • 声明一个字典用于存储折叠栏状态(每个类可折叠查看)
//字段存储折叠状态
private readonly Dictionary<Message, bool> foldoutDic = new Dictionary<Message, bool>();
  • 插入、删除
//滚动视图
scroll = GUILayout.BeginScrollView(scroll);
for (int i = 0; i < messages.Count; i++)
{var message = messages[i];GUILayout.BeginHorizontal();foldoutDic[message] = EditorGUILayout.Foldout(foldoutDic[message], message.name, true);//插入新类if (GUILayout.Button("+", GUILayout.Width(20f))){Message insertMessage = new Message();messages.Insert(i + 1, insertMessage);foldoutDic.Add(insertMessage, true);Repaint();return;}//删除该类if (GUILayout.Button("-", GUILayout.Width(20f))){messages.Remove(message);foldoutDic.Remove(message);Repaint();return;}GUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
  • 底部新增、清空菜单:
GUILayout.BeginHorizontal();
//创建新的类
if (GUILayout.Button("New Message"))
{Message message = new Message();messages.Add(message);foldoutDic.Add(message, true);
}
//清空所有类
if (GUILayout.Button("Clear Messages"))
{//确认弹窗if (EditorUtility.DisplayDialog("Confirm", "是否确认清空所有类型?", "确认", "取消")){//清空messages.Clear();foldoutDic.Clear();//重新绘制Repaint();}
}
GUILayout.EndHorizontal();

编辑字段

  • 折叠栏为打开状态时,绘制该类具体的字段:
//如果折叠栏为打开状态 绘制具体字段内容
if (foldoutDic[message])
{//编辑类名message.name = EditorGUILayout.TextField("Name", message.name);//字段数量为0 提供按钮创建if (message.fieldsList.Count == 0){if (GUILayout.Button("New Field")){message.fieldsList.Add(new Fields(1));}}else{for (int j = 0; j < message.fieldsList.Count; j++){var item = message.fieldsList[j];GUILayout.BeginHorizontal();//修饰符类型item.modifier = (ModifierType)EditorGUILayout.EnumPopup(item.modifier);//字段类型item.type = (FieldsType)EditorGUILayout.EnumPopup(item.type);if (item.type == FieldsType.Custom){item.typeName = GUILayout.TextField(item.typeName);}//编辑字段名item.name = EditorGUILayout.TextField(item.name);GUILayout.Label("=", GUILayout.Width(15f));//分配标识号item.flag = EditorGUILayout.IntField(item.flag, GUILayout.Width(50f));//插入新字段if (GUILayout.Button("+", GUILayout.Width(20f))){message.fieldsList.Insert(j + 1, new Fields(message.fieldsList.Count + 1));Repaint();return;}//删除该字段if (GUILayout.Button("-", GUILayout.Width(20f))){message.fieldsList.Remove(item);Repaint();return;}GUILayout.EndHorizontal();}}
}

导入、导出Json文件

  • 导出Json文件以及生成Proto文件之前都需要判断当前的编辑是否有效,从以下几个方面判断:
    • proto file name:文件名编辑是否输入为空;
    • message name:类名编辑是否输入为空;
    • 自定义字段类型时,是否输入为空;
    • 标识号是否唯一 。

为Message、Fields类添加有效性判断函数:

/// <summary>
/// 类
/// </summary>
public class Message
{/// <summary>/// 类名/// </summary>public string name = "New Message";/// <summary>/// 所有字段/// </summary>public List<Fields> fieldsList = new List<Fields>(0);public bool IsValid(){bool flag = !string.IsNullOrEmpty(name);for (int i = 0; i < fieldsList.Count; i++){flag &= fieldsList[i].IsValid();if (!flag) return false;for (int j = 0; j < fieldsList.Count; j++){if (i != j){flag &= fieldsList[i].flag != fieldsList[j].flag;}if (!flag) return false;}}return flag;}
}
/// <summary>
/// 字段
/// </summary>
public class Fields
{public ModifierType modifier;public FieldsType type;public string typeName;public string name;public int flag;public Fields() { }public Fields(int flag){modifier = ModifierType.Required;type = FieldsType.String;name = "FieldsName";typeName = "FieldsType";this.flag = flag;}public bool IsValid(){return type != FieldsType.Custom || (type == FieldsType.Custom && !string.IsNullOrEmpty(typeName));}
}
  • 最终编辑有效性判断:
//编辑的内容是否有效
private bool ContentIsValid()
{bool flag = !string.IsNullOrEmpty(fileName);flag &= messages.Count > 0;for (int i = 0; i < messages.Count; i++){flag &= messages[i].IsValid();if (!flag) break;}return flag;
}
  • 导入、导出Json:

GUILayout.BeginHorizontal();
//导出Json
if (GUILayout.Button("Export Json File"))
{if (!ContentIsValid()){EditorUtility.DisplayDialog("Error", "请按以下内容逐项检查:\r\n1.proto File Name是否为空\r\n2.message类名是否为空\r\n" +"3.字段类型为自定义时 是否填写了类型名称\r\n4.标识号是否唯一", "ok");}else{//文件夹路径string dirPath = Application.dataPath + workspacePath;//文件夹不存在则创建if (!Directory.Exists(dirPath))Directory.CreateDirectory(dirPath);//json文件路径string filePath = dirPath + "/" + fileName + ".json";if (EditorUtility.DisplayDialog("Confirm", "是否保存当前编辑内容到" + filePath, "确认", "取消")){//序列化string json = JsonMapper.ToJson(messages);//写入File.WriteAllText(filePath, json);//刷新AssetDatabase.Refresh();}}
}
//导入Json
if (GUILayout.Button("Import Json File"))
{//选择json文件路径string filePath = EditorUtility.OpenFilePanel("Import Json File", Application.dataPath + workspacePath, "json");//判断路径有效性if (File.Exists(filePath)){//读取json内容string json = File.ReadAllText(filePath);//清空messages.Clear();foldoutDic.Clear();//反序列化messages = JsonMapper.ToObject<List<Message>>(json);//填充字典for (int i = 0; i < messages.Count; i++){foldoutDic.Add(messages[i], true);}//文件名称FileInfo fileInfo = new FileInfo(filePath);fileName = fileInfo.Name.Replace(".json", "");//重新绘制Repaint();return;}
}
GUILayout.EndHorizontal();

生成.proto文件

主要是字符串拼接工作:

//生成proto文件
if (GUILayout.Button("Generate Proto File"))
{if (!ContentIsValid()){EditorUtility.DisplayDialog("Error", "请按以下内容逐项检查:\r\n1.proto File Name是否为空\r\n2.message类名是否为空\r\n" +"3.字段类型为自定义时 是否填写了类型名称\r\n4.标识号是否唯一", "ok");}else{string protoFilePath = EditorUtility.SaveFilePanel("Generate Proto File", Application.dataPath, fileName, "proto");if (!string.IsNullOrEmpty(protoFilePath)){StringBuilder protoContent = new StringBuilder();for (int i = 0; i < messages.Count; i++){var message = messages[i];StringBuilder sb = new StringBuilder();sb.Append("message " + message.name + "\r\n" + "{\r\n");for (int n = 0; n < message.fieldsList.Count; n++){var field = message.fieldsList[n];//缩进sb.Append("    ");//修饰符sb.Append(field.modifier.ToString().ToLower());//空格sb.Append(" ");//如果是自定义类型 拼接typeName switch (field.type){case FieldsType.Int: sb.Append("int32"); break;case FieldsType.Long: sb.Append("int64"); break;case FieldsType.Custom: sb.Append(field.typeName); break;default: sb.Append(field.type.ToString().ToLower()); break;}//空格sb.Append(" ");//字段名sb.Append(field.name);//等号sb.Append(" = ");//标识号sb.Append(field.flag);//分号及换行符sb.Append(";\r\n");}sb.Append("}\r\n");protoContent.Append(sb.ToString());}//写入文件File.WriteAllText(protoFilePath, protoContent.ToString());//刷新(假设路径在工程内 可以避免手动刷新才看到)AssetDatabase.Refresh();//打开该文件夹FileInfo fileInfo = new FileInfo(protoFilePath);Process.Start(fileInfo.Directory.FullName);}}
}

生成.bat文件

  • 使用OpenFolderPanel打开protogen.exe文件所在的文件夹,.bat文件需要生成在该文件夹下:

protogen.exe

  • 获取proto文件夹下的所有.proto文件的名称,拼接编译指令:
//创建.bat文件
if (GUILayout.Button("Create .bat"))
{//选择路径(protogen.exe所在的文件夹路径)string rootPath = EditorUtility.OpenFolderPanel("Create .bat file(protogen.exe所在的文件夹)", Application.dataPath, string.Empty);//取消if (string.IsNullOrEmpty(rootPath)) return;//protogen.exe文件路径string protogenPath = rootPath + "/protogen.exe";//不是protogen.exe所在的文件夹路径if (!File.Exists(protogenPath)){EditorUtility.DisplayDialog("Error", "请选择protogen.exe所在的文件夹路径", "ok");}else{string protoPath = rootPath + "/proto";DirectoryInfo di = new DirectoryInfo(protoPath);//获取所有.proto文件信息FileInfo[] protos = di.GetFiles("*.proto");//使用StringBuilder拼接字符串StringBuilder sb = new StringBuilder();//遍历for (int i = 0; i < protos.Length; i++){string proto = protos[i].Name;//拼接编译指令sb.Append(rootPath + @"/protogen.exe -i:proto\" + proto + @" -o:cs\" + proto.Replace(".proto", ".cs") + "\r\n");}sb.Append("pause");//生成".bat文件"string batPath = $"{rootPath}/run.bat";File.WriteAllText(batPath, sb.ToString());//打开该文件夹Process.Start(rootPath);}
}

最终运行.bat文件,就可以将.proto文件转化为.cs脚本文件:

运行.bat文件

相关文章:

ProtoEditor - 如何在Unity中实现一个Protobuf通信协议类编辑器

文章目录简介Protobuf 语法规则Proto Editor实现创建窗口定义类、字段增删类编辑字段导入、导出Json文件生成.proto文件生成.bat文件简介 在Socket网络编程中&#xff0c;假如使用Protobuf作为网络通信协议&#xff0c;需要了解Protobuf语法规则、编写.proto文件并通过编译指令…...

2022 OpenCV Spatial AI大赛前三名项目分享,开源、上手即用,优化了OAK智能双目相机的深度效果。

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手…...

Android 蓝牙开发——HCI log 分析(二十)

HCI log 是用来分析蓝牙设备之间的交互行为是否符合预期,是否符合蓝牙规范。对于蓝牙开发者来说,通过 HCI log 可以帮助我们更好地分析问题,理解蓝牙协议。 一、抓取HCI log 1、手机抓取HCI log 在开发者选项中打开启用蓝牙HCI信息收集日志开关,Android系统就开始自动地收…...

flask入门-4.项目实战

4. 项目实战1 1. 问答平台项目结构搭建 项目结构 config.py hostname "127.0.0.1" port 3306 username "root" password "root"database "flask_qa"# 在 app.config 中设置连接数据库的信息 SQLALCHEMY_DATABASE_URI f"…...

java 1(概要、变量与运算符)

java ——概要、变量与运算符 ✍作者&#xff1a;电子科大不知名程序员 &#x1f332;专栏&#xff1a;java学习指导 各位读者如果觉得博主写的不错&#xff0c;请诸位多多支持&#xff1b;如果有错误的地方&#xff0c;欢迎在评论区指出 目录java ——概要、变量与运算符命令行…...

​力扣解法汇总2363. 合并相似的物品

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 给你两个二维整数数组 items1 和 items2 &#xff0c;表示两个物品集合。每个数…...

2022年终总结-找回初心

和“那个夏天”群聊的几位死党聊完天后&#xff0c;发现自己已经忘了初心2年有余了&#xff0c;也是这次聊天让我重新燃起了要继续努力奋斗的想法。那就说一说2022年我过得如何吧。2022年过完春节刚来公司的几天就传来了一个好消息&#xff0c;我涨薪了。在没有涨薪之前私下有时…...

Allegro如何打开或者关闭DFA规则设置操作指导

Allegro如何打开或者关闭DFA规则设置操作指导 在用Allegro做PCB布局的时候,器件与器件之间的DFA规则可以避免器件出现装配问题。如下图 当DFA规则设置好之后,如何打开或者关闭规则,具体操作如下 点击Setup点击Constraints...

kind kubernetes 集群内如何通过 helm 部署定制化 Prometheus-Operator?

文章目录1. Prometheus 简介2. Prometheus 优势3. Prometheus 架构图4. Prometheus-Operator 简介5. Prometheus-Operator 架构图6. 环境准备7. Kind 部署 Kubernetes7.1 安装 Ingress-nginx 组件7.2 安装 Metric Server 组件8. helm 快速安装 Prometheus-Operator9. 定制 Prom…...

流媒体付服务器 ZLMediaKit 学习记录

1.官方github&#xff1a;ZLMediaKit 依赖于 media-server 库 #国内用户推荐从同步镜像网站gitee下载 git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit cd ZLMediaKit #千万不要忘记执行这句命令 git submodule update --init 之后 cd ZLMediaKit mkdir build…...

2023年了还不会写软件测试简历吗,那就来看这里吧,怎么样才能更容易让HR看到你的简历

作为软件测试的从业者&#xff0c;面试或者被面试都是常有的事。 可是不管怎样&#xff0c;和简历有着理不清的关系&#xff0c;面试官要通过简历了解面试者的基本信息、过往经历等。 面试者希望通过简历把自己最好的一面体现给面试官&#xff0c;所以在这场博弈中&#xff0…...

第四阶段08-基于element-ui的vue2.0脚手架(续)

42. VUE脚手架项目嵌套路由 在配置路由&#xff08;配置/src/router/index.js&#xff09;时&#xff0c;如果配置的路由对象是routes常量的直接数组元素&#xff0c;则此路由配置的视图会显示在App.vue的<router-view/>中。 在设计视图时&#xff0c;可能会出现<ro…...

数据库设计规范

三范式首先&#xff0c;设计数据库&#xff0c;要尽可能的满足三范式&#xff0c;遵循三范式开发会减少数据冗余、提升系统可扩展性和查询性能。第一范式的目标是确保每列的原子性如果每列都是不可再分的最小数据单元&#xff08;也称为最小的原子单元&#xff09;&#xff0c;…...

深入浅出PaddlePaddle函数——paddle.Tensor

分类目录&#xff1a;《深入浅出PaddlePaddle函数》总目录 Tensor是Paddle中最为基础的数据结构&#xff0c;有几种创建Tensor的不同方式&#xff1a; 用预先存在的数据创建1个Tensor&#xff0c;请参考paddle.to_tensor创建一个指定shape的Tensor&#xff0c;请参考paddle.on…...

docker删除已停止的容器

一、docker删除已停止的容器 1、根据容器的状态&#xff0c;删除Exited状态的容器 先停止容器、再删除镜像中的容器、最后删除none的镜像。执行命令如下&#xff1a; docker stop $(docker ps -a | grep "Exited" | awk {print $1 }) #停止容器 docker rm $(docke…...

JS#1 引入方式和基础语法

JavaScript(JS)是一门跨平台, 面向对象的脚本语言, 来控制网页行为的, 它能够是网页可交互一. 引入方式内部脚本与外部脚本内部脚本: 将JS代码定义在HTML页面中外部脚本: 将JS代码定义在外部JS文件中, 然后引入到HTML页面中注意: 在HTML中,JS代码必须位于<script></sc…...

面了一个测试工程师,明显感觉他背了很多面试题...

最近有朋友去字节面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…...

C#生成缩略图

using System;using System.Collections.Generic;using System.Drawing;using System.Drawing.Drawing2D;using System.Drawing.Imaging;using System.Text;namespace learun.util{public enum ThumbnailMode{/// <summary>/// 指定宽度&#xff0c;高度按照比例缩放/// …...

算法 # SimHash 算法:文本相似度、文本去重、海量文本快速查询

SimHash SimHash 是 Google 发明的海量网页去重的高效算法,将原始的文本映射为 64 位的二进制串,然后通过比较二进制的差异进而表示原始文本内容的差异。 传统的 Hash 算法只负责将原始内容尽量均匀随机地映射为一个 hash 值,原理上相当于伪随机数产生算法。SimHash 本身属…...

Java程序设计-JSP程序设计-SSM校园二手交易系统

摘 要 网络的广泛应用给生活带来了十分的便利。所以把二手物品交易管理与现在网络相结合&#xff0c;利用java技术建设二手物品交易系统&#xff0c;实现二手物品交易的信息化。则对于进一步提高二手物品交易管理发展&#xff0c;丰富二手物品交易管理经验能起到不少的促进作用…...

Dism++:Windows系统维护的高效解决方案

Dism&#xff1a;Windows系统维护的高效解决方案 【免费下载链接】Dism-Multi-language Dism Multi-language Support & BUG Report 项目地址: https://gitcode.com/gh_mirrors/di/Dism-Multi-language 你是否曾经遇到过这样的场景&#xff1f;系统盘空间莫名其妙被…...

2025届学术党必备的六大AI辅助论文网站横评

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 立足于学术写作范畴之内&#xff0c;人工智能工具能够极为显著地提高毕业论文在文献整理方面…...

Spyglass CDC实战:从约束到验证的完整流程解析

1. Spyglass CDC验证入门&#xff1a;为什么需要它&#xff1f; 第一次接触多时钟域设计时&#xff0c;我完全低估了CDC问题的复杂性。直到仿真阶段出现数据丢失&#xff0c;才意识到异步时钟域交互就像两个语言不通的人交流——如果没有合适的翻译机制&#xff08;同步器&…...

完全免费的Windows离线语音转文字工具:TMSpeech终极指南

完全免费的Windows离线语音转文字工具&#xff1a;TMSpeech终极指南 【免费下载链接】TMSpeech 腾讯会议摸鱼工具 项目地址: https://gitcode.com/gh_mirrors/tm/TMSpeech 还在为会议记录手忙脚乱&#xff1f;还在为在线课程笔记而烦恼&#xff1f;TMSpeech是你的完美解…...

Android Studio Gradle下载太慢,国内怎么最快加速

Gradle 下载慢是 Android 开发中最令人头疼的问题之一&#xff0c;主要原因是默认的 Maven Central 和 Google 仓库服务器都在国外。 要实现“最快加速”&#xff0c;你需要同时解决两个层面的下载问题&#xff1a; Gradle 本体下载慢&#xff08;下载 gradle-x.x.x-bin.zip&am…...

3步打造高效多平台直播:OBS Multi RTMP插件完整解决方案

3步打造高效多平台直播&#xff1a;OBS Multi RTMP插件完整解决方案 【免费下载链接】obs-multi-rtmp OBS複数サイト同時配信プラグイン 项目地址: https://gitcode.com/gh_mirrors/ob/obs-multi-rtmp 想要突破单一平台限制&#xff0c;实现多平台同步直播却苦于操作复杂…...

Java 19+ Loom生产事故复盘:某银行核心交易链路OOM崩溃始末,5个致命配置反模式曝光

第一章&#xff1a;Java Loom响应式编程转型的必要性与战略定位在高并发、低延迟、资源敏感型现代服务架构中&#xff0c;传统基于线程池的阻塞式I/O与回调驱动的响应式模型正面临双重瓶颈&#xff1a;JVM线程成本高企&#xff0c;而Project Reactor或RxJava等响应式库又引入了…...

Pixel Couplet Gen实操手册:像素春联生成结果导出PNG/SVG格式的前端实现方案

Pixel Couplet Gen实操手册&#xff1a;像素春联生成结果导出PNG/SVG格式的前端实现方案 1. 项目背景与核心价值 Pixel Couplet Gen是一款融合传统春节文化与现代像素艺术风格的创新工具。通过ModelScope大模型的文本生成能力&#xff0c;结合精心设计的8-bit视觉元素&#x…...

HTTP接口设计进阶技巧:http-api-guide高级应用解析

HTTP接口设计进阶技巧&#xff1a;http-api-guide高级应用解析 【免费下载链接】http-api-guide 项目地址: https://gitcode.com/gh_mirrors/ht/http-api-guide 在API开发领域&#xff0c;设计一套规范、高效且易于维护的HTTP接口至关重要。http-api-guide作为一份全面…...

计算机毕业设计:Python全国天气数据可视化与预测系统 Django框架 可视化 随机森林 爬虫 中国天气网 机器学习 深度学习(建议收藏)✅

1、项目介绍 技术栈 采用 Python 语言开发&#xff0c;基于 Django 框架搭建后端服务&#xff0c;使用 MySQL 数据库进行数据存储&#xff0c;通过 requests 爬虫技术从中国天气网采集历史天气数据&#xff0c;前端利用 Echarts 实现数据可视化展示&#xff0c;并运用机器学习…...