网站首页> 文章专栏> 【重磅开源】兰亭集库(yaLanTingLibs)在github上开源了!
【重磅开源】兰亭集库(yaLanTingLibs)在github上开源了!
编辑时间:2022-10-27 00:08:03 作者:qicosmos 3条评论

什么是兰亭集库(yaLanTingLibs)?

“仰观宇宙之大,俯察品类之盛,所以游目骋怀,足以极视听之娱,信可乐也。”--《兰亭集序》

C++20 基础库兰亭集库 https://github.com/alibaba/yalantinglibs (yaLanTingLibs, ya不发音)正式开源!她的名字灵感正是来源于“兰亭集序”,因为兰亭集库是一个C++20 库的集合,里面有平时开发中常用到的库,如协程库(async_simple)、序列化库(struct_pack)和rpc(coro_rpc)库,后面还会不断增加新的基础库如http库,orm等库,总之,兰亭集库的目标是帮助C++用户快速构建高性能C++应用,易用性、性能和安全性是兰亭集库的主要特色!兰亭集库的长期目标是完善C++开发的生态。

因为yaLanTingLibs是基础库的合集,所以里面的每个子库都是可以独立使用的,如果你只需要序列化功能,只需要包含struct_pack的头文件就行,如果你只需要rpc功能,就包含coro_rpc的头文件,如果你只想使用协程库,那就只包含async_simple头文件。

真心希望大家yaLanTingLibs能帮助大家快速开发C++应用,希望大家能积极去使用yaLanTingLibs,感受一下它带来的开发效率和高性能!

yaLanTingLibs对编译器的要求:需要C++20标准,clang13+, gcc10+, msvc2022正在支持当中。推荐使用clang13+

长期主义

开源是起点而不是终点,我们会长期维护yaLanTingLibs,也会把它作为purecpp社区重点开源项目,每个月会组织和社区的会议,讨论兰亭库新特性支持和未来的工作,欢迎大家加入到兰亭库的维护与开发中来。

yaLanTingLibs的特色

前面已经讲到了兰亭集库的主要特色是易用、安全和高性能,这里想通过一些example来展示这些特性。

序列化库struct_pack:一行代码序列化/反序列化

struct_pack是一个以零成本抽象,高度易用为特色序列化库。只需一行代码即可完成复杂结构体的序列化/反序列化。用户无需定义任何DSL,宏或模板代码,struct_pack可通过编译期反射自动支持对C++结构体的序列化。其性能比protobuf,msgpack高一个数量级(10-50倍,详细可以看benchmark部分)。

以一个简单的对象为例展示struc_pack的基本用法。

struct person {
  int64_t id;
  std::string name;
  int age;
  double salary;
};

序列化/反序列化example

// 初始化一个person对象
person person1{.id = 1, .name = "hello struct pack", .age = 20, .salary = 1024.42};

// 1行代码序列化
std::vector<char> result = serialize(person1);
// 1行代码反序列化
auto person2 = deserialize<person>(buffer);
assert(person2); //person2.has_value()==true
assert(person2.value()==person1);

部分反序列化

有时候只想反序列化对象的某个特定的字段而不是全部,这时候就可以用部分反序列化功能了,这样可以避免全部反序列化,大幅提升效率。

// 只反序列化person的第2个字段
auto name = get_field<person, 1>(buffer.data(), buffer.size());
assert(name); //name.has_value()==true
assert(name.value() == "hello struct pack");

支持序列化所有的STL容器、自定义容器、optional、variant和expected

含各种容器的对象序列化

enum class Color { red, black, white };

struct complicated_object {
  Color color;
  int a;
  std::string b;
  std::vector<person> c;
  std::list<std::string> d;
  std::deque<int> e;
  std::map<int, person> f;
  std::multimap<int, person> g;
  std::set<std::string> h;
  std::multiset<int> i;
  std::unordered_map<int, person> j;
  std::unordered_multimap<int, int> k;
  std::array<person, 2> m;
  person n[2];
  std::pair<std::string, person> o;
  std::optional<int> p;
};

struct nested_object {
  int id;
  std::string name;
  person p;
  complicated_object o;
};

nested_object nested{.id = 2, .name = "tom", .p = {20, "tom"}, .o = {}};
auto buffer = serialize(nested);
auto nested2 = deserialize(buffer.data(), buffer.size());
assert(nested2)
assert(nested2==nested1);

自定义容器的序列化

// We should not inherit from stl container, this case just for testing.
template <typename Key, typename Value>
struct my_map : public std::map<Key, Value> {};

my_map<int, std::string> map1;
map1.emplace(1, "tom");
map1.emplace(2, "jerry");

absl::flat_hash_map<int, std::string> map2 =
    {{1, "huey"}, {2, "dewey"}, {3, "louie"},};

auto buffer1 = serialize(map1);
auto buffer2 = serialize(map2);

struct_pack benchmark

benchmark测试方法

待序列化的对象已经预先初始化,存储序列化结果的内存已经预先分配。对每个测试用例。我们运行一百万次序列化/反序列化,对结果取平均值。

测试对象

  1. 含有整形、浮点型和字符串类型person对象
struct person {
  int64_t id;
  std::string name;
  int age;
  double salary;
};
  1. 含有十几个字段包括嵌套对象的复杂对象monster
enum Color : uint8_t { Red, Green, Blue };

struct Vec3 {
  float x;
  float y;
  float z;
};

struct Weapon {
  std::string name;
  int16_t damage;
};

struct Monster {
  Vec3 pos;
  int16_t mana;
  int16_t hp;
  std::string name;
  std::vector<uint8_t> inventory;
  Color color;
  std::vector<Weapon> weapons;
  Weapon equipped;
  std::vector<Vec3> path;
};
  1. 含有4个int32的rect对象
struct rect {
  int32_t x;
  int32_t y;
  int32_t width;
  int32_t height;
};

测试环境

Compiler: Alibaba Clang 13

CPU: (Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz)

测试结果

向前/向后兼容性

当对象增加新的字段时,怎么保证兼容新旧对象的解析呢?当用户需要添加字段时,只需要在新对象末尾 增加新的 struct_pack::compatible<T> 字段即可。
以person对象为例:

struct person {
  int age;
  std::string name;
};

struct person1 {
  int age;
  std::string name;
  struct_pack::compatible<int32_t> id;
  struct_pack::compatible<bool> maybe;
};

struct_pack保证这两个类可以通过序列化和反序列化实现安全的相互转换,从而实现了向前/向后的兼容性。

coro_rpc:基于C++20 协程的高性能RPC

coro_rpc是用C++20开发的基于无栈协程和编译期反射的高性能的rpc库,在单机上echo测试qps达到2000万(详情见benchmark部分) ,性能远高于grpc。然而高性能不是它的主要特色,coro_rpc的主要特色是易用性,免安装,包含头文件就可以用,几行代码就可以完成一个rpc服务器和客户端。

coro_rpc的设计理念是:以易用性为核心,回归rpc本质,让用户专注于业务逻辑而不是rpc框架细节,几行代码就可以完成rpc开发。 rpc的本质是什么?rpc的本质就是一个远程函数,除了rpc底层的网络IO之外,其它的就和普通函数一样。用户不需要关注rpc底层的网络IO、路由、序列化等细节,用户只需要专注于rpc函数的业务逻辑即可,这就是coro_rpc的设计理念,正是基于这一设计理念,coro_rpc提供了非常简单易用的API给用户使用。通过一个例子来看coro_rpc的易用性。

coro_rpc example

rpc_server端

1.定义rpc函数

// rpc_service.hpp
inline std::string echo(std::string str) { return str; }

2.注册rpc函数和启动server

#include "rpc_service.hpp"
#include <coro_rpc/coro_rpc_server.hpp>

int main() {
  register_handler<echo>(); // 注册rpc函数

  coro_rpc_server server(/*thread_num =*/hardware_concurrency(), /*port =*/9000);
  server.start(); // 启动server并阻塞等待
}

对于rpc服务端来说,用户只需要定义rpc函数再启动server即可,不需要关注其它细节,5,6行代码就可以提供一个rpc服务了,是不是很简单!再来看看client是怎么调用hello这个rpc服务的。

rpc_client端

  1. 连接服务端
  2. rpc调用
#include "rpc_service.hpp"
#include <coro_rpc/coro_rpc_client.hpp>

Lazy<void> test_client() {
  coro_rpc_client client;
  co_await client.connect("localhost", /*port =*/9000);

  auto r = co_await client.call<echo>("hello coro_rpc"); //传参数调用rpc函数
  std::cout << r.result.value() << "\n"; //will print "hello coro_rpc"
}

int main() {
  syncAwait(test_client());
}

client调用rpc函数也同样简单,5,6行代码就可以实现rpc调用了。 就像调用本地函数一样调用远程rpc函数,在call里面输入函数名字和参数就可以实现远程调用了,非常简单。

相信上面的这个简单的例子已经充分展示了coro_rpc的易用性和特点了,也体现了rpc的本质,即用户可以像调用本地函数那样调用远程函数,用户只需要关注rpc函数的业务逻辑即可。

coro_rpc的接口易用性还体现在rpc函数几乎没有任何限制,你可以定义任意个数和任意类型参数的rpc函数, 参数的序列化和反序列化由rpc库自动完成,用户无需关心,简言之,coro_rpc可以让你像写本地函数一样写rpc函数,像调用本地函数一样调用rpc函数。

rpc函数支持任意参数

// rpc_service.h
// 客户端只需要包含这个头文件即可,无需把rpc的定义暴露给客户端。
void hello(){};
int get_value(int a, int b){return a + b;}

struct person {
  int id;
  std::string name;
  int age;
};
person get_person(person p, int id);

struct dummy {
  std::string echo(std::string str) { return str; }
};

// rpc_service.cpp
#include "rpc_service.h"

int get_value(int a, int b){return a + b;}

person get_person(person p, int id) {
  p.id = id;
  return p;
}

server端

#include "rpc_service.h"
#include <coro_rpc/coro_rpc_server.hpp>

int main() {
  register_handler<hello, get_value, get_person>();//注册任意参数类型的普通函数

  dummy d{};
  register_handler<&dummy::echo>(&d); //注册成员函数

  coro_rpc_server server(/*thread_num =*/hardware_concurrency(), /*port =*/9000);
  server.start(); // 启动server
}

client端

# include "rpc_service.h"
# include <coro_rpc/coro_rpc_client.hpp>

Lazy<void> test_client() {
  coro_rpc_client client;
  co_await client.connect("localhost", /*port =*/9000);

  //RPC调用
  co_await client.call<hello>();
  co_await client.call<get_value>(1, 2);

  person p{};
  co_await client.call<get_person>(p, /*id =*/1);

  auto r = co_await client.call<&dummy::echo>("hello coro_rpc");
  std::cout << r.result.value() << "\n"; //will print "hello coro_rpc"
}

int main() {
  syncAwait(test_client());
}

这里面get_person函数的参数和返回值都是结构体,通过编译期反射的序列化库struct_pack实现自动的序列化和反序列化,用户无感知,省心省力。

rpc函数支持协程函数

Lazy<int> get_coro_value(int val) { co_return val; }

int main() {
  register_handler<get_coro_value>();//注册协程函数
  coro_rpc_server server(/*thread_num =*/hardware_concurrency(), /*port =*/9000);
  server.start();
}

支持delay处理

void hello_with_delay(connection<std::string> conn) {
  // 在线程或者线程池中处理rpc请求
  std::thread([conn]() mutable {
    // 处理完成后response结果
    conn.response_msg("hello coro_rpc");
  }).detach();
}

int main() {
  register_handler<hello_with_delay>();
  coro_rpc_server server(2, 8801);
  server.start();
}

支持经典的异步回调

如果你不想使用协程,那么就用经典的异步回调的server

# include <coro_rpc/async_rpc_server.hpp>
inline std::string hello() { return "hello coro_rpc"; }
int main() {
  register_handler<hello>();
  async_rpc_server server(/*thread_num =*/hardware_concurrency(), /*port =*/9000);
  server.start();
}

coro_rpc benchmark

测试环境

Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz 96核 OS: Linux version 4.9.151-015.ali3000.alios7.x86_64 编译器:Alibaba Clang13 C++20

编译选项:Release –O3

测试case

客户端和服务端都在同一台机器上,使用不同连接数发送请求做echo测试 Pipeline 模式下极限qps 测试 Ping-pong模式下的qps和时延测试

测试备注

  • brpc由于采用了连接复用,实际上的socket连接数并没有那么多(实际连接数为96),coro_rpc的连接数是实际的连接数。
  • 测试客户端统一用coro_rpc的压测客户端测试,压测效果较好,基本能把cpu打满。没有使用brpc测试客户端原因是:如果用brpc客户端压测,brpc qps会降低一倍;
  • grpc的qps始终不会超过10万,故没有放进来做性能比较;
  • 目前测试场景仅仅是echo测试,没有测试更多更复杂的场景,测试数据仅供参考;

极限qps

alt

pingpong qps

alt

pingpong latency

alt

长尾测试 qps

alt

长尾测试 latency

alt

最后

Newer is Better 是purecpp社区倡导的,新技术带来新的编程思想,新的编程思想带来技术创新,只有通过技术创新才能在性能、易用性上超越经典。兰亭集库(yaLanTingLibs)就是这一倡导的最佳体现。

是因循守旧,止步不前,还是拥抱新技术,走积极进取的创新之路,it's your choice!

真心希望兰亭集库能帮助大家快速开发高性能C++应用,让大家享受到新技术带来的易用和效率方面的好处。

最后希望喜欢和准备使用yaLanTingLibs的用户加入到我们的群参与讨论,新需求,未来的方向都需要你们来规划,yaLanTingLibs是大家的,是社区的,希望大家能积极参与到这个开源项目中来。

微信群:

alt

钉钉群:

alt

qq群

alt


    出自:purecpp.cn

    地址: www.purecpp.cn

    转载请注明出处!


来说两句吧
登录才能发表评论。
最新评论
  • qicosmos
    fqbqrr 2022-10-26 02:34:21

    好库,不过能不能在`gitee`上放一个地址.

    `github`有时访问不了.

  • qicosmos
    sacomplex 2022-10-26 08:23:07

    希望能支持Arm, 还有项目不要太监了

  • qicosmos
    qicosmos 2022-10-27 00:08:58

    暂时没有精力去维护两个仓库,你们可以自己转过去;

    项目会长期维护,purecpp社区会重点支持兰亭集库。

Absolutely

purecpp

一个很酷的modern c++开源社区


[社区开源项目列表,点击前往]


purecpp社区自2015年创办以来,以“Newer is Better”为理念,相信新技术可以改变世界,一直致力于现代C++研究、应用和技术创新,期望通过现代C++的技术创新来提高企业生产力和效率。


社区坚持只发表原创技术文章,已经累计发表了一千多篇原创C++技术文章;


组织了十几场的C++沙龙和C++大会,有力地促进了国内外C++开发者之间的技术交流;


开源了十几个现代C++项目,被近百家公司所使用,有力地推动了现代C++在企业中的应用。


期待更多的C++爱好者能参与到社区C++社区的建设中来,一起为现代C++开源项目添砖加瓦,一起完善C++基础设施和生态圈。


微信公众号:purecpp, 社区邮箱: purecpp@163.com


友情链接