由于项目可能会需要 proto3 的某些特性,但是当前项目中使用的是 proto2,因此我这次开始了 proto2 升级 proto3 的历程。标题是一次尝试,那是十有八九我是失败了 (╥╯^╰╥),但通过这次失败还是总结出了一丢丢结论,故书此篇。

(文章中的 proto2 版本为 2.6.1,proto3 的版本为 3.15.6。)

# 1.proto2 与 proto3 的主要区别

# I. 移除了 default 和 required 选项

proto3 中不再能指定 default 值,未赋值则使用默认的 “0” 值,并去除了 required 关键字。

这一定程度上影响了 proto 协议的灵活性,项目中还是有一些字段在使用非 0 的 default 值后可以减少代码量,使代码简洁明了。

但是这提高了 proto 的兼容性和稳定性,项目中不同版本的 proto 可能会改变 default 值,假如默认值不是 0,使用 proto 通信的两端版本不一致,很有可能会产生一些难以定位的问题。

# II. 字段不指定修饰词默认为 singular

proto3 中,可以不指定修饰词,这样的字段默认为 singular。singular 字段的好处在于,在设置为默认值时,在序列化后,该字段是不会占用空间的,这对于性能的提升还是有很大帮助的,此次尝试也主要是想使用这个特性。(网上挺多说法是 optional 修饰的字段也有这个特性,但我实际使用下来 optional 的字段设置为默认值以后还是会占用空间的,也许是版本更新过了)

不过 singular 的字段会导致取到默认值后,无法判断该字段是没有赋值还是赋过了 0 值,这两者在实际项目中有时是需要区别的。这种情况可以使用 optional 字段,或是使用一个 bool 值去标记是否赋值。

# III.proto3 的 repeated 字段默认 packed=true

对于没有指定 packed=true 的字段,处理数据时会保存 n 条 key-value 结构;而 packed=true 的字段在处理数据时,会将 int 类型的数据打成一个包,存储在一段连续的空间中,不保存键值,因此一定程度上能提高性能。

# IV. 其他

除此之外,proto3 还加入了 Any,OneOf 等新特性,移除了 groups 等语法,不过这些新特性对于正在使用 proto2 的项目可能没有很多适合的使用场景。

# 2.proto2 与 proto3 的兼容

由于当前项目本身使用的是 proto2,规模较大,不太可能整体换成 proto3,因此需要考虑 2 和 3 的兼容问题。

首先,我尝试使用 proto2 的 lib 去编译 proto3 生成的代码,在编译时,会有很多方法找不到实现,所以这条最简单的路已经断了。

我又在 proto3 的环境下,进行了以下一些兼容性的测试:

# I.proto 语法

syntax = "proto2";
message RedisItem
{
  optional uint32 num = 1 [ default = 0 ];
  optional string str = 2;
  required uint32 num1 = 3;
  repeated uint32 nums = 4;
}

我使用了下常用的字段和关键词简单地测试了下,只要开头添加了'syntax = "proto2";' 语句指定解析的语法,就可以使用 proto3 的 lib、proto2 的语法,去生成相应的代码了,default、required 等的关键词不会产生错误。

# II. 编译

syntax = "proto3";
message RedisItemTest
{
  uint32 num = 1;
  string str = 2;
}

我新增了一个使用 proto3 语法的文件。我使用 g 进行了简单的编译,编译通过,所以语法上 proto2 和 proto3 至少在 c 这个语言上是可以共存的。

# III. 序列化

由于项目中很多结构已经序列化存入过数据库,因此序列话的兼容性是一定得考虑的。

message RedisItemTest
{
  optional uint32 num = 1;
  optional string str = 2;
}

<center>
(i).proto2
</center>

syntax = "proto3";
message RedisItemTest
{
  uint32 num = 1;
  string str = 2;
}

<center>(ii).proto3</center>

int main() {
  ifstream file;
  char str[100];
  file.open("testdata");
  file >> str;
  
  Cmd::RedisItemTest oTest;
  oTest.ParseFromString(str);
  cout << oTest.ShortDebugString() << endl;
  file.close();
  return 0;
}

<center>(iii). 读取代码 </center>

我先使用 proto2 的库和 (i) 中 proto2 语法文件,将 num=1,str="testtest" 的 RedisItemTest 序列化。然后,我使用 proto3 的库,生成 (ii) 中定义的 proto 文件,使用 (iii) 中的读取代码,得到了以下输出:

./test
num: 1 str: "testtest"

所以可以大致得出结论,proto2 与 proto3 至少在 c++ 上,序列化与反序列化方面是基本可以兼容的(由于只是可行性方面的测试,repeated 等字段没有进行细致的测试)。

# IV. 结论

目前为止,关于兼容性,大致我们得出了以下结论:

使用 proto2 的 lib 兼容 proto3 生成的代码:

​ 编译 ——❎

使用 proto3 的 lib 兼容 proto2 的文件:

​ proto 语法 ——✅

​ 编译 ——✅

​ 序列化 ——✅

所以目前看来,假如想将项目部分 proto2 结构升级为 proto3,将 lib 升级为 proto3,旧的 proto 文件使用 proto2 标记,似乎是个可行的方案。

# 3. 实际使用

经过以上的尝试,初步判断方案可行后,我开始了实际操作。

我将项目中默认值占用较多空间的 proto 结构放入了一个 proto3 的文件中,其余文件开头都添加了 proto2 的语法标记,成功使用 proto 文件生成了 c++ 代码。

但是在编译时,出现了一些问题:

首先,ByteSize () 这个方法,在 proto3 中替换为了 ByteSizeLong (),不过 3 中并没有删除该方法,实现改成了调用 ByteSizeLong (),所以代码运行不会有问题,只是编译会报一个警告,这个问题并不严重。

但是,proto3 生成的类都使用了 final 去修饰,不管语法是 proto2 还是 proto3,继承了其中的类在编译中会报错。2 中没有这个限制,而且通过 proto 中的类的子类,在代码中管理会更灵活,因此代码中不少地方定义了 proto 生成类的子类,导致了大量报错,修改的成本较高,于是这次升级在此以失败告终┭┮﹏┭┮。

最终,采用了以下宏,替换 set 方法,暂时解决设置默认值占用空间的问题,虽然并不优雅,但是是比较稳的一种解法。

#define proto_set(cmd, func, value) \
  {                                 \
    if (cmd.func() != value)        \
    {                               \
      cmd.set_##func(value);        \
    }                               \
  }

# 4. 尚存疑惑

目前,我不清楚为啥 proto3 要将生成的类用 final 修饰?

然后代码中的 final 都是用宏 PROTOBUF_FINAL 定义的,所以是不是可能可以通过开关控制?

目前有这些疑问,尚未查到答案,先记着看看未来能不能解决吧~

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Nirvana 支付宝

支付宝