手把手写C++服务器(19):序列化数据网络传输解决方案

Posted 沉迷单车的追风少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器(19):序列化数据网络传输解决方案相关的知识,希望对你有一定的参考价值。

前言:数据传输是服务器编程必须要面临的问题之一,原始数据传输是非常脆弱的,序列化传输是业界常用的方法,其中谷歌的PB方案广受欢迎,常用作项目中主要的解决方案,值得C++服务器编程者学习。

目录

C++传输序列化数据解决方案

1、二进制方式

2、XML

3、Boost.Serialization

4、Protocol buffers

PB使用流程简述

详细使用步骤

1、定义.proto文件

2、定义消息

3、标注数据类型

4、代码示例

5、编译生成类

6、使用API

官方优化建议:

缺点和优点


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

C++中文教程翻译:https://www.cnblogs.com/cposture/p/9029033.html

Python教程地址:https://googleapis.dev/python/protobuf/latest/

详细使用步骤

1、定义.proto文件

.proto 文件以一个 package 声明开始,这可以避免不同项目的命名冲突。在 C++,你生成的类会被置于与 package 名字一样的命名空间。

2、定义消息

下一步,你需要定义消息(message)。消息只是一个包含一系列类型字段的集合。大多标准简单数据类型是可以作为字段类型的,包括 boolint32floatdouble 和 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等方法。举例说明:如果有 nameidemailphone 字段,将会生成以下方法:

// 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),而 nameemail 字段有两个额外的方法,因为它们是字符串——一个是可以获得字符串直接指针的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紧急传输的方法——带外数据 (原理与代码示例)

手把手写C++服务器(21):Linux socket网络编程入门基础

手把手写C++服务器:专栏文章-汇总导航更新中

手把手写C++服务器:Linux四大必备网络分析工具