跳至正文

[Walo系列]3. 序列化和rpc的选型与思考

  • 随笔

rpc库

决心推进这个计划之后就开始预研各种序列化和rpc库,看了很多对比,也做了一些测试,在这里统一记录一下。

仔细看来序列化做的事情就是把数据打包,rpc要考虑的核心事情是如何把函数的形参和返回类型同步给双端。

我看rpc库好像也只有两种形式,一个是通过proto定义好,例如grpc;另外一个是直接直接在调用的时候限定,属于一种硬编码?类似于rpclib

浅浅地看了三个rpc库:

  • rpclib
  • grpc
  • capnproto

通过各自的手段定义rpc的消息协议,然后可以快速简单地开始写一个网络通信程序。但是基本上是把连接管理的活都干完了,这不是我所需要的。

因为我是需要同时TCP/UDP,并且基于kcp或者之类的形式做字节流,所以我除了实现数据传输,还需要一些连接控制的消息,所以rpc库不是我需要的。

但是看了一下rpclib和grpc底层的代码,rpc的核心业无非就是两件事情,序列化反射

如何快速将rpc调用变成字节流,通知到远端之后,远端如何快速解码出参数和rpc过程,并且反射找出本地的函数。除了这个核心的机制之外的功能,都是不同的库提供的特性。

所以我要专注于序列化和反射这两块,看看能不能找到合适的库或者自己做,目的的核心第一是学习,其次才是实用。

序列化

序列化做的事情就是将数据按照某种格式序列化成二进制流,然后将二进制流恢复到内存。

不同的库致力于解决不同大小场景的问题,所有的库都是各有优劣。首先两种序列化库是要写proto的:

写proto的库

  • protobuf
  • capnproto

我以为,这样的库主要解决的问题有几个:

  1. 跨语言的通信一致性
    因为精确地定义了协议,服务接入者按照协议接入就能使用,他们能生产对应语言的结构。这个场景主要是在互联网微服务的架构下,不同的微服务可以用不同的技术栈,对外通过统一的proto提供快速开发的手段。
  2. 快速开发
    这些库都是直接server.listen()就能联网通信,开发效率嘎嘎快。
  3. 简单地兼容性维护
    不用proto能做到兼容性维护,但是得在协议层或者入口处新增很多if的代码。而proto的协议帮你处理好了这一切,还提供很多默认协议之类的功能,是极其简单的。

capnproto

优点是无需解码,可以直接从内存里拿。但是仔细看了一下文档,它拿的方式并不是定义一个class或者struct,而是直接对流数据进行读写。不过它读写的效率并不低。
我们通常读一个结构的内存是这样:

struct C{
   int a;
   int b;
};
void foo()
{
    C c;
    c.a = 23;
    bar(c.a);
}

但是capnp proto的读写方式是这样:

// capnp proto
struct EntityMessage
{
    msgid @0: UInt32;
    entityid @1: UInt32;
    msgdata @2: Text;
}

void foo(void)
{
    capnp::MallocMessageBuilder message;
    walo::EntityMessage::Builder entityMessage = message.initRoot<walo::EntityMessage>();
    entityMessage.setEntityid(21);
    entityMessage.setEntityid(47);

    const auto msgBuilder = capnp::Text::Builder(const_cast<char*>("test"));
    entityMessage.setMsgdata(msgBuilder);
    capnp::writePackedMessageToFd(1, message);
}

如上面的代码,并不能直接new一个proto的struct,而是需要用它提供的Builder去构造一个对象。

然后要怎么发送呢?或者说怎么获得一个“正常”的接口呢?
不知道…官方文档里没写,网上找到是要这样:

void sendMessage( const char* data, const std::size_t size ); 

void writeAddressBook()
{
    ::capnp::MallocMessageBuilder message;

    auto addressBook = message.initRoot<AddressBook>();
    auto people = addressBook.initPeople(1);

    auto alice = people[0];
    alice.setId(123);
    alice.setName("Alice");
    alice.setEmail("alice@example.com");

    auto alicePhones = alice.initPhones(1);
    alicePhones[0].setNumber("555-1212");
    alicePhones[0].setType(Person::PhoneNumber::Type::MOBILE);
    alice.getEmployment().setSchool("MIT");

    // get char array and send

    const auto m = capnp::messageToFlatArray( message );
    const auto c = m.asChars();
    std::cout << c.size() << '\n';

    sendMessage( c.begin(), c.size() ); // pass as char array
}

库有两个命名空间kj::capnp::,虽然差不多理清楚了,基础数据相关都是kj::提供,序列化相关都是capnp::提供,但还是挺……没必要的,这俩库都是老哥自己搞的。
而且作者竟然提供了直接输出到fd的接口,但没提供输出到标准库的stream或者字符串的输出…作者老哥确实也在文章里提到了stl比较重,就自己撸了一套…

protobuf

不过多介绍了,甚至算行业标准。

无需proto的库

这种库太灵活了,它的优势就是无需proto,可以动态处理,快速迭代。

对于游戏内的协议序列化,一定是选择这种协议,那这种协议就多了,有一点看不过来,索性打算做一个完整地测试。

对比

目标库

需要proto

  • protobuf
  • capnproto

无需proto

  • zpp_bits: C++20的高性能序列化库
  • msgpack: 非常高效的全语言序列化库
  • bitsery: C++11的序列化库,自称很适合游戏
  • Cista++: 基于C++17的struct序列化&反射库
  • yas: 有一个测评说它很快,但是很多年没维护了
  • boost::Serialization: 可以序列化stl容器对象,同时也能序列化指针对象
  • cereal: 高效的结构序列化库,比boost序列化要低一点,不支持指针
  • Flatbuffers: 跨平台的序列化库,内存利用率高,并且在不解析的情况下读数据

对比方案

其实这些库拥有不完全相同的场景,例如boost::Serialization还会将指针所指向的对象做序列化,以实现指针结构的序列化。
包括有几个是专门给C++的struct结构直接使用的,还有一些是跨语言的序列化库。
虽然如此,但是我的目的很明确,我想给我的Walo选择底层的序列化库,我的诉求就是速度快且内存大小小,而我的Walo是服务于游戏的底层。

就这个实际的场景,我的目标有两种:

  • 协议控制包
  • 游戏数据包
    协议控制代表连接控制相关的内容,例如握手,权限校验等;游戏数据包代表游戏具体的属性同步以及rpc等内容。
    协议控制包
    协议控制包会让有proto的库一起参与测试,同时我会倾向于用固定结构的包,即使最后场景式非proto的协议更好,我也会确固定这部分的内容。
    因为这个协议是需要量好设计,且易读易改好累计的。这个场景更多是小体积,结构固定的包,核心诉求还是体积小且快,具体选型等测试结果。

游戏数据包
这是游戏的主要数据协议,细节我还没完全设计好,例如要不要在最底层的协议去设计actor模型,要不要在消息层面支持多个channel。
当然这种类型最主要的还是数据和动态序列化本身,因为涉及到上层的脚本,甚至下层还会有zip压缩,这里的速度快可能也会被整体拖慢,所以我以为,包体大小会更重要一点。

下一篇这个系列就做测试。

End.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

目录