手把手写C++服务器(19):序列化数据网络传输解决方案
Posted 沉迷单车的追风少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器(19):序列化数据网络传输解决方案相关的知识,希望对你有一定的参考价值。
前言:数据传输是服务器编程必须要面临的问题之一,原始数据传输是非常脆弱的,序列化传输是业界常用的方法,其中谷歌的PB方案广受欢迎,常用作项目中主要的解决方案,值得C++服务器编程者学习。
目录
C++传输序列化数据解决方案
1、二进制方式
以二进制形式发送/接收原生的内存数据结构。通常,这是一种脆弱的方法,因为接收/读取代码的编译必须基于完全相同的内存布局、大小端等等。同时,当文件增加时,原始格式数据会随着与该格式相连的软件拷贝而迅速扩散,这将很难扩展文件格式。
2、XML
Extensible Markup Language,可拓展标记语言。用来传送及携带数据信息,不用来表现或展示数据,html则用来表现数据,所以XML用途的焦点是它说明数据是什么,以及携带数据信息。 但是XML
是空间密集型的,且在编码和解码时,它对程序会造成巨大的性能损失。同时,使用 XML DOM 树被认为比操作一个类的简单字段更加复杂。C++有个优秀的开源库支持XML解析与使用,具体引用下图:
3、Boost.Serialization
Boost.Serialization可以创建或重建程序中的等效结构,并保存为二进制数据、文本数据、XML或者有用户自定义的其他文件。该库具有以下吸引人的特性:
- 代码可移植(实现仅依赖于ANSI C++)。
- 深度指针保存与恢复。
- 可以序列化STL容器和其他常用模版库。
- 数据可移植。
- 非入侵性。
4、Protocol buffers
Protocol Buffers(简称:ProtoBuf)是一种谷歌推出的一种序列化数据结构的协议。对于透过管道(pipeline)或存储资料进行通信的程序开发上是很有用的。这个方法包含一个接口描述语言,描述数据结构,并提供程序工具根据这些描述产生代码,用于将这些数据结构产生或解析资料流。 针对C++原生支持,能够灵活、高效、自动化的解决序列化数据传输问题。
PB使用流程简述
使用 Protocol buffers
,需要写一个 .proto
说明,用于描述你所希望存储的数据结构。利用 .proto
文件,protocol buffer 编译器可以创建一个类,用于实现自动化编码和解码高效的二进制格式的 protocol buffer 数据。产生的类提供了构造 protocol buffer
的字段的 getters 和 setters,并且作为一个单元,关注读写 protocol buffer
的细节。重要的是,protocol buffer
格式支持扩展格式,代码仍然可以读取以旧格式编码的数据。
PB不仅支持C++,还针对C#、Python、Java、Goland、dart提供了接口和编译器,具体链接汇总如下:
源码和编译器安装地址:https://github.com/protocolbuffers/protobuf
官网地址:https://developers.google.com/protocol-buffers
教程地址:https://developers.google.com/protocol-buffers/docs/tutorials
C++教程地址:https://developers.google.com/protocol-buffers/docs/cpptutorial
详细使用步骤
1、定义.proto文件
.proto
文件以一个 package 声明开始,这可以避免不同项目的命名冲突。在 C++,你生成的类会被置于与 package 名字一样的命名空间。
2、定义消息
下一步,你需要定义消息(message)。消息只是一个包含一系列类型字段的集合。大多标准简单数据类型是可以作为字段类型的,包括 bool
、int32
、float
、double
和 string
。具体对应关系如下表所示:
.proto type | c++ | notes |
double | double | |
float | float | |
int32 | int32 | 使用可变长编码方式,负数时不够高效,应该使用sint32 |
int64 | int64 | 同上 |
uint32 | uint32 | 使用可变长编码方式 |
uint64 | uint64 | 同上 |
sint32 | int32 | 使用可变长编码方式,有符号的整型值,编码时比通常的int32高效 |
sint64 | sint64 | 同上 |
fixed32 | uint32 | 总是4个字节,如果数值总是比2^28大的话,这个类型会比uint32高效 |
fixed64 | uint64 | 总是8个字节,如果数值总是比2^56大的话,这个类型会比uint64高效 |
sfixed32 | int32 | 总是4个字节 |
sfixed64 | int64 | 总是8个字节 |
bool | bool | |
string | string | 一个字符串必须是utf-8编码或者7-bit的ascii编码的文本 |
bytes | string | 可能包含任意顺序的字节数据 |
3、标注数据类型
-
required:必须提供字段的值,否则消息会被认为是 "未初始化的"(uninitialized)。如果
libprotobuf
以 debug 模式编译,序列化未初始化的消息将引起一个断言失败。以优化形式构建,将会跳过检查,并且无论如何都会写入消息。然而,解析未初始化的消息总是会失败(通过 parse 方法返回false
)。除此之外,一个 required 字段的表现与 optional 字段完全一样。
required 是永久性的,在把一个字段标识为 required 的时候,应该特别小心。如果在某些情况下你不想写入或者发送一个 required 的字段,那么将该字段更改为 optional 可能会遇到问题。 -
optional:字段可能会被设置,也可能不会。如果一个 optional 字段没被设置,它将使用默认值。对于简单类型,你可以指定你自己的默认值,正如例子中我们对电话号码的
type
一样,否则使用系统默认值:数字类型为 0、字符串为空字符串、布尔值为 false。对于嵌套消息,默认值总为消息的"默认实例"或"原型",它的所有字段都没被设置。调用 accessor 来获取一个没有显式设置的 optional(或 required) 字段的值总是返回字段的默认值。 -
repeated:字段可以重复任意次数(包括 0)。repeated 值的顺序会被保存于 protocol buffer。可以将 repeated 字段想象为动态大小的数组。
4、代码示例
直接copy谷歌官方的例子吧:
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
5、编译生成类
编译.proto文件,生成用于读写消息的类,运行protoc编译器即可,也可以讲编译方法写到Makefile文件中编译。
编译器下载:https://developers.google.com/protocol-buffers/docs/downloads.html
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
编译后会自动生成以下文件:
addressbook.pb.h
,声明生成类的头文件。addressbook.pb.cc
,包含类的实现。
6、使用API
怎样使用呢?具体来说,提供了set、clear、has等方法。举例说明:如果有 name
、id
、email
和 phone
字段,将会生成以下方法:
// name
inline bool has_name() const;
inline void clear_name();
inline const ::std::string& name() const;
inline void set_name(const ::std::string& value);
inline void set_name(const char* value);
inline ::std::string* mutable_name();
// id
inline bool has_id() const;
inline void clear_id();
inline int32_t id() const;
inline void set_id(int32_t value);
// email
inline bool has_email() const;
inline void clear_email();
inline const ::std::string& email() const;
inline void set_email(const ::std::string& value);
inline void set_email(const char* value);
inline ::std::string* mutable_email();
// phone
inline int phone_size() const;
inline void clear_phone();
inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
inline ::tutorial::Person_PhoneNumber* add_phone();
数字 id
字段仅有上述的基本读写函数集合(accessors),而 name
和 email
字段有两个额外的方法,因为它们是字符串——一个是可以获得字符串直接指针的mutable_
getter ,另一个为额外的 setter。注意,尽管 email
还没被设置(set),你也可以调用 mutable_email
;因为 email
会被自动地初始化为空字符串。在本例中,如果你有一个单一的(required 或 optional)消息字段,它会有一个 mutable_
方法,而没有 set_
方法。
repeated 字段也有一些特殊的方法——如果你看看 repeated phone
字段的方法,你可以看到:
- 检查 repeated 字段的
_size
(也就是说,与Person
相关的电话号码的个数) - 使用下标取得特定的电话号码
- 更新特定下标的电话号码
- 添加新的电话号码到消息中,之后你便可以编辑。(repeated 标量类型有一个
add_
方法,用于传入新的值)
为了获取 protocol 编译器为所有字段定义生成的方法的信息,可以查看 C++ generated code reference。
备注:由于API一次性提供了清理、读写等所有接口,从面向对象设计的角度来说,再次封装、仅暴露必须使用的接口,是一个更好的使用方法。
官方优化建议:
C++ Protocol Buffer 库已极度优化过了。但是,恰当的用法能够更多地提高性能。这里是一些技巧,可以帮你从库中挤压出最后一点速度:
-
尽可能复用消息对象。即使它们被清除掉,消息也会尽量保存所有被分配来重用的内存。因此,如果我们正在处理许多相同类型或一系列相似结构的消息,一个好的办法是重用相同的消息对象,从而减少内存分配的负担。但是,随着时间的流逝,对象可能会膨胀变大,尤其是当你的消息尺寸(译者注:各消息内容不同,有些消息内容多一些,有些消息内容少一些)不同的时候,或者你偶尔创建了一个比平常大很多的消息的时候。你应该自己通过调用 SpaceUsed 方法监测消息对象的大小,并在它太大的时候删除它。
-
对于在多线程中分配大量小对象的情况,你的操作系统内存分配器可能优化得不够好。你可以尝试使用 google 的 tcmalloc。
缺点和优点
缺点:
protobuf不支持二维数组(指针),不支持STL容器序列化
稍复杂点的数据结构或类结构里出现二维数组、二维指针和STL容器(set、list、map等)很频繁,但因为 protobuf简单的实现机制,只支持一维数组和指针(用repeated修饰符修饰),不能使用repeated repeated来支持二维数组, 也不支持STL,因此在选择该方案之前,一定要确保你的数据结构里没有这些不支持的类型。
优点:
1.更强表达能力,更小存储空间。
2.数据包含自解释的schema,可以让数据生产者与使用者构建共同的数据视图。
3.结构化数据,可以让系统处理更高效,使用更方便。带结构的日志可以被视为表,query engine可以直接查询pb日志,意味着日志打印出来后就可以直接用sql来进行查询。
4.从数据源头规范数据,构建统一的数据格式。
参考
以上是关于手把手写C++服务器(19):序列化数据网络传输解决方案的主要内容,如果未能解决你的问题,请参考以下文章
手把手写C++服务器(22):Linux socket网络编程进阶第一弹
手把手写C++服务器(22):Linux socket网络编程进阶第一弹
手把手写C++服务器(18):TCP紧急传输的方法——带外数据 (原理与代码示例)