冰冰学习笔记:简单了解protobuf

news2024/11/25 15:52:35

欢迎各位大佬光临本文章!!!

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.comhttps://gitee.com/BingbingSuperEffort


目录

前言

1.初识protobuf

1.1protobuf简介

1.2快速使用protobuf

2.protobuf的语法介绍 

2.1 字段规则与消息类型的定义和使用

2.2 protoc命令选项

2.3 enum类型

2.4 Any类型

2.5 oneof类型

2.6 map类型

2.7 默认值

3.消息更新

3.1 更新规则

3.2 删除规则

3.3 未知字段       

4.option选项

5.实战使用:网络通信录


前言

        序列化与反序列化是我们在通讯传输和文件保存时常用的手段。尤其是在网络传输协议中,字符串与各种格式之间的转换也需要这种手段。protobuf,json,xml都是常用的集成库,json我们已经了解过了,而protobuf作为Google公司的产品,其具备的优点更是数不胜数。下面我们一起了解一下protobuf。

1.初识protobuf

1.1protobuf简介

        protobuf是Google公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可以用于序列化与反序列化。他与语言无关,平台无关,可用于即时通讯、数据存储等领域。相比于xml和json,protobuf更加轻便,体量更小,解析速度更快。

        protobuf最常用的就是序列化与反序列化操作,那什么是序列化呢?序列化就是将对象转换为字节序列的过程,反序列化就是把字节序列恢复为对象的过程。在网络通信中,我们往往需要将传输的报文进行序列化操作,然后在发送给远端机器,远端接收到后,在通过反序列化操作将内容进行解析,这才真正得到发送的内容。

        protobuf作为一种轻量化的序列化工具,他的扩展性以及兼容性更加灵活,我们可以更新数据结构而无需担心影响和破坏原有的旧程序。protobuf最重要的特点就是需要依赖通过编译生成的头文件和源文件来使用。

1.2快速使用protobuf

        首先在使用protobuf之前我们需要先安装protobuf,大家可自行搜索查找安装流程,这里不再赘述。

        我们需要创建一个后缀为.proto的文件,在该文件中进行protobuf的编写逻辑。文件命名应该使用全小写字母命名,多个字母之间使用下划线 “_” 进行连接。例如first_pro.proto。

(1)指定语法

        进入文件后,我们需要指定protobuf的语法,我们常用的语法为proto3。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。因此我们需要在首行添加如下代码:

syntax = "proto3";

  (2)package声明符

        package 是⼀个可选的声明符,能表示 .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为 了避免我们定义的消息出现冲突。在编译完成后,相当于C++中的命名空间。

 (3)定义消息及消息字段

         消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。实际上就是编译生成后的class。.proto文件的定义消息的格式如下:

syntax = "proto3";
package conntacts;

//定义信息
message PeopleInfo
{
    string name = 1;//姓名,=1 表示字段编号
    int32 age = 2;//年龄
}

        命名规范则采用驼峰法命名,首字母大写。

        name和age是我们在message中定义的属性字段,字段定义格式为:字段类型  字段名称 = 字段唯一编号;

        字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。

        字段类型分为:标量数据类型和特殊类型(包括枚举、其他消息类型等)。

        字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。

        实际上,属性字段就对应的C++语言的成员变量,只不过后面不再是定义的默认值,而是定义的编号,用来在生成的.h文件中进行标识这些成员。

标量数据类型与C++类型对应表:

.protoc TypeNotesC++ Type
doubledouble
floatfloat
int32使用变长编码[1]。负数的编码效率较低⸺若字段可能为负值,应使用 sint32 代替int32
int64使用变长编码[1]。负数的编码效率较低⸺若字段可 能为负值,应使用 sint64 代替int64
uint32使用变⻓编码[1]。uint32
uint64使用变长编码[1]。uint64
sint32使用变长编码[1]。符号整型。负值的编码效率高于常规的 int32 类型int32
sint64使用变长编码[1]。符号整型。负值的编码效率高于 常规的 int64 类型。int64
fixed32定长4 字节。若值常大于2^28 则会⽐ uint32 更高 效。uint32
fixed64定长8 字节。若值常大于2^56 则会⽐ uint64 更高效uint64
sfixed32定长4 字节int32
sfixed64定长8 字节。int64
boolbool
string包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过 2^32string
bytes可包含任意的字节序列但⻓度不能超过 2^32。string

(4)编译命令

protoc -I path --cpp_out=DST_DIR path/to/file.proto

1.protoc 为编译命令

2.-I 指定被编译的.proto文件所在目录,可多次指定,当前指定为path

3.--cpp_out:表示编译生成C++文件

4.= 后面加生成文件的路径

        执行编译命令后,会在指定的文件目录中出现.h和.cc的C++文件,而我们在.proto文件中定义的message将会生成对应的类,而类中的操作方法则定义在生成的.h文件和.cc文件中。

        例如上文中定义的message,在生成的.h文件中就有如下方法:

        而对于序列化和反序列化的代码,则位于MessageLite类中,该类为message的父类。

class MessageLite {
public:
 //序列化:
 bool SerializeToOstream(ostream* output) const; // 将序列化后数据写⼊⽂件流
 bool SerializeToArray(void *data, int size) const;
 bool SerializeToString(string* output) const;
 
 //反序列化:
 bool ParseFromIstream(istream* input); // 从流中读取数据,再进⾏反序列化动作
 bool ParseFromArray(const void* data, int size);
 bool ParseFromString(const string& data);
};

        序列化的结果为二进制数据,并非文本数据。 并且序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容,而是将序列化的结果保存到函数入参指定的地址中。

(5)序列化与反序列化的使用

        下面的代码我们使用先前创建的message并且创建一个对象设置对应的信息,然后调用SerializeToString函数将其序列化为字符串。随后在将字符串内容调用ParseFromString函数反序列化出来。

#include<iostream>
#include"contacts.pb.h"
#include<string>
using namespace std;
int main()
{
    string peostr;
    {
        conntacts::PeopleInfo people;
        people.set_name("大兵");
        people.set_age(24);
        if(!people.SerializeToString(&peostr))
        {
            cout<<"序列化失败"<<endl;
            return 1;
        }
        cout<<"序列化成功"<<endl;
        cout<<"序列化结果:"<<peostr<<endl;
    }
    {
        conntacts::PeopleInfo people;
        if(!people.ParseFromString(peostr))
        {
            cout<<"反序列化失败"<<endl;
            return 1;
        }
        cout<<"反序列化成功"<<endl;
        cout<<"反序列化结果:"<<endl
            <<"name:"<<people.name()<<endl
            <<"age:"<<people.age()<<endl;
    }
    return 0;
}

(6)编译链接库

        正如C++引入其他库一样,在使用.proto文件生成的.h里面的函数时,我们也需要连接protobuf提供的第三方库protobuf。这里要注意,在连接protobuf库时,一定要增加-std=c++11,因为protobuf中使用了部分c++11的语法。

g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf

总结:protobuf的使用流程

  • 编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容
  • 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中
  • 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏ 设置和获取,和对 message 对象进⾏序列化和反序列化。

2.protobuf的语法介绍 

2.1字段规则与消息类型的定义和使用

(1)singular:消息中可以包含该字段零次或一次(不超过一次)。 Proto3语法中,字段默认使用规则

(2)repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数

        注:Proto3语法支持嵌套定义message,支持多个message定义在同一个文件,不同message中的字段编号并不冲突。

当我们想引入其他定义的.proto文件时,需要使用import引入。

import  “phone.proto”    //引入其他的 .proto文件

syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";//引入其他文件

message PeopleInfo
{
    string name = 1; //姓名,=1 表示字段编号
    int32 age = 2;   //年龄
    message Phone
    {
        string number=1;
    }
    repeated Phone phione=3;//电话  实际为一个数组
   
}

2.2 protoc命令选项

protoc -h :查看所有选项

protoc –decode=contacts2.Contacts contacts.proto < contacts.bin

        查看二进制文件的内容,将其转为我们认识的字符

        contacts2.Contacts:contacs2命名空间下的Contacts结构体的输出内容

        contacts.proto:该结构体存储在contacts.proto文件中

        contacts.bin:输出的二进制内容的文件

2.3 enum类型

        枚举类型,使⽤驼峰命名法,⾸字⺟⼤写。里面的常量值为全大写多个字⺟之间⽤ _ 连接。

enum PhoneType {
 MP = 0; // 移动电话
 TEL = 1; // 固定电话
}

定义特点:

  1. (1)0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认值,且值为 0
  2. (2)枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)
  3. (3)枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。
  4. (4)将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto 文件下测试时,编译后会报错
  5. (5)同级(同层)的枚举类型,各个枚举类型中的常量不能重名
  6. (6)单个 .proto ⽂件下,最外层枚举类型和嵌套枚举类型,不算同级
  7. (7)多个 .proto ⽂件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级
  8. (8)多个 .proto ⽂件下,若⼀个文件引入了其他文件,且每个文件都声明了 package,不算同级。

2.4 Any类型

        字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也可用 repeated 来修饰。

        Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .proto 文件。

        引入:import "google/protobuf/any.proto";  使用

  1. 使用PackFrom() 方法可以将任意消息类型转为 Any 类型
  2. 使用 UnpackTo() 方法可以将 Any 类型转回之前设置的任意消息类型
  3. 使用Is()方法可以用来判断存放的消息类型是否为typename T
message Address{
    string home_address =1;
    string unit_address =2;
}

message PeopleInfo
{
    string name = 1; //姓名,=1 表示字段编号
    int32 age = 2;   //年龄
    message Phone
    {
        string number=1;
        enum PhoneType
        {
            MP=0;//移动电话
            TEL=1;//固定电话
        }
        PhoneType type =2;
    }
    repeated Phone phione=3;//电话
    google.protobuf.Any data = 4;//添加any类型
}

        在输入处进行any类型的绑定

        contacts2::Address address;//定义Address类
        cout<<"请输入联系人家庭地址:";
        string homeadd;
        getline(cin,homeadd);
        address.set_home_address(homeadd);
        cout<<"请输入联系人单位地址:";
        string unitadd;
        getline(cin,unitadd);
        address.set_unit_address(unitadd);
        google::protobuf::Any* data = pcont->mutable_data();
        data->PackFrom(address);//将Any类型绑定为Address类型

        在读取处进行数据读取

        if(people.has_data()&&people.data().Is<contacts2::Address>())
        {
            contacts2::Address addr;
            people.data().UnpackTo(&addr);
            if(!addr.home_address().empty())
            {
                cout<<"家庭地址:"<<addr.home_address()<<endl;
            }
            if(!addr.unit_address().empty())
            {
                cout<<"单位地址:"<<addr.unit_address()<<endl;
            }
        }

        has_data()方法为检测是否绑定了data类型,Is<Type>()方法为检测绑定的类型是否为Type。

2.5 oneof类型

        如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用 oneof 加强这个行为,也能有节约内存的效果。

        我们不能使用repeated,只会保留最后设置的内容,并且字段编号不能与其他字段重复。

//.proto文件中message新增oneof修饰的字段
    oneof other_contact{
     string qq=5;
     string wechat=6;
     
    }

//写入:
        cout<<"请选择联系方式:1.qq 2.wechat";
        int other_contact;
        cin>>other_contact;
        cin.ignore(256,'\n');
        if(1==other_contact)
        {
            cout<<"请输入qq: ";
            string qq;
            getline(cin,qq);
            pcont->set_qq(qq);
        }
        else if(2==other_contact)
        {
             cout<<"请输入wechat: ";
            string wechat;
            getline(cin,wechat);
            pcont->set_wechat(wechat);
        }
        else{
            cout<<"选择错误"<<endl;
        }

//读取:
        switch (people.other_contact_case())
        {
        case contacts2::PeopleInfo::OtherContactCase::kQq:
            cout<<"qq号码:"<<people.qq()<<endl;
            break;
        case contacts2::PeopleInfo::OtherContactCase::kWechat:
            cout<<"wechat号码:"<<people.wechat()<<endl;
            break;
        
        default:
            break;
        }

2.6 map类型

语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:

map<key_type, value_type> map_field = N

  1. key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型
  2. map 字段不可以用 repeated 修饰
  3. map 中存入的元素是无序的
//.proto文件中新增map
map<string,string> remake = 7;

//写入map类型的备注
for(int i=1;;i++)
{
   cout<<"请输入备注"<<i<<"标签:";
   string remark_key;
   getline(cin,remark_key);
   if(remark_key.empty())
   {
      break;
   }
   cout<<"请输入备注"<<i<<"内容:";
   string remark_value;
   getline(cin,remark_value);
   pcont->mutable_remake()->insert({remark_key,remark_value});
}

//读取类型为map的备注信息
if(!people.remake().empty())
{
   cout<<"备注信息:"<<endl;
}
for(auto it = people.remake().cbegin();it!=people.remake().cend();it++)
{
   cout<<"   "<<it->first<<": "<<it->second<<endl;
}

 

2.7 默认值

        反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,当我们反序列化对象中相应字段时,就会设置为该字段的默认值。

        对于标量数据类型,proto3语法下没有生成has_方法。

        对于消息字段、oneof字段和any字段 ,C++ 和 Java 语言中都有 has_ 方法来检测当前字段是否被设置。

        各类型的默认值设置:

类型默认值
字符串空字符串
字节空字节
布尔值false
数值类型0
枚举第一个被定义的默认值,必须为0
消息字段依赖于语言取值
repeated修饰后

3.消息更新

3.1 更新规则

        如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。

        我们只需要保证更新的字段的名称和编号不要和老字段冲突即可。

        对于修改字段则具备以下规则:

  1. 禁止修改任何已有字段的字段编号。
  2. int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的一个改为另一个, 而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++ 一致的处理方案 (例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)。
  3. sint32 和 sint64 相互兼容但不与其他的整型兼容。
  4. string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
  5. bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
  6. fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。

  7. enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。

  8. oneof: 将一个单独的值更改为新oneof 类型成员之一是安全和二进制兼容的。 若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。 将任何字段移入已存在的 oneof 类型是不安全的。

3.2 删除规则

         对于删除字段,则具备下列规则:

        不能直接删除字段。若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号 (reserved),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。

       reserved关键字,保留字段编号。使用reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。

        reserved可以指定编号也可以指定名称。

reserved 2,10,100 to 200; //保留2,10,100到200的编号

reserved “age”;//保留“age”字段

3.3 未知字段       

        未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。

        本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。

        例如,我们定义了一个client和一个server两个通信端,并且原本约定的字段类型为名字和年龄,但是后面我们将server端的年龄字段保留,并且新增了生日字段,而client并没有更改,此时我们按照之前的通信方式,client接收到的年龄字段将会是默认值,而生日字段将是未知字段。

        如果我们想打印出来未知字段,就需要使用如下代码:

void PrintCon(c_contacts::PeopleContacts& con)
{
    for(int i=0;i<con.cont_size();i++)
    {
        cout<<"-------------联系人"<<i+1<<"--------------------"<<endl;
        const c_contacts::PeopleInfo& people = con.cont(i);
        cout<<"姓名:"<<people.name()<<endl;
        cout<<"年龄:"<<people.age()<<endl;
        for(int j=0;j<people.phione_size();j++)
        {
            const c_contacts::PeopleInfo_Phone& phone = people.phione(j);
            cout<<"电话"<<j+1<<":"<<phone.number();
            cout<<"  ("<<phone.PhoneType_Name(phone.type())<<")"<<endl;
        }
//获取未知字段
       const Reflection* reflection = PeopleInfo::GetReflection();
       const UnknownFieldSet& unknowSet = reflection->GetUnknownFields(people);

       for(int j=0;j<unknowSet.field_count();j++)
       {
        const UnknownField& fild = unknowSet.field(j);
        cout<<"未知字段"<<j+1<<":"<<"字段编号:"<<fild.number()<<"类型:"<<fild.type();
        switch(fild.type())
        {
            case UnknownField::Type::TYPE_VARINT:
                cout<<" 值:"<<fild.varint()<<endl;
                break;
            case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                cout<<" 值:"<<fild.length_delimited()<<endl;
                break;
        }
       }
    }
}

4.option选项

        .proto 文件中可以声明许多选项,使用option 标注。选项能影响 proto 编译器的某些处理方式。

        选项的完整列表在google/protobuf/descriptor.proto中定义。选项分为文件级、消息级、字段级等等, 但并没有一种选项能作用于所有的类型。

        optimize_for : 该选项为⽂件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、 CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto ⽂件后生成的代码内容不同。

        SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码 编译后会占⽤更多的空间。 SPEED 是默认选项。

         CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来 实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。

         LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常 少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化功能, 所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源 有限的平台,例如移动⼿机平台中。

  option optimize_for = LITE_RUNTIME;

        allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。 举个例⼦:

enum PhoneType {
 option allow_alias = true;
 MP = 0;
 TEL = 1;
 LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}

5.实战使用:网络通信录

代码仓库:https://gitee.com/BingbingSuperEffort/protobuf

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

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

相关文章

Fiddler(Statistics、Inspectors)详解

一、Fiddler Statistics详解 Fiddler的 Statistics 分页会统计请求和响应的一些信息。可以使用它完成简单的性能测试&#xff0c;查看其接口的响应时间。 如果你想学习Fiddler抓包工具&#xff0c;我这边给你推荐一套视频&#xff0c;这个视频可以说是B站播放全网第一的Fiddle…

Java 网络编程 —— RMI 框架

概述 RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架&#xff0c;采用客户/服务器通信方式&#xff0c;在服务器上部署了提供各种服务的远程对象&#xff0c;客户端请求访问服务器上远程对象的方法&#xff0c;它要求客户端与服务器端都是 Java 程序 RMI 框架采用代…

提升安全性与合规性的关键工具ADAudit Plus

在当今数字化时代&#xff0c;企业对于安全性和合规性的要求越来越高。特别是在Active Directory&#xff08;AD&#xff09;域中&#xff0c;作为组织的核心身份验证和访问管理系统&#xff0c;审计活动的重要性变得前所未有。为了满足这一需求&#xff0c;ADAudit Plus成为了…

TwinCAT3 安装和打开项目问题记录

安装的VS版本: cn_visual_studio_professional_2015_with_update_3_x86_x64_dvd_8923256.iso 安装的TwinCAT版本: TC31-FULL-Setup.3.1.4024.29 更多的TwinCAT版本我都放在了百度云盘,有需求可以留言 4022.22 链接:https://pan.baidu.com/s/1D505FdgL7l1DAUANKb-VLg 提取…

【C++】入门基础知识详解(一)

目录 一、C关键字 二、命名空间 1、命名空间的定义 2、命名空间的使用 三、C输入&&输出 四、缺省参数 1、缺省参数的概念 2、缺省参数的分类 2.1 全缺省参数 2.2 半缺省参数 一、C关键字 我们在学习C之前&#xff0c;我相信大家大多数都对C语言多多少少都有所了解…

JMeter之__threadNum妙用:将接口查询结果列表按顺序赋值给各线程

使用JMeter做性能测试会遇到这么一个场景&#xff1a;后面的请求需要根据前面的查询列表结果通过正则表达式提取器取值后赋值&#xff0c;而后面用户的赋值必须是唯一的&#xff0c;此时该如何做&#xff1f; 如果按编程思维来说&#xff0c;这个问题并不难。只需要把前面的结…

Azkaban搭建与使用

下载最新azkaban源文件&#xff1a;https://github.com/azkaban/azkaban/releases 集群模式安装 1.解压 azkaban-db-3.84.4.tar.gz、 azkaban-exec-server-3.84.4.tar.gz 和 azkaban-web-server-3.84.4.tar.gz 到/opt/install/azkaban 目录下 linux>tar -zxvf azkaban-db-3…

算法:静态查找表

查找表&#xff08;Search table&#xff09;是由同一类型的数据元素&#xff08;或记录&#xff09;构成的集合。关键字(key)是数据元素中某个数据项的值&#xff0c;又称为键值&#xff0c;用它可以表示一个数据元素&#xff0c;也可以标识一个记录的数据项&#xff08;字段&…

当深度学习撞上高性能计算,科研仿佛坐上了加速器

今天深度学习无处不在&#xff0c;当你打开移动终端的时候&#xff0c;各种APP会推荐到你喜欢的食物、你喜欢的电影&#xff0c;你关注的新闻热点。在生活中更是改变着我们&#xff0c;今天的智能语音让语言障碍破除&#xff0c;在预测疾病基因大数据领域预测疾病来确定药物治疗…

2023年大学生就业怎么样?双一流高校就业率仅15%,到底是咋了?

2023年&#xff0c;大学毕业生就业状况如何&#xff0c;一直是社会关注的焦点。尤其是中国的双一流高校&#xff0c;以其优越的教学与研究背景和实力&#xff0c;被众多年轻人视为就业的理想选择。 然而&#xff0c;在最新的统计数据中&#xff0c;这些一流高校的就业率却惊人…

欧科云链OKLink全新推出Onchain AML服务 助力新金融合规健康发展

据香港大公报报道&#xff0c;为期两天的全球高端经济峰会2023格林威治经济论坛(GreenwichEconomicForum&#xff0c;下称GEF论坛)于6月15日在香港交易所举办&#xff0c;欧科云链控股有限公司&#xff08;下称“欧科云链”&#xff0c;股票代码&#xff1a;01499.HK&#xff0…

【北邮国院大三下】Logistics and Supply Chain Management 物流与供应链管理 Week4

北邮国院大三电商在读&#xff0c;随课程进行整理知识点。仅整理PPT中相对重要的知识点&#xff0c;内容驳杂并不做期末突击复习用。个人认为相对不重要的细小的知识点不列在其中。如有错误请指出。转载请注明出处&#xff0c;祝您学习愉快。 如需要pdf格式的文件请私信联系或…

VALSE 2023 无锡线下参会个人总结 6月12日-3

VALSE2023 无锡线下参会个人总结 6月12日-3 6月12日会议日程安排Workshop&#xff1a;多模态大模型与提示学习左旺孟&#xff1a;预训练模型和语言增强的零样本视觉学习余宙&#xff1a;知识增强的多模态预训练和提示学习王云鹤&#xff1a;多模态交织&#xff1a;高效模型架构…

STM32中断设置以及中断优先级设置-不含代码例程

本项目使用到的是STM32F030C8型号的MCU&#xff0c;我们可以从官方下载到的标准库文件中的启动汇编文件中&#xff0c;查看到本型号单片机的外部中断向量表。&#xff08;如下图所示&#xff09; 首先&#xff0c;我们了解一下NVIC是什么&#xff0c;在core_cm0.h文件中的标准库…

海川润泽“巧克力”DTU强势来袭

1、设备介绍 “巧克力”DTU(型号&#xff1a;HCRZ-DTU200)&#xff0c;它可以实现远程通信、安全传输等功能&#xff0c;提高了传输的效率和可靠性。采用全新工业级设计&#xff0c;灵活应用于不同场景。此款DTU体积小(76mm31mm21.8mm)、重量轻(150g)&#xff0c;安装简便&…

「任务派发」上线,高效管理跨平台营销任务

作为企业矩阵号相关业务的负责人&#xff0c;您在工作中是否遇到&#xff1a; 下达一个营销任务&#xff0c;需要在几十上百个群里沟通对接&#xff1f;任务发布后&#xff0c;无法掌握各运营者执行情况&#xff1f;账号数据难回收&#xff0c;无法检验任务完成效果&#xff1…

阿里云携手开放原子开源基金会倡议发起云原生工作委员会,两大开源项目达成捐赠意向

6月11日&#xff0c;2023开放原子全球开源峰会正式拉开帷幕。本届峰会以“开源赋能&#xff0c;普惠未来”为主题&#xff0c;设置了开幕式暨高峰论坛、20余场分论坛和开源成果及重点项目展览。在峰会主论坛&#xff0c;开放原子开源基金会理事长孙文龙协各家单位共同倡议发起云…

抖音加码自营电商,拿什么做出差异化?

今年618期间&#xff0c;抖音电商可谓是花尽了心思。不仅推出了补贴活动&#xff0c;上架了单品超值购、商城频道主题日、搜索彩蛋等玩法&#xff0c;而且在售后方面&#xff0c;还推出了“安心购”服务&#xff0c;可提供“七天无理由退货”“极速退款”“运费险”“过敏包退”…

关于QTreeView使用自定义Tree Model时,插入子项目时,扩展箭头不显示的问题

在使用自定义的TreeModel显示数据时&#xff0c;添加子项目后&#xff0c;扩展箭头不显示&#xff0c;但是点击原来扩展箭头在的地方时&#xff0c;树仍能够展开添加的子项目。 经过研究发现&#xff0c;QTreeView有以下代码&#xff1a; 说明如果调用的beginInserRows中的par…

Apikit 自学日记: 如何创建/生成 API 文档

在 API 研发管理产品中&#xff0c;几乎所有的协作工作都是围绕着 API 文档进行的。 我们在接触了大量的客户后发现&#xff0c;采用 文档驱动 的协作模式会比先开发、后维护文档的方式更好&#xff0c;团队协作效率和产品质量都能得到提高。因此我们建议您尝试基于文档来进行工…