关于Thrift RPC之C++服务端和客户端的一个封装

  之前也介绍过,Thrift总体架构是分层设计的,用户就可以根据自己的需求对各个层进行个性化选择和定制,但这也带来了弊病:细节多了的话就增加了复杂性,尤其Thrift相关文档严重匮乏已经被诟病许久,这么一来无疑增加了初用者的门槛,就类似于一个工具无论再强大,但是man出来有几十上百个选项参数,估计再有能耐的人也会立马蔫了一半。其实大多数服务端项目的需求都相似,而且一旦固定下来就不会变更了,所以选择常规并符合业务模型的选项组件,将其封装好服务端和客户端调用框架,这样业务开发者就可以方便的用来做业务开发,因而整体说来是一件很有意义的事情。
  Thrift已经在项目中被广泛使用,总体来说性能不俗、稳定可靠,而且很多公司自己开发的RPC框架都主动兼容Thrift协议,说明这货使用还算挺广泛的,虽然之前也说到使用Thrift有些小坑需要注意,比如TSocket Timeout参数,服务端、客户端阻抗匹配等问题。还有就是Thrift是一个纯粹的RPC,功能相对比较单一,不像Zeroc-ICE除了做RPC外还集成了大量的服务治理的功能组件:服务发布和发现、负载均衡、配置更新和同步、服务自动尝试等,但是有时候想想,Do one thing and do it well不正是UNIX的哲学精髓么,ZeroC-ICE不是一时半会儿可以玩溜的,而这些服务治理的功能完全可以用自己熟悉的组件来实现对应功能。
  在之前的开源项目tzmonitor中已经使用Thrift了,基本按照目录结构和封装在其中的添加自己的业务逻辑代码就可以直接使用了,这里主要描述一下新建一个Thrift接口需要的步骤和流程。
apache-thrift

(a) 编写.thrift文件
  在thrifting/source中,可以任意添加.thrift文件,在这个文件中我们会常规的定义数据类型struct和服务接口service,相关的语法请直接参考Thrift相关文档;
(b) 生成语言绑定的相关支持代码
  我们在根目录的CMakeLists.txt中增加了如下代码:

1
cd ${PROJECT_SOURCE_DIR}/source/thrifting/source && rm -fr ../gen-cpp && mkdir -p ../gen-cpp && make

  其实就是在thrifting/source中执行make命令,在这个命令中我们会生成语言相关的绑定支持代码。目前项目重度使用C++,其实现在我在有些项目中也使用Java了,主要是诸如涉及到加解密等操作,需要调用底层的openssl,大家知道这种C库很容易导致程序挂起或者内存泄漏,相比而言Java对加解密和数据封装的操作安全简单,刚好Thrift是跨语言的,用它来耦合十分合适。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace cpp tz_thrift
namespace java net.taozj.devel.thrift

struct result_t {
1:required i32 code; // 0: 成功; <0:失败
2:required string desc;
}
struct ping_t {
1: required string msg;
}
......
service monitor_service {
result_t ping_test(1: ping_t req);
}

  其Makefile内容如下,执行完毕后会在thrifting/gen-cpp和thrifting/gen-java中生成相关辅助代码,因为我们的项目使用CMake进行管理,默认CMake会将所有.cpp代码都搜集起来,所以需要将skeleton的相关代码删除掉,否则会导致main重复定义,当然也可以在CMakelist.txt中排除相关的文件。

1
2
3
4
5
6
7
8
9
10
11
IDLS=$(wildcard *.thrift)
OBJS=$(IDLS:%.thrift=../gen-cpp/%.thrift.cc)
OBJS_JAVA=$(IDLS:%.thrift=../gen-java/%.thrift.class)
all: ${OBJS} ${OBJS_JAVA}
@echo "remove ../gen-cpp/*.skeleton.cpp files"
rm -fr ../gen-cpp/*.skeleton.cpp

../gen-cpp/%.thrift.cc: %.thrift
thrift -r -gen cpp -out $(@D) $^
../gen-java/%.thrift.class: %.thrift
thrift -r -gen java -out $(@D) $^

(c) 实现业务代码
  thrifting/biz中是真正实现业务逻辑的地方。前面在.thrift中定义的服务接口,都会在对应的serviceIf中被声明为虚函数,然后你需要填充业务代码来override这些虚函数。剩下还有介个辅助类型别名和辅助类,是方便使用创建的。
(d) 服务端和客户端辅助代码
  这些代码都在thrifting/helper当中,他们是不依赖于具体的业务和接口的,所以你无须任何改动,如果感兴趣可以阅读了解一下。这部分代码主要定义了数据封装的格式、传输细节,以及辅助快速创建TThreaded、TThreadPool、TNonblocking三种模式的服务端代码和创建客户端的代码。
  关于服务端的三种模式,TThreaded基本很少有人使用,而TThreadPool和TNonblocking,部分人会认为TNonblocking比较高端、性能应该也更好。其实这很难说,TNonblocking在扛高并发的多客户端请求,或者在网络环境比较差的情况下应该会有不错的表现,比如evernote用Thrift做开放API,但是在内部服务间通信的时候,并发数量有限、网络环境也很好,所以TNonblocking很难发挥出他的优势来,而且TNonblocking本身需要额外的数据结构维护连接收取的数据和连接的状态,需要额外单独的io thread负责处理网络通信,其额外开销应当反而比TThreadPool要大,所以我还是推荐内网环境使用TThreadPool模式的服务端的。各位有时间精力可以帮我做一下测试验证更好。
(e) 创建服务端例程
  创建Thrift服务端需要三个要素:ServiceHandler、ServiceProcessor和ServerHelperType(见TThriftServer.h),ServiceHandler是我们之前说到的继承serviceIf并实现我们定义的虚函数接口的那个类;ServiceProcessor我们可以直接使用系统产生的xxxx_serviceProcessor类,其主要帮助处理整个RPC调用过程中数据接收、调用对应的Handler、结果数据的返回发送的主流程框架;ServerHelperType是我们先前说明的那三种模型的服务端辅助类,这个我也都在thrifting/helper中实现了。注意启动服务端的时候当前线程会阻塞住,所以我们在封装的时候单独开辟了一个线程来启动每个ThriftServer实例,这样我们可以在一个服务中启动多个ThriftServer,同时主线程还可以做其他的事情。另外,刚好三种模型的服务端对参数需求不一样,所以我直接在TThriftServer中通过重载构造函数的方式自动创建底层响应类型的服务端,就没有单独弄出一个工厂方法做这个事情了。
  基本上,整个服务端的构建可以表述为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TProtocol - TBinaryProtocol
boost::shared_ptr<ProtocolFactory> protocolFactory
= boost::make_shared<ProtocolTypeFactory>();

// TTransport - TFramedTransport
boost::shared_ptr<transport::TTransportFactory> transportFactory
= boost::make_shared<TransportTypeFactory>();

// TProcessor with ServiceHandler
boost::shared_ptr<ServiceHandler> handler = boost::make_shared<ServiceHandler>();
boost::shared_ptr<TProcessor> processor = boost::make_shared<ServiceProcessor>(handler);

// TServer
boost::shared_ptr<transport::TServerTransport> tSocket
= boost::make_shared<transport::TServerSocket>(port_);
boost::shared_ptr<server::TThreadedServer> serverPtr
= boost::make_shared<server::TThreadedServer>(processor, tSocket, transportFactory, protocolFactory);

  客户端的使用需要包含TThriftClient.h,TThriftClient也是一个比较干净、不带任何业务代码的类,其中的成员函数都是模板函数,使用它的时候再额外包含业务头文件就可以实例化对应的客户端了。我们支持提供服务端IP:Port的方式调用,也支持先创建客户端,然后多次使用该客户端执行调用,主要是因为Thrift支持长连接机制,创建一个客户端然后进行多次调用应该会有性能方面的收益。
(f) Java语言的绑定
  在Java中使用Thrift发现更加的简单,我们通过thrift产生Java辅助文件后,将辅助文件打包成一个jar,同时Thrift源代码编译后还会产生一个libthrift-0.9.2.jar,通过这两个jar文件,然后再通过初始化Handler和Processor对象,就可以快速创建服务端实例了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ThreadPoolThriftServer {

public static XXXXHandler handler; // Iface Impl
public static xxxx_service.Processor processor;

public static void start_service() {
try {
handler = new XXXXHandler();
processor = new xxxx_service.Processor(handler);

Runnable service = new Runnable() {
public void run() {
threadpool_service(processor);
}
};
new Thread(service).start();
} catch (Exception x) {
x.printStackTrace();
}
}

private static void threadpool_service(pbi_channel_service.Processor processor) {
try {
TServerSocket socket = new TServerSocket(listen_port);
TThreadPoolServer.Args arg = new TThreadPoolServer.Args(socket);
arg.protocolFactory(new TBinaryProtocol.Factory());
arg.transportFactory(new TFramedTransport.Factory());
arg.processorFactory(new TProcessorFactory(processor));
arg.maxWorkerThreads(thread_num);

TThreadPoolServer server = new TThreadPoolServer(arg);
server.serve();

} catch (Exception e) {
e.printStackTrace();
}
}
}

  上面是使用样例代码,应为我对Java不是很熟悉,所以也没讲求什么优化、抽象,能跑起来就万岁了。不过对于一个C++程序员来说满屏的new却看不到一个delete着实让人心里发慌啊……

本文完!