Protobuf序列化协议使用指南

news2025/1/29 6:57:41

简介

        在本篇博客中,将会介绍protobuf的理论及使用方法。该文章仅做分享使用及自我复习使用,使用的图片来自百度,无法找到作者,如若侵权请联系删除。

目录

简介

概述

1.protobuf是什么?

2.序列化/反序列是什么?

2.1 序列化与反序列化:定义与概念

2.2 序列化协议的特性

2.3 序列化的实现组件

2.4 常见序列化协议及选型建议

Protobuf:高效的序列化协议

典型应用场景与局限性

适用场景

局限性

Protobuf 的 IDL 文件示例

使用 Protocol Buffers 的完整流程

1. 定义 .proto 文件

2. 使用 protoc 编译 .proto 文件

命令示例

编译结果

3. 在项目中使用生成的代码

3.1 初始化消息对象并设置值

3.2 序列化消息

3.3 反序列化消息

4. 使用 CMake 集成

5. Protobuf 常用序列化与反序列化 API

protobuf消息格式

1. Protobuf 编码基础

1.1 Varints 编码规则

1.2 Varints 编码示例

2. 字段编号(Field Number)

3. 传输类型(Wire Type

4. Field 结构

5. Protobuf 编解码的关键点

示例解析

示例 1:简单整数编码

示例 2:嵌套消息编码

Protobuf 的负数编码

示例:


概述

1.protobuf是什么?

protobuf是 Google 的语言中立、平台中立、可扩展的序列化结构化数据机制

  • 类似于 XML,json,但更小、更快、更简单。

  • 只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码,使用各种语言轻松地在各种数据流中写入和读取结构化数据。

Protoc 自己的编译器,c++/python

2.序列化/反序列是什么?

        在了解protobuf,之前我们需要知道序列化和反序列是什么。

2.1 序列化与反序列化:定义与概念

        随着互联网的普及,机器间通信的需求迅速增长。为了实现设备间的信息交换,必须使用预先约定的通讯协议,而序列化和反序列化正是其中的重要组成部分。

        通讯协议通常采用分层模型,例如 TCP/IP 协议的四层模型和 OSI 的七层模型。根据 OSI 模型,序列化和反序列化位于展现层(Presentation Layer),其作用是:

  • 序列化:将应用层中的数据结构或对象(如 C++ 的类)转换为连续的二进制数据流。
  • 反序列化:将二进制数据流还原为应用层的数据结构或对象。

通常,序列化协议属于 TCP/IP 协议的应用层,而本文的讨论基于 OSI 七层模型。

2.2 序列化协议的特性

        每种序列化协议都有其独特的设计目标和使用场景,在技术选型时需要综合考虑以下几个维度:

  1. 通用性

    • 跨平台与跨语言支持:协议的技术通用性越强,使用范围就越广。
    • 流行度:协议的使用范围直接影响学习成本和支持的广度。
  2. 鲁棒性

    • 成熟度高的协议通常经历了全面的测试,能够提供稳定可靠的支持。
    • 支持多种语言或平台时,可能需要在功能与通用性之间作出权衡。
  3. 可读性与调试性

    • 像 JSON 和 XML 这种文本格式的序列化数据具有人类可读性,便于调试。
    • 二进制协议(如 Protobuf)虽然性能优越,但调试成本较高,尤其在跨团队或跨公司环境下。
  4. 性能

    • 序列化和反序列化过程的空间与时间开销会直接影响系统的性能。
    • 空间开销(如描述字段的冗余)可能导致存储和传输成本增加。
  5. 可扩展性与兼容性

    • 在业务需求快速变化的情况下,协议需要支持字段的动态扩展,避免对现有服务造成影响。
  6. 安全性

    • 在跨网络通信场景中,协议是否支持安全传输(如通过 HTTP/HTTPS)是一个关键考虑因素。

2.3 序列化的实现组件

一个典型的序列化协议实现包含以下几个核心组件:

  1. 接口描述语言(IDL)文件

    • IDL 是用于描述通讯双方数据结构及接口的语言,具有跨平台、跨语言的特点。
    • 协议通过 IDL 文件约定数据结构和接口。
  2. IDL 编译器

    • IDL 编译器将 IDL 文件转换为目标语言的代码或动态库,便于客户端和服务端使用。
    • 示例:Protobuf 的 protoc 工具和 ROS 的 genmsg 工具。
  3. Stub 和 Skeleton 库

    • Stub:位于客户端,负责序列化请求并将其发送至服务端,接收服务端的响应并反序列化。
    • Skeleton:位于服务端,负责接收客户端的序列化请求并反序列化,将结果发送回客户端。
  4. 应用层(Client/Server)

    • 应用层代码通过调用由 IDL 生成的类或结构体,与 Stub 和 Skeleton 进行交互。
  5. 底层协议栈

    • 序列化后的数据通过传输层、网络层等被发送至远端,实现设备间的通信。

2.4 常见序列化协议及选型建议

目前主流的序列化协议包括 Protobuf、JSON、XML、Thrift 等。以下是它们的特点及适用场景:

  • Protobuf:高性能的二进制协议,适用于高效数据传输的场景,但调试成本较高。
  • JSON:易读易用,适合需要频繁调试和高可读性的场景。
  • XML:虽然冗余较大,但在一些老旧系统中仍然常见。
  • Thrift:功能丰富,适用于多语言、多平台的大规模分布式系统。

技术选型时需要权衡性能、可读性和可扩展性,并结合具体场景和需求。

 由于本文主要介绍protobuf,所以其他序列化协议暂且不深入讨论。

Protobuf:高效的序列化协议

Protobuf 是一款性能卓越的序列化协议,具备许多优秀协议应有的典型特性:

  1. 标准化的 IDL 和编译器

    • Protobuf 提供了完善的接口描述语言(IDL)和对应的编译器,使其在开发中对工程师十分友好。
  2. 紧凑的序列化数据

    • 与 XML 相比,Protobuf 序列化后的数据量仅为 XML 的 1/3 至 1/10,极大减少了存储和传输开销。
  3. 超快的解析速度

    • 在解析效率上,Protobuf 的性能比 XML 快 20 到 100 倍,非常适合对响应时间有严格要求的场景。
  4. 简单易用的动态库

    • Protobuf 提供了易用的动态库,反序列化操作甚至只需一行代码即可完成。

作为一个纯粹的展示层协议,Protobuf 可以灵活地与各种传输层协议(如 HTTP 或 TCP)搭配使用。同时,其文档完善,对开发者非常友好。然而,作为由 Google 推出的协议,目前 Protobuf 的语言支持较为有限,仅覆盖 Java、C++ 和 Python 三种语言。

典型应用场景与局限性

适用场景
  1. 高性能 RPC 调用

    • Protobuf 凭借其低空间开销和高解析速度,非常适合公司内部对性能要求较高的远程过程调用(RPC)。
  2. 跨公司通信

    • 得益于其标准化的 IDL 文件和与传输层协议的解耦,Protobuf 非常适用于跨公司场景。通过与 HTTP 协议结合,还可以实现良好的防火墙穿透能力。
  3. 应用层数据持久化

    • Protobuf 序列化后的数据体积小,适合用于持久化存储应用层对象的场景。
局限性
  • 有限的语言支持
    当前仅支持 Java、C++ 和 Python,限制了其在多语言生态中的应用。

  • 传输协议调试的复杂性
    由于 Protobuf 并未绑定特定的传输层协议,在跨公司通信中,传输协议的调试可能会带来额外的麻烦。

Protobuf 的 IDL 文件示例

以下是一个简单的 Protobuf IDL 文件,它定义了 UserInfo 和嵌套的 Address 数据结构:

message Product {
    required string product_id = 1;      // 商品唯一标识
    required string product_name = 2;   // 商品名称
    optional string description = 3;    // 商品描述
    optional float price = 4;           // 商品价格
}

message Order {
    required string order_id = 1;            // 订单唯一标识
    required string customer_name = 2;       // 客户姓名
    repeated Product product_list = 3;       // 订单中的商品列表
    optional string order_date = 4;          // 订单日期
}

  

使用 Protocol Buffers 的完整流程

Protocol Buffers(简称 Protobuf)是一种高效的序列化协议,用于定义数据结构并在多种语言和平台间进行数据传输。以下是从定义到使用 Protobuf 的完整流程:

1. 定义 .proto 文件

Proto 文件定义了需要存储或传输的数据结构及其字段。以下是一个示例:

syntax = "proto3";
package shopping;

message CartItem {
    string product_id = 1;   // 商品唯一标识
    int32 quantity = 2;      // 商品数量
    float price = 3;         // 单价
}

message User {
    string user_id = 1;          // 用户唯一标识
    string user_name = 2;        // 用户姓名
    repeated CartItem cart = 3;  // 购物车中商品列表
}

proto的基本数据类型如下图所示

2. 使用 protoc 编译 .proto 文件

运行 Protocol Buffers 编译器 (protoc) 将 .proto 文件编译为目标语言代码(如 C++ 或 Java)。

命令示例
protoc -I. --cpp_out=./generated user.proto
编译结果
  • 对于 C++,会生成以下文件:
    • test.pb.h:包含消息类型的头文件。
    • test.pb.cc:实现消息类型的方法。

3. 在项目中使用生成的代码

3.1 初始化消息对象并设置值

使用生成的类来操作定义好的消息类型:

#include "user.pb.h"
#include <iostream>
#include <string>

int main() {
    // 创建用户对象
    shopping::User user;
    user.set_user_id("u12345");
    user.set_user_name("Alice");

    // 添加购物车中的商品
    auto* item1 = user.add_cart();
    item1->set_product_id("p67890");
    item1->set_quantity(2);
    item1->set_price(15.99);

    auto* item2 = user.add_cart();
    item2->set_product_id("p12345");
    item2->set_quantity(1);
    item2->set_price(42.50);

    // 输出用户信息
    std::cout << "User ID: " << user.user_id() << std::endl;
    std::cout << "User Name: " << user.user_name() << std::endl;

    for (int i = 0; i < user.cart_size(); i++) {
        const auto& item = user.cart(i);
        std::cout << "Product ID: " << item.product_id()
                  << ", Quantity: " << item.quantity()
                  << ", Price: " << item.price() << std::endl;
    }

    return 0;
}
3.2 序列化消息

将消息对象序列化为二进制数据,以便存储或传输:

std::string serialized_data;
if (user.SerializeToString(&serialized_data)) {
    std::cout << "Serialization successful!" << std::endl;
} else {
    std::cerr << "Serialization failed!" << std::endl;
}

3.3 反序列化消息

从二进制数据解析消息对象:

shopping::User deserialized_user;
if (deserialized_user.ParseFromString(serialized_data)) {
    std::cout << "Deserialization successful!" << std::endl;

    // 输出反序列化后的数据
    std::cout << "User ID: " << deserialized_user.user_id() << std::endl;
    std::cout << "User Name: " << deserialized_user.user_name() << std::endl;

    for (int i = 0; i < deserialized_user.cart_size(); i++) {
        const auto& item = deserialized_user.cart(i);
        std::cout << "Product ID: " << item.product_id()
                  << ", Quantity: " << item.quantity()
                  << ", Price: " << item.price() << std::endl;
    }
} else {
    std::cerr << "Deserialization failed!" << std::endl;
}

4. 使用 CMake 集成

在 CMake 项目中,可以使用 protobuf_generate 指令生成代码。以下是一个示例配置:

find_package(Protobuf REQUIRED)

set(PROTO_FILES user.proto)

protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})

add_executable(shopping_app main.cpp ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(shopping_app PRIVATE protobuf::libprotobuf)
  • PROTO_FILES:定义需要编译的 .proto 文件列表。
  • protobuf_generate_cpp:生成 .pb.h.pb.cc 文件。
  • target_link_libraries:链接 Protobuf 的动态库。

5. Protobuf 常用序列化与反序列化 API

方法功能说明
SerializeToString(std::string*)将消息序列化为字符串(二进制内容)。
ParseFromString(const std::string&)从字符串反序列化为消息对象。
SerializeToArray(void*, int)将消息序列化到数组中。
ParseFromArray(const void*, int)从数组解析消息对象。
SerializeToOstream(std::ostream*)将消息写入输出流(如文件)。
ParseFromIstream(std::istream*)从输入流解析消息对象。

protobuf消息格式

1. Protobuf 编码基础

1.1 Varints 编码规则

        Varints 是一种高效的整数序列化方法,通过使用一个或多个字节表示整数,数值越小,占用的字节数越少。

编码规则:

  • 每个字节的最高位(msb)是标志位
    • 若为 1,表示后续还有字节。
    • 若为 0,表示这是最后一个字节。
  • 每个字节的低 7 位用于存储数值。
  • 使用小端字节序(低位字节在前)。

1.2 Varints 编码示例

以下是几个具体示例,帮助理解 Varints 编码过程:

  1. 数值 1 的编码

    • 二进制表示:0000 0001
    • 最高位为 0,表示只有一个字节。
    • 编码结果:0000 0001(十六进制为 0x01
  2. 数值 255 的编码

    • 二进制表示:1111 1111(需要两个字节)
    • 第一个字节:1111 1111(最高位为 1,表示有后续字节)
    • 第二个字节:0000 0001(最高位为 0,表示结束)
    • 编码结果:1111 1111 0000 0001(十六进制为 0xFF 0x01
  3. 数值 200 的编码

    • 二进制表示:1100 1000(需要两个字节)
    • 第一个字节:1100 1000(最高位为 1,表示有后续字节)
    • 第二个字节:0000 0001(最高位为 0,表示结束)
    • 编码结果:1100 1000 0000 0001(十六进制为 0xC8 0x01

2. 字段编号(Field Number)

        在 .proto 文件中,每个字段都被分配一个唯一的编号,用来标识字段。Protobuf 使用字段编号而不是字段名称进行传输,这显著降低了传输数据的大小。

3. 传输类型(Wire Type)

每个字段都有一个对应的传输类型,用于指示数据的编码方式。常见的传输类型包括:

4. Field 结构

        message由一个个字段组成,一个字段的完整的二进制描述即<<编号,传输类型>,值>通常称为一个field,如下图。

  • 具体而言每个field的构成为Tag-[Length]-Value;这里的[Length]是否需要是依据Tag最后三位的wire_type来决定的。

  • Tag:由字段编号和传输类型组成。
    • 编码规则:Tag = (field_num << 3) | wire_type
    • Tag 也是 Varints 编码,三位低位表示传输类型,其他位表示字段编号。
  • Length:可选项,仅当字段类型为 Length-delimited 时需要。
  • Value:字段值,根据字段类型采用不同的编码方式。

5. Protobuf 编解码的关键点

  • 在消息流中每个Tag(key/键)都是varint,编码方式为:field_num << 3 | wire_type。即,Tag(key/键)由 .proto文件中字段的编号(field_num) 和 传输类型(wire_type)两部分组成。 注:Tag也是Varints编码,其后三位是传输类型(wire_type),之前的数值为是字段编号(field_num)。 使用Varint编码方式几千/几万的字段序号(field_num)都是可以被表示的。

  • 在对一条消息(message)进行编码的时候是把该消息中所有的key-value对序列化成二进制字节流;key和value分别采用不同的编码方式。

  • 消息的二进制格式只使用消息字段的字段编号(field_num)作为Tag(key/键)的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。

  • 解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个key-value对;如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。

示例解析

示例 1:简单整数编码

定义一个字段:

int32 age = 1;
  • 字段编号1
  • 传输类型Varint(类型为 0)
  • Tag 编码
    • Tag = (1 << 3) | 0 = 8(十进制)
    • Tag 的 Varints 编码为:0000 1000(十六进制 0x08
    • 假设值为 25,Varints 编码为:0001 1001(十六进制 0x19
  • 完整消息
    • 二进制格式:0x08 0x19

示例 2:嵌套消息编码

定义一个嵌套的 Protobuf 消息:

message User {
    int32 id = 1;
    string name = 2;
}

message Group {
    int32 group_id = 1;
    repeated User members = 2;
}

编码过程:

  1. Group 消息的字段编号与类型
    • group_id1,类型为 Varint
    • members2,类型为 Length-delimited
  2. 嵌套消息编码
    • 每个 User 消息会被编码成 Length-delimited 类型,先序列化 User 的字段,然后将其作为 members 的值。

完整编码输出将是多个 Tag-Value 对的组合。

Protobuf 的负数编码

对于负数,Protobuf 不直接使用 Varints,而是采用 ZigZag 编码 优化:

  • ZigZag 编码将有符号整数映射为无符号整数,使得负数占用的字节数与正数一致。
  • 编码规则:(n << 1) ^ (n >> 31)(假设整数为 32 位)

示例:

  • -1 的 ZigZag 编码为:1
  • -2 的 ZigZag 编码为:3

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

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

相关文章

83,【7】BUUCTF WEB [MRCTF2020]你传你[特殊字符]呢

进入靶场 图片上这个人和另一道题上的人长得好像 54&#xff0c;【4】BUUCTF WEB GYCTF2020Ezsqli-CSDN博客 让我们上传文件 桌面有啥传啥 /var/www/html/upload/344434f245b7ac3a4fae0a6342d1f94a/123.php.jpg 成功后我就去用蚁剑连了&#xff0c;连不上 看了别的wp知需要…

低代码系统-产品架构案例介绍、轻流(九)

轻流低代码产品定位为零代码产品&#xff0c;试图通过搭建来降低企业成本&#xff0c;提升业务上线效率。 依旧是从下至上&#xff0c;从左至右的顺序 名词概述运维层底层系统运维层&#xff0c;例如上线、部署等基础服务体系内置的系统能力&#xff0c;发消息、组织和权限是必…

Linux——网络(udp)

文章目录 目录 文章目录 前言 一、upd函数及接口介绍 1. 创建套接字 - socket 函数 2. 绑定地址和端口 - bind 函数 3. 发送数据 - sendto 函数 4. 接收数据 - recvfrom 函数 5. 关闭套接字 - close 函数 二、代码示例 1.服务端 2.客户端 总结 前言 Linux——网络基础&#xf…

Nxopen 直齿轮参数化设计

NXUG1953 Visualstudio 2019 参考论文&#xff1a; A Method for Determining the AGMA Tooth Form Factor from Equations for the Generated Tooth Root Fillet //FullGear// Mandatory UF Includes #include <uf.h> #include <uf_object_types.h>// Internal I…

初阶数据结构:链表(二)

目录 一、前言 二、带头双向循环链表 1.带头双向循环链表的结构 &#xff08;1)什么是带头&#xff1f; (2)什么是双向呢&#xff1f; &#xff08;3&#xff09;那什么是循环呢&#xff1f; 2.带头双向循环链表的实现 &#xff08;1&#xff09;节点结构 &#xff08;2…

使用openwrt搭建ipsec隧道

背景&#xff1a;最近同事遇到了个ipsec问题&#xff0c;做的ipsec特性&#xff0c;ftp下载ipv6性能只有100kb, 正面定位该问题也蛮久了&#xff0c;项目没有用openwrt, 不过用了开源组件strongswan, 加密算法这些也是内核自带的&#xff0c;想着开源的不太可能有问题&#xff…

网络安全 | F5-Attack Signatures详解

关注&#xff1a;CodingTechWork 关于攻击签名 攻击签名是用于识别 Web 应用程序及其组件上攻击或攻击类型的规则或模式。安全策略将攻击签名中的模式与请求和响应的内容进行比较&#xff0c;以查找潜在的攻击。有些签名旨在保护特定的操作系统、Web 服务器、数据库、框架或应…

MATLAB绘图时线段颜色、数据点形状与颜色等设置,介绍

MATLAB在绘图时&#xff0c;设置线段颜色和数据点的形状与颜色是提高图形可读性与美观性的重要手段。本文将详细介绍如何在 MATLAB 中设置这些属性。 文章目录 线段颜色设置单字母颜色表示法RGB 值表示法 数据点的形状与颜色设置设置数据点颜色和形状示例代码 运行结果小结 线段…

论文速读|Matrix-SSL:Matrix Information Theory for Self-Supervised Learning.ICML24

论文地址&#xff1a;Matrix Information Theory for Self-Supervised Learning 代码地址&#xff1a;https://github.com/yifanzhang-pro/matrix-ssl bib引用&#xff1a; article{zhang2023matrix,title{Matrix Information Theory for Self-Supervised Learning},author{Zh…

FPGA工程师成长四阶段

朋友&#xff0c;你有入行三年、五年、十年的职业规划吗&#xff1f;你知道你所做的岗位未来该如何成长吗&#xff1f; FPGA行业的发展近几年是蓬勃发展&#xff0c;有越来越多的人才想要或已经踏进了FPGA行业的大门。很多同学在入行FPGA之前&#xff0c;都会抱着满腹对职业发…

计算机组成原理(2)王道学习笔记

数据的表示和运算 提问&#xff1a;1.数据如何在计算机中表示&#xff1f; 2.运算器如何实现数据的算术、逻辑运算&#xff1f; 十进制计数法 古印度人发明了阿拉伯数字&#xff1a;0&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6&#…

简化配置与动态表达式的 Spring EL

1 引言 在现代软件开发中,配置管理和动态逻辑处理是构建灵活、可维护应用程序的关键。Spring 框架以其强大的依赖注入和面向切面编程功能而闻名,而 Spring Expression Language (Spring EL) 则为开发者提供了一种简洁且强大的方式来简化配置并实现动态表达式。 1.1 Spring …

python实现http文件服务器访问下载

//1.py import http.server import socketserver import os import threading import sys# 获取当前脚本所在的目录 DIRECTORY os.path.dirname(os.path.abspath(__file__))# 设置服务器的端口 PORT 8000# 自定义Handler&#xff0c;将根目录设置为脚本所在目录 class MyHTT…

在php中怎么打开OpenSSL

&#xff08;点击即可进入聊天助手&#xff09; 背景 在使用php做一些项目时,有用到用户邮箱注册等,需要开启openssl的能力 在php系统中openssl默认是关闭状态的,在一些低版本php系统中,有的甚至需要在服务器终端后台,手动安装 要打开OpenSSL扩展&#xff0c;需要进行以下步骤 …

二次封装的方法

二次封装 我们开发中经常需要封装一些第三方组件&#xff0c;那么父组件应该怎么传值&#xff0c;怎么调用封装好的组件原有的属性、插槽、方法&#xff0c;一个个调用虽然可行&#xff0c;但十分麻烦&#xff0c;我们一起来看更简便的方法。 二次封装组件&#xff0c;属性怎…

基于Springboot用axiospost请求接收字符串参数为null的解决方案

问题 ​ 今天在用前端 post 请求后端时发现&#xff0c;由于是以 Json对象的形式传输的&#xff0c;后端用两个字符串形参无法获取到对应的参数值 前端代码如下&#xff1a; axios.post(http://localhost:8083/test/postParams,{a: 1, b:2} ,{Content-Type: application/jso…

STM32 OLED屏配置

1.OLED简介 OLED&#xff08;Organic Light Emitting Diode&#xff09;&#xff1a;有机发光二极管 OLED显示屏&#xff1a;性能优异的新型显示屏&#xff0c;具有功耗低、相应速度快、宽视角、轻薄柔韧等特点 0.96寸OLED模块&#xff1a;小巧玲珑、占用接口少、简单易用&a…

DiffuEraser: 一种基于扩散模型的视频修复技术

视频修复算法结合了基于流的像素传播与基于Transformer的生成方法&#xff0c;利用光流信息和相邻帧的信息来恢复纹理和对象&#xff0c;同时通过视觉Transformer完成被遮挡区域的修复。然而&#xff0c;这些方法在处理大范围遮挡时常常会遇到模糊和时序不一致的问题&#xff0…

STM32完全学习——RT-thread在STM32F407上移植

一、写在前面 关于源码的下载&#xff0c;以及在KEIL工程里面添加操作系统的源代码&#xff0c;这里就不再赘述了。需要注意的是RT-thread默认里面是会使用串口的&#xff0c;因此需要额外的进行串口的初始化&#xff0c;有些人可能会问&#xff0c;为什么不直接使用CubMAX直接…

QT TLS initialization failed

qt使用QNetworkAccessManager下载文件&#xff08;给出的链接可以在浏览器里面下载文件&#xff09;&#xff0c;下载失败&#xff0c; 提示“TLS initialization failed”通常是由于Qt在使用HTTPS进行文件下载时&#xff0c;未能正确初始化TLS&#xff08;安全传输层协议&…