|
一个故事
这也是很久之前了,在一直都怀念的读书时代,参与的第一个的项目,其中有一部分网络通信,基于socket编程。网络通讯TCP/IP相当于交通工具,上层应用协议还得自己设计。学过计算机网络这门课的,自然会对所学的知识举一反三。
首先查看一个TCP的协议格式, 采用二进制的表示方式进行数据表示。比如一个端口unsigned short,那么网络传输就是16bits。那么相对于二进制传输的协议,比如一些JSON直接是字符串形式传输的,那么一个端口比如65530,那么在JSON中就要用5个字节去分别表示这个5个字节6,5,5,3,0, 会占用更多的带宽。
那么假设我们需求是传递一个学生信息(这个信息也将作为本文后续的例子),信息描述如下:
//这里故意用的汉语拼音,防止英文单词多重含义
enum XingBie
{
FEMALE,
MALE
};
struct School
{
std::string m_strName; //学校名字
std::string m_Address; //学校地址
};
struct Student
{
std::string m_strName; //姓名
unsigned int m_uAge; //年龄
XingBie m_eXingbie; //性别
std::vector<School> m_vSchools; //学习过的学校
};先来说说通信协议的定义:
- 整形: 就采用四个字节
- 字符串: 方法有多种,假设选择了最后一种。
- 可以在协议中以\0结尾表示结束,也可以在字符串
- 以固定长度来表示,比如255
- 在字符串的表述前面加一个长度,这样也可以用来表示任意长度的,任意字符的字节流
- 数组: 比如上述的Student就读过多所School,那么可以在数组前面加个数量,然后依次输入School信息
这个是一个刚入行的程序员设计的,结果如下.
接下来就会涉及到一个问题了,那就是序列化和反序列化。
- 序列化: 内存里面的对象是连续内存的,但是对象管理啊的数据不一定,序列化就是将这些内存的数据表示到连续的内存中。作为客户端,将序列化的内容发送到服务端。
- 反序列化: 一般来说接受到数据的服务器再将数据反序列化为内存里对象的结构状态,便于我们去操作。
而这些序列化的方法就由上述定义的协议来进行代码编写,反序列化则是一个解析数据的过程,也需要进行代码编写。
写着写着,我们就碰到了一些困难:
- 代码后续要增加新的类型,得重新在协议中定义
- 后续传输的数据进行变更,对象的成员和方法,序列化与反序列化代码都得跟着去修改,并且可能存在服务器与客户端不一致的兼容性问题。
后来有一天有个爱钻研技术的同学和我说, “你知道google出了个Protobuf吗?”,于是看了看,这个完美的解决了我们的痛点啊。那么接下来就让我们看一看本文的主人公Protocol Buffers(Protobuf)。
相关视频推荐
高并发之protobuf通信协议设计|序列化|协议粘包丨即时通讯设计丨
高性能服务器通信协议设计之xml-json-protobuf对比分析
LinuxC++后台服务器开发架构师免费学习地址
【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!~点击832218493加入(需要自取)
Protobuf for C++
Protobuf 可以快速的帮你完成以下两件事儿:
- 编写一个Student.proto文件,去定义你的一个Message
- 然后根据Student.proto, 用protoc生成相应的语言代码, 比如C++, Golang, Python, C#, Java等等。这样也便于在分布式环境中,多个不同语言的服务之间通过Protobuf去通信。其实除了分布式的网络访问方式,有时候也可以在同一个进程里跨语言调用,比如C#/Python/Golang调用C++的代码,使用了Protobuf也就不用过于关心不同语言之间数据类型兼容的问题,调用的时候只需要传入一个序列化的数据地址和数据大小。
Student.proto
这个文件用来定义我们的数据结构,将上一章的例子使用Protobuf来定义。可以看到如下:
- 协议采用的是proto3
- package ProtoSample 那么就转换为C++的namespace ProtoSample
- 所有的字段均是singular, 也就是proto2中的optional, 并且注意proto3中也没有required字段了。
syntax = &#34;proto3&#34;;
package ProtoSample;
message School {
string name = 1;
string address = 2;
}
enum XingBie {
FEMALE = 0;
MALE = 1;
}
message Student {
string name = 1;
int32 age = 2;
XingBie xingbie = 3;
repeated School schools = 4;
}编译Student.proto
Protobuf使用的流程如下图所示:
- 先编译Protobuf的源码,参照官方文档即可。以Windows为例(Linux类似),编译后产生protoc.exe和libprotobuf.lib
- protoc.exe用于编译Student.proto,将产生两个源码文件Student.pb.h和Student.pb.cc: 这个文件主要就是传输的数据结构的定义,包括设置/获取接口,序列化与反序列化等。
- 最后就是自己的项目编译,把上面产生的Student.pb.h, Student.pb.cc和libprotobuf.lib都引用在项目中。
简单说下编译Student.proto到C++的源码文件的命令:protoc -I=. --cpp_out=. Student.proto
protobuf的代码使用
我写了个简单示例, 这个示例展示了Protobuf产生的对象的使用:
- CreateStudent中直接构造一个对象
- SerializeToString序列化
- ParseFromString反序列化
- 在有些系统构成中,可能还需要用到json,也可以直接使用MessageToJsonString将对象序列化为一个json
#include <iostream>
#include <string>
#include <vector>
#include &#34;Student.pb.h&#34;
#include &#34;google/protobuf/util/json_util.h&#34;
// 构造一个学生对象
void CreateStudent(ProtoSample::Student& student)
{
student.set_name(u8&#34;一个程序员的修炼之路&#34;);
student.set_age(18);
student.set_xingbie(ProtoSample::XingBie::MALE);
auto pSchool = student.add_schools();
pSchool->set_name(u8&#34;小学&#34;);
pSchool->set_address(u8&#34;老地方&#34;);
pSchool = student.add_schools();
pSchool->set_name(u8&#34;初中&#34;);
pSchool->set_address(u8&#34;新地方&#34;);
}
//打印一个学生信息
void PrintStudent(const ProtoSample::Student& student)
{
std::cout << &#34;Student Name: &#34; << student.name() << std::endl
<< &#34;Student Age: &#34; << student.age() << std::endl
<< &#34;Student Xingbie: &#34; << student.xingbie() << std::endl;
for (auto& school : student.schools())
{
std::cout << &#34;School Name: &#34; << school.name() << std::endl
<< &#34;School Address: &#34; << school.address() << std::endl;
}
}
int main()
{
setlocale(LC_CTYPE, &#34;zh-cn.utf-8&#34;);
// 1. 构造一个学生信息
ProtoSample::Student student;
CreateStudent(student);
// 2. 序列化学生信息
std::string strStudentInBytes;
student.SerializeToString(&strStudentInBytes);
// 3. 反序列化学生
ProtoSample::Student studentNew;
studentNew.ParseFromString(strStudentInBytes);
PrintStudent(studentNew);
// 4. 序列化到Json格式
std::string strJson;
google::protobuf::util::MessageToJsonString(studentNew, &strJson);
std::cout << strJson << std::endl;
return 0;
}
proto 2 和 proto 3
proto 1是google从2001年就开始开发内部使用,不过还不够完善,也并没有开源,后来完善后开源了proto 2。
proto 2和proto 3并不是相互兼容的,个人认为proto 3除了支持更多的功能,也更加简明。比如Proto 3废弃了optional, 虽然现在等同于默认的singular,但是在proto2中optional int32 name可以使用has_name()来判断是否具有设置这个值,而在proto3中不可以,并且为默认值0,这个在参考3中有比较详细的讨论。
关于更多的区别可以直接查看 Proto V3:
https://github.com/protocolbuffers/protobuf/releases/tag/v3.0.0
这里我们关心一个问题,如果是一个新的项目,该使用Proto 2还是Prot 3。以下是google的官方回答, 一句话建议使用Proto 3。
proto3 is the current version of the language. This is the most commonly used version of the language. We encourage new code to use proto3.
proto2 is an older version of the language. Despite being superseded by proto3, proto2 is still fully supported. Protobuf VS Json
Json也是一个广泛应用于数据传输,那么什么时候用Json什么时候选择Protobuf呢,那就要从他们的特点对比来看一看了。
使用复杂度
相对于而言JSON的使用比较方便:
- Protobuf需要定义一个Schema文件.proto,并且需要编译,引入源码文件和库。
- JSON直接文本形式表述,很多语言内置支持。
数据表达能力
JSON适合用于表达相对简单的数据结构,而Protobuf直接生成相应语言对应的结构,基本可以表达任意结构,更胜一筹。
数据格式
这个就看使用场景,文本的优势在于可读性好,这样更利于在一些Web调用方面更加合适,便于使用浏览器直接调试。而Protobuf适用于分布式环境中的内部交互,并且一般要求数据表达能力更强,或者使用效率更高的场景。
当然了
- JSON采用文本, 一般来说体积比二进制大,传输的带宽和效率也会相对较低。
- Protbuf二进制
效率
在序列化,反序列化,一般来说Protobuf效率更高。举个最简单的例子,比如二进制存储(Bytes),在JSON中必然要使用对字节的编码,并且解码,而在Protobuf中直接使用二进制存储。
语言支持
这个必然是Json使用的更加广泛,并且基本语言要么内置JSON解析器,要么就是有很多的SDK。而Protobuf支持的语言数量还是有限。
综合来看,个人的使用意向是,如果Json能够完整表达数据,并且没有太高的效率要求,首选JSON。否则,就选Protobuf吧。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|