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

【5】openGL使用宏和函数进行错误检测

当我们编写openGL程序,没有报编译链接错误,但是运行结果是黑屏,这不是我们想要的。

openGL提供了glGetError 来检查错误,我们可以通过在运行时进行打断点查看glGetError返回值,得到的是一个十进制数,将其转为十六进制,再转到 glew.h 里查询这个数,就能看到错误类型。

举个例子:

static void GLClearError() {while (glGetError() != GL_NO_ERROR); /* ? */
}static void GLCheckError() {while (GLenum error = glGetError()){std::cout << "[OpenGL_Error] (" << error << ")" << std::endl;}
}
    GLClearError();/*清除错误*/glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);GLCheckError();

上面这是正常代码。现在我们将 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);GL_UNSIGNED_INT 改为 GL_INT。如果不添加错误信息检测代码,直接运行的话,只能看到黑屏,不会有错误提示,有了错误检测,会看到终端循环输出错误码:1280。1280的十六进制是0x0500,转到 glew.h查看,错误类型:在这里插入图片描述
循环输出错误码,是不太好,我们想让他出现错误就跟碰到断点一样停下来,因此定义一个宏:

#define ASSERT(x) if(!(x)) __debugbreak();

再把清除错误和错误检查的代码写在一个函数里,同时我们还想得到错误在哪一行,函数名,文件名:

static bool GLLogCall(const char* function, const char* file, int line) {while (GLenum error = glGetError()){std::cout << "[OpenGL_Error] (" << error << "): " << function<< " " << file << ":" << line << std::endl;return false;}return true;
}
/*注意 \ 后面直接打回车,不要打空格*/
#define GLCall(x) GLClearError();\x;\ASSERT(GLLogCall(#x, __FILE__, __LINE__))

这样就简化了操作,还得到了更多信息:

GLCall(glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr));

完整代码:

#include <iostream>
#include <string> #include <GL/glew.h> 
#include <GLFW/glfw3.h>
#include <fstream>
#include <sstream>#define ASSERT(x) if(!(x)) __debugbreak();
/*注意 \ 后面直接打回车,不要打空格*/
#define GLCall(x) GLClearError();\x;\ASSERT(GLLogCall(#x, __FILE__, __LINE__))static void GLClearError() {while (glGetError() != GL_NO_ERROR); /* ? */
}static void GLCheckError() {while (GLenum error = glGetError()){std::cout << "[OpenGL_Error] (" << error << ")" << std::endl;}
}static bool GLLogCall(const char* function, const char* file, int line) {while (GLenum error = glGetError()){std::cout << "[OpenGL_Error] (" << error << "): " << function<< " " << file << ":" << line << std::endl;return false;}return true;
}struct ShaderProgramSource
{std::string VertexSource;std::string FragmentSource;
};static ShaderProgramSource ParseShader(const std::string& filepath) {std::ifstream stream(filepath);/*您提出了一个好问题。从语法角度来分析一下,enum class 为什么被称为"带作用域的枚举类型":- 普通的 enum 定义是:enum EnumName {value1, value2}- 枚举值不加作用域,可以直接使用值名- 而 enum class 定义是:enum class EnumName {value1,value2} - 这里使用了class关键字- 根据C++标准,class关键字会为枚举类型生成一个新的作用域- 枚举值名会放在这个新的作用域中- 所以要使用枚举值名,需要加上作用域操作符::如EnumName::value1- 这样就隔离开其他作用域中的可能重复名称- 并防止枚举值名与其他名称冲突所以,从enum class语法中class关键字产生的作用域来看:- 它为枚举类型值名生成了一个独立的命名空间- 这就产生了"带作用域"的语义希望这个分析可以帮您理解enum class的语法机制!*/enum class ShaderType { /* 带作用域的枚举类型,不是类*/NONE = -1, VERTEX = 0, FRAGMENT = 1};std::string line;std::stringstream ss[2];ShaderType type = ShaderType::NONE;while (getline(stream, line)) {if (line.find("#shader") != std::string::npos) { /* 找到了*/if (line.find("vertex") != std::string::npos) {// set mode to vertextype = ShaderType::VERTEX;}else if (line.find("fragment") != std::string::npos) {// set mode to fragmenttype = ShaderType::FRAGMENT;}}else {ss[(int)type] << line << '\n';}}return { ss[0].str(), ss[1].str() };
}/*方便起见,写成一个函数*/
static unsigned int CompileShader(unsigned int type, const std::string& source) {unsigned int id = glCreateShader(type);/*vertex 或者 fragment */const char* src = source.c_str(); /*或者写 &source[0]*/glShaderSource(id, 1, &src, nullptr);glCompileShader(id);int result;glGetShaderiv(id, GL_COMPILE_STATUS, &result);if (result == GL_FALSE) {int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);// char message[length]; /*这里会发现因为长度不定,无法栈分配,但你仍要这么做*/char* message = (char*)alloca(length * sizeof(char));glGetShaderInfoLog(id, length, &length, message);std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex":"fragment" )<< "shader!请定位到此行" << std::endl;std::cout << message << std::endl;glDeleteShader(id);return 0;}return id;
}/*使用static是因为不想它泄露到其他翻译单元?
使用string不是最好的选择,但是相对安全, int类型-该着色器唯一标识符,一个ID*/
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader) {/*使用unsigned是因为它接受的参数就是这样,或者可以使用 GLuint,但是作者不喜欢这样,因为它要使用多个图像api*/unsigned int program = glCreateProgram();unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);glAttachShader(program, vs);glAttachShader(program, fs);glLinkProgram(program);glValidateProgram(program);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main(void)
{GLFWwindow* window;/* Initialize the library */if (!glfwInit())return -1;//if (glewInit() != GLEW_OK)/*glew文档,这里会报错,因为需要上下文,而上下文在后面*///    std::cout << "ERROR!-1" << std::endl;/* Create a windowed mode window and its OpenGL context */window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);if (!window){glfwTerminate();return -1;}/* Make the window's context current */glfwMakeContextCurrent(window);if (glewInit() != GLEW_OK)/*这里就不会报错了*/std::cout << "ERROR!-2" << std::endl;std::cout << glGetString(GL_VERSION) << std::endl;float positions[] = { /*冗余的点,因此需要index buffer*/-0.5f, -0.5f,// 00.5f, -0.5f,// 10.5f, 0.5f, // 2-0.5f, 0.5f, // 3};unsigned int indices[] = {0, 1, 2,2, 3, 0};/*这段代码是创建和初始化顶点缓冲对象(Vertex Buffer Object,简称VBO)。VBO是OpenGL中一个很重要的概念,用于高效渲染顶点数据。它这段代码的作用是:glGenBuffers生成一个新的VBO,ID保存到buffer变量中。glBindBuffer将这个VBO绑定到GL_ARRAY_BUFFER目标上。glBufferData向被绑定的这个VBO中填充实际的顶点数据。通过这三步:我们得到了一个可以存储顶点数据的VBO对象后续绘制调用只需要指定这个VBO就可以加载顶点数据教程强调VBO是因为:相对直接送入顶点更高效绘制调用不再需要每帧重复发送相同顶点提高渲染性能所以总结下VBO可以高效绘制复杂顶点数据至显卡,是OpenGL重要概念glGenBuffers(1, &buffer);
glGenBuffers作用是生成VBO对象的ID编号。第一个参数1表示要生成的VBO数量,这里只生成1个。第二个参数&buffer是用于返回生成的VBO ID编号。glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBindBuffer用于将VBO对象绑定到指定的目标上。第一个参数GL_ARRAY_BUFFER表示要绑定的目标是顶点属性数组缓冲。GL_ARRAY_BUFFER指定将要保存顶点属性数据如位置、颜色等。第二个参数buffer就是前面glGenBuffers生成的VBO ID。所以总结下:glGenBuffers生成1个VBO对象并获取ID编号glBindBuffer将这个VBO绑定到属性缓冲目标上,作为后续顶点数据的存储对象。glBufferData的作用是向之前绑定的VBO对象中填充实际的顶点数据。参数说明:GL_ARRAY_BUFFER:指定操作目标为顶点属性缓冲(与glBindBuffer一致)6 * sizeof(float):数据大小,这里 positions 数组有6个float数positions:数组指针,提供实际的数据源GL_STATIC_DRAW:数据使用模式GL_STATIC_DRAW:数据不会或很少改变
GL_DYNAMIC_DRAW:数据可能会被修改
GL_STREAM_DRAW:数据每次绘制都会改变
它的功能是:分配指定大小内存给当前绑定的VBO对象将positions数组内容拷贝到VBO对象内存中以GL_STATIC_DRAW模式,显卡知道如何优化分配内存这样一来,positions数组中的顶点数据就上传到GPU中VBO对象里了。OpenGL随后通过该VBO对象来读取顶点数据进行绘制。*/unsigned int buffer;glGenBuffers(1, &buffer);glBindBuffer(GL_ARRAY_BUFFER, buffer);glBufferData(GL_ARRAY_BUFFER, 6 * 2 * sizeof(float), positions, GL_STATIC_DRAW);glEnableVertexAttribArray(0);/*index-只有一个属性,填0size-两个数表示一个点,填2stripe-顶点之间的字节数pointer-偏移量好的,我们来用一个例子来解释glVertexAttribPointer的参数含义:假设我们有一个VBO,里面存放3个三维顶点数据,每个顶点由(x,y,z)组成,每个元素类型为float。那么数据在VBO中排列如下:VBO地址 | 数据
0     |  x1
4     |  y1\
8     |  z1
12     |  x2
16     |  y2
20     |  z2
24     |  x3
28     |  y3
32     |  z3现在我们要告诉OpenGL如何解析这些数据:glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 12, 0);- 0:属性为位置数据
- 3:每个位置由3个float组成,(x,y,z)
- GL_FLOAT:数据类型是float
- 12:当前属性到下一个属性的间隔,即一个顶点需要12个字节
- 0:这个属性起始位置就是VBO的开头这样OpenGL就知道:- 从VBO开始地址读取3个float作为第一个顶点的位置
- 下一个顶点偏移12字节再读取3个float最后一个参数0就是告诉OpenGL属性的起始读取偏移是多少。好的,用一个例子来具体说明一下这种情况:假设我们有一个VBO来存储顶点数据,每个顶点包含位置和颜色两个属性。数据在VBO内部的排列方式为:位置x | 位置y | 位置z | 颜色r | 颜色g | 颜色b那么对于第一个顶点来说,它在VBO内的布局是:VBO地址 | 数据
0     |  位置x\
4     |  位置y
8     |  位置z
12    |  颜色r
16    |  颜色g
20    |  颜色b此时,我们设置位置属性和颜色属性的指针:glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, 12);可以看到:- 位置属性从0字节处开始读取
- 颜色属性从12字节处开始读取(让出位置数据占用的空间)这就是为什么位置属性的偏移不能写0,需要指定非0偏移量让出给颜色属性存储空间。这样才能正确解析这两个分开但共处一个VBO的数据。*/glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);/* (const void)*/unsigned int ibo;glGenBuffers(1, &ibo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);// 测试 ShaderProgramSourceShaderProgramSource source = ParseShader("res/shaders/Basic.shader"); unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);glUseProgram(shader);/* Loop until the user closes the window */while (!glfwWindowShouldClose(window)){/* Render here */glClear(GL_COLOR_BUFFER_BIT);/*检查错误,例如下面经典错误,如果不检查,得到的结果只是输出一个黑屏没有图像*///GLClearError();/*清除错误*/ glDrawArrays(GL_TRIANGLES, 0, 6);//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); GLCheckError();///*GLCheckError();检查错误,上一条语句错误测试,应该使用GL_UNSIGNED_INT,//而不是GL_INT,显示错误1280,//转十六位 0x500,可以在glew.h查看定义*////*使用GLCheckError比较麻烦,需要执行的时候手动打断点,因此换一个*///ASSERT(GLLogCall());/*每次检查错误都要在语句前面使用使用GLCearError()很麻烦,修改的更方便*/GLCall(glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr));/*    glBegin(GL_TRIANGLES);glVertex2f(-0.5f, 0.5f);glVertex2f(0.0f, 0.0f);glVertex2f(0.5f, 0.5f);glEnd();*//* Swap front and back buffers */glfwSwapBuffers(window);/* Poll for and process events */glfwPollEvents();}glDeleteProgram(shader);glfwTerminate();return 0;
}

相关文章:

【5】openGL使用宏和函数进行错误检测

当我们编写openGL程序&#xff0c;没有报编译链接错误&#xff0c;但是运行结果是黑屏&#xff0c;这不是我们想要的。 openGL提供了glGetError 来检查错误&#xff0c;我们可以通过在运行时进行打断点查看glGetError返回值&#xff0c;得到的是一个十进制数&#xff0c;将其转…...

STM32 CAN快速配置(HAL库版本)

STM32 CAN快速配置&#xff08;HAL库版本&#xff09; 目录 STM32 CAN快速配置&#xff08;HAL库版本&#xff09;前言1 软件编程1.1 初始化1.1.1 引脚设置1.1.2 CAN参数设置1.1.3 CAN滤波器设置 1.2 CAN发送1.3 CAN接收 2 运行测试结束语 前言 控制器局域网总线&#xff08;CA…...

【文末送书】全栈开发流程——后端连接数据源(二)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…...

leetcode_27_最小栈

class MinStack { public:MinStack() {}void push(int val) {//只要是压栈&#xff0c;先将元素保存到_elem中_elem.push(val);//如果x小于_min中栈顶的元素&#xff0c;将x再压入_min中if(_min.empty() || val < _min.top()){_min.push(val);}}void pop() {//如果——min栈…...

01-ZooKeeper快速入门

1 Zookeeper概念 Zookeeper是Apache Hadoop项目下的一个子项目&#xff0c;是一个树形目录服务。 zookeeper翻译过来就是 动物园管理员&#xff0c;它是用来管理Hadoop&#xff08;大象&#xff09;、Hive&#xff08;蜜蜂&#xff09;、Pig&#xff08;小猪&#xff09;的管…...

[经典面试题]JS的typeof和instanceof区别

一、typeof typeof 是一个一元操作符不是函数&#xff0c;所以不需要传递参数&#xff0c;使用方法非常简单&#xff1a;typeof A 对于基本类型 let s "Nicholas"; let b true; let i 22; let u; let sb undefined; console.log(typeof s); // string console.…...

C++内存区堆和栈

在C中&#xff0c;内存分成5个区&#xff0c;堆、栈、自由存储区、全局/静态存储区和常量存储区。 栈&#xff0c;就是那些由编译器在需要的时候分配&#xff0c;在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。 堆&#xff0c;就是那些…...

QT中闹钟的设置

.h文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPushButton> //按钮 #include <QTextEdit> //文本 #include <QLabel> //标签 #include <QLineEdit> //行编辑器#include <QTimerEvent> //定时器事件类头文件 #…...

【Redis】几款redis可视化工具(推荐Another Redis Desktop Manager)

Redis是一个超精简的基于内存的键值对数据库(key-value)&#xff0c;一般对并发有一定要求的应用都用其储存session&#xff0c;乃至整个数据库。不过它公自带一个最小化的命令行式的数据库管理工具&#xff0c;有时侯使用起来并不方便。不过Github上面已经有了很多图形化的管理…...

肖sir__设计测试用例方法之因果图07_(黑盒测试)

设计测试用例方法之因果图 一、定义&#xff1a;因果图提供了一个把规格转化为判定表的系统化方法&#xff0c;从该图中可以产生测试数据。其 中&#xff0c;原因是表示输入条件&#xff0c;结果是对输入执 行的一系列计算后得到的输出。 二、因果图方法最终生成的就是判定表。…...

李宏毅-21-hw3:对11种食物进行分类-CNN

一、代码慢慢阅读理解总结内化&#xff1a; 1.关于torch.nn.covd2d()的参数含义、具体用法、功能&#xff1a; &#xff08;1&#xff09;参数含义&#xff1a; 注意&#xff0c;里面的“padding”参数&#xff1a;《both》side所以是上下左右《四》边都会加一个padding数量…...

成集云 | 飞书审批同步金蝶云星空销售订单 | 解决方案

源系统成集云目标系统 方案介绍 飞书是字节跳动于2016年自研的新一代一站式协作平台&#xff0c;将即时沟通、日历、云文档、云盘和工作台深度整合&#xff0c;通过开放兼容的平台&#xff0c;让成员在一处即可实现高效的沟通和流畅的协作&#xff0c;全方位提升企业效率。 …...

06 科技英语|控制与优化学科词汇

maneuver n 策略&#xff1b;v 操控、调遣 manipulate vt 熟练控制 scalability n 可扩展性 leverage n 杠杆&#xff1b;v 促使...改变 flexibility n 弹性 dispatch n 急件&#xff1b;v 调度&#xff1b;派遣 leverage …...

【网络教程】GitHub搜索技巧大揭秘

文章目录 1. 使用关键词优化搜索2. 结合布尔运算符3. 利用星号扩展搜索4. 高级搜索语法5. 按照星标数量搜索6. 使用文件类型搜索7. 在特定分支上搜索8. 使用文件名搜索9. 搜索贡献者10. 使用标签筛选仓库在开发过程中,我们经常需要在GitHub上查找代码、库或相关文档。本文将介…...

AUTOSAR LIN: LDF(LIN Description File)文件解析

LDF文件示例 LIN_description_file "lin_example.ldf" { LIN_protocol_version "2.0";LIN_language_version "2.0";nodes {master: MasterNode;slaves: SlaveNode1, SlaveNode2;};signals {Signal1: MasterNode, SlaveNode1;Signal2: Maste…...

Vue.js 报错:Cannot read property ‘validate‘ of undefined“

错误解决 起因&#xff0c;是我将elemnt-ui登录&#xff0c;默认放在mounted()函数里面&#xff0c;导致vue初始化就调用这个函数。 找了网上&#xff0c;有以下错误原因&#xff1a; 1.一个是你ref写错了&#xff0c;导致获取不了这个表单dom&#xff0c;我这显然不是。 2.…...

vue使用wangEditor

vue版本2.0&#xff1b;editor5.1.23版本&#xff1b;editor-for-vue&#xff1a;1.0.2版本 api文档入口 效果图 安装步骤入口 npm install wangeditor/editor --savenpm install wangeditor/editor-for-vue --save代码 <template><div><div style"bor…...

网络编程、socket编程、多进程并发服务器

网络编程 一、TCP编程的API socket: int socket(int domain, int type, int protocol); 返回值&#xff1a;> 0 代表函数调用成功&#xff0c;这个值是一个文件描述符< 0 代表函数调用失败 int domain&#xff1a;地址簇 AF_INET&#xff1a;IPv4 AF_INET6: IPv6 ​ i…...

Elasticsearch:自动使用服务器时间设置日期字段并更新时区

在大多数情况下&#xff0c;你的数据包含一个以 create_date 命名的字段。 即使没有日期字段&#xff0c;处理各种格式和时区的日期对数据仓库来说也是一个重大挑战。 与此类似&#xff0c;如果要检测变化的数据&#xff0c;则必须准确设置日期字段。 在 Elasticsearch 中还有…...

网络技术三:局域网基本原理

局域网基本原理 使用的协议及线缆 物理层 双绞线 同轴电缆 光纤 无线电 数据链路层 以太网 唯一事实标准 令牌环 淘汰 FDDI 光纤分布式接口 网络层 IP 唯一的事实标准 IPX 淘汰 Apple talk 淘汰 局域网设备 集线器 内部为总线型拓扑 任意时间只能由一台主机占用总线&a…...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢

随着互联网技术的飞速发展&#xff0c;消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁&#xff0c;不仅优化了客户体验&#xff0c;还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用&#xff0c;并…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题

在数字化浪潮席卷全球的今天&#xff0c;软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件&#xff0c;这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下&#xff0c;实现高效测试与快速迭代&#xff1f;这一命题正考验着…...

嵌入式常见 CPU 架构

架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集&#xff0c;单周期执行&#xff1b;低功耗、CIP 独立外设&#xff1b;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel&#xff08;原始…...

vue3 daterange正则踩坑

<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...

Unity中的transform.up

2025年6月8日&#xff0c;周日下午 在Unity中&#xff0c;transform.up是Transform组件的一个属性&#xff0c;表示游戏对象在世界空间中的“上”方向&#xff08;Y轴正方向&#xff09;&#xff0c;且会随对象旋转动态变化。以下是关键点解析&#xff1a; 基本定义 transfor…...

沙箱虚拟化技术虚拟机容器之间的关系详解

问题 沙箱、虚拟化、容器三者分开一一介绍的话我知道他们各自都是什么东西&#xff0c;但是如果把三者放在一起&#xff0c;它们之间到底什么关系&#xff1f;又有什么联系呢&#xff1f;我不是很明白&#xff01;&#xff01;&#xff01; 就比如说&#xff1a; 沙箱&#…...

在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7

在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤&#xff1a; 第一步&#xff1a; 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为&#xff1a; // 改为 v…...

Vue 3 + WebSocket 实战:公司通知实时推送功能详解

&#x1f4e2; Vue 3 WebSocket 实战&#xff1a;公司通知实时推送功能详解 &#x1f4cc; 收藏 点赞 关注&#xff0c;项目中要用到推送功能时就不怕找不到了&#xff01; 实时通知是企业系统中常见的功能&#xff0c;比如&#xff1a;管理员发布通知后&#xff0c;所有用户…...