C++ 序列化与 RPC

96 阅读3分钟

ToplingDB 的 分布式 Compact 中 Client-Server 交互,使用了 topling-zip 中的序列化框架,该序列化框架初版完成于 2006 年,随后基于该序列化框架,实现了一个 RPC 系统。

后来整个库命名为 febird 库在 google code 上开源,再后来 google code 停止服务,febird 迁移到 github,有段时间重命名为 nark,之后重命名为 terark,目前 topling-zip 中代码的 namespace 仍是 terark。从 2006 年至今,除 namespace 名称之外,该序列化框架的接口一直保持稳定,2016 年的时候,针对 C++11 进行了模板推导相关的大幅优化,但仍保持了接口的稳定。

以下为原文正文,排版有轻微改动。

用 C++ 的高级模版特性实现一个不需要 IDL 的 RPC
作者: rockeet
发表日期: 2006年12月09日
分类: C++序列化 评论: 4 条 阅读次数: 4,053 次

当然,首要的是为了有用,其次是挑战自己驾驭 C++ 的能力,目前已经全部完成,并且取得了非常好的效果。使用该 RPC 的简短示例代码:

(一)接口定义

// sample usage...
// test.h 
typedef std::vector<unsigned> vint_vec;
class AsyncInterface : public GlobaleScope {
public:
	BEGIN_RPC_ADD_MF(AsyncInterface)
		RPC_ADD_MF(get_val)
		RPC_ADD_MF(get_len)
		RPC_ADD_MF(squareVec)
		RPC_ADD_MF(multiVec)
	END_RPC_ADD_MF()
 
	RPC_DECLARE_MF(get_val, (rpc_in<int> x))
	RPC_DECLARE_MF(get_len, (const std::string& x))
	RPC_DECLARE_MF(squareVec, (vint_vec& x))
	RPC_DECLARE_MF(multiVec, (vint_vec& z, vint_vec& x, vint_vec& y))
};
RPC_TYPEDEF_PTR(AsyncInterface);
// more convenient way than AsyncInterface...
BEGIN_RPC_INTERFACE(SampleRPC_Interface2, SessionScope)
	RPC_ADD_MF(get_val)
	RPC_ADD_MF(get_len)
END_RPC_ADD_MF()
	RPC_DECLARE_MF(get_val, (rpc_in<int> x))
	RPC_DECLARE_MF(get_len, (const std::string& x))
END_RPC_INTERFACE()

(二)客户端

该客户端代码展示了同步和异步 RPC 调用,同步的更简单。

// async_client.cpp
#include <iostream>
#include <febird/rpc/client.h>
#include <febird/io/SocketStream.h>
using namespace std;
using namespace febird;
using namespace febird::rpc;
#include "../test.h"
void printVec(const vint_vec& vec) {
  for (int i = 0; i != vec.size(); ++i)
    cout << "vec[" << i << "]=" << vec[i] << "/n";
}
class AsyncImpl : public AsyncInterface {
public:
  AsyncImpl() {
    this->multiVec.set_async_callback(&AsyncImpl::on_multiVec);
  }
private:
  void on_multiVec(const client_packet_base& packet, vint_vec& z, vint_vec& x, vint_vec& y) {
    printf("AsyncImpl::on_multiVec/n");
    printf("ret=%u, z=%u, x=%u, y=%u/n", packet.retv, z.size(), x.size(), y.size());
  }
};
RPC_TYPEDEF_PTR(AsyncImpl);
int main(int argc, char* argv[]) try {
  auto_ptr<SocketStream> cs(ConnectSocket("127.0.0.1:8001"));
  rpc_client<PortableDataInput, PortableDataOutput> client(cs.get());
  AsyncImplPtr obj1;
  SampleRPC_Interface2Ptr obj2;
  client.create(obj1, "obj1");
  client.create(obj2, "obj2");
  int ret;
  ret = client.retrieve(obj2, "obj2");
  rpc_ret_t val = obj1->get_val(100);
  cout << "obj1->get_val(100)=" << val << "/n";
  val = obj1->get_len("hello, world!");
  cout << "obj1->get_len(/"hello, world!/")=" << val << "/n";
  std::vector<unsigned> vec;
  vec.push_back((1));
  vec.push_back((2));
  vec.push_back((3));
  vec.push_back((4));
  vec.push_back((11));
  vec.push_back((22));
  vec.push_back((33));
  vec.push_back((44));
  val = obj1->squareVec(vec);
  printVec(vec);
  val = obj1->squareVec(vec);
  printVec(vec);
  std::vector<unsigned> vec2;
  for (int i = 0; i != vec.size(); ++i) {
    vec2.push_back(i + 1);
  }
  for (int i = 0; i < 5; ++i) {
    std::vector<unsigned> vec3;
    obj1->multiVec.async(vec3, vec, vec2);
    cout << "obj1->multiVec(vec3, vec, vec2) = " << val << "/n";
    printVec(vec3);
  }
  client.wait_pending_async();
  return 0;
}
catch (const std::exception& exp) {
  printf("exception: what=%s/n", exp.what());
  return 1;
}

(三)服务端

// async_server.cpp
#include <febird/rpc/server.h>
#include <febird/io/SocketStream.h>
#include <iostream>
using namespace febird;
using namespace febird::rpc;
#include "../test.h"
// rpc 函数的返回值是 rpc_ret_t,主要是为了避免专门为 void 返回值的函数编写特化代码
// use macro for convenient
BEGIN_RPC_IMP_INTERFACE(SampleRPC_Imp1, AsyncInterface)
  rpc_ret_t get_val(rpc_in<int> x) {
    std::cout << "AsyncInterface::get_val(rpc_in<int> x=" << x.r << ")/n";
    return x.r;
  }
  rpc_ret_t get_len(const std::string& x) {
    std::cout << "AsyncInterface::get_len(const std::string& x=/"" << x << "/")/n";
    return x.size();
  }
  rpc_ret_t squareVec(vint_vec& x) {
    for (vint_vec::iterator i = x.begin(); i != x.end(); ++i) {
      *i *= *i;
    }
    return x.size();
  }
  rpc_ret_t multiVec(vint_vec& z, vint_vec& x, vint_vec& y) {
    z.clear();
    for (int i = 0; i != x.size(); ++i) {
      z.push_back(x[i] * y[i]);
    }
    return 0x12345678;
  }
END_RPC_IMP_INTERFACE()
// don't use macro for more control
class SampleRPC_Imp2 : public SampleRPC_Interface2 {
  rpc_ret_t get_val(rpc_in<int> x) {
    std::cout << BOOST_CURRENT_FUNCTION << "x=" <<x.r << "/n";
    return x.r;
  }
  rpc_ret_t get_len(const std::string& x) {
    std::cout << BOOST_CURRENT_FUNCTION << "x=" << x << "/n";
    return x.size();
  }
public:
  // if use macro, such as SampleRPC_Imp1, app can not use custom create
  static remote_object* create() {
    SampleRPC_Imp2* p = new SampleRPC_Imp2;
    // set p ...
    return p;
  }
};

int main(int argc, char* argv[]) {
  try {
    SocketAcceptor acceptor("0.0.0.0:8001");
    rpc_server<PortableDataInput, PortableDataOutput> server(&acceptor);
    // register rpc implementation class...
    RPC_SERVER_AUTO_CREATE(server, SampleRPC_Imp1);
    server.auto_create((SampleRPC_Imp2*)0, &SampleRPC_Imp2::create);
    server.start();
  }
  catch (const std::exception& exp) {
    printf("exception: what=%s/n", exp.what());
  }
  return 0;
}

(四)解释说明

主要思路就是在 Server 端,用宏生成函数声明和工厂代码,把类注册到工厂中,启动server后,就可以自动接收client端的函数调用了。client发来的函数调用请求会派发到server端的用户定义函数体。当然,client端没有用户定义的函数体。

在client端,同样名字的宏,生成了和server端不同的代码,这些代码生成stub函数,把函数调用的参数序列化到网络。

其中主要使用了 boost::tuple,和 boost::any,来生成stub的虚函数。

这个实验很有趣,它说明了 C++ 强大的表达能力,即使没有 IDL,仅使用语言本身就可以实现 RPC。