[TOC]
一、介绍
Thrift 架构Thrift 源于 Facebook,在 2007 年 Facebook 将 Thrift 作为一个开源项目提交给 Apache 基金会。对于当时的 Facebook 来说,创造 Thrift 是为了解决 Facebook 各系统间大数据量的传输通信以及系统之间语言环境不同需要跨平台的特性,因此 Thrift 可以支持多种程序语言,如 C++、Cocoa、Erlang、Haskell、Java、Ocami、Perl、PHP、Python、Ruby 和 Smalltalk。
在多种不同的语言之间通信,Thrift 可以作为高性能的通信中间件使用,它支持数据(对象)序列化和多种类型的 RPC 服务。Thrift 适用于静态的数据交换,需要先确定好它的数据结构,当数据结构发生变化时,必须重新编辑 IDL 文件,生成代码和编译,这一点跟其他 IDL 工具相比可以视为是 Thrift 的弱项。Thrift 适用于搭建大型数据交换及存储的通用工具,对于大型系统中的内部数据传输,相对于 JSON 和 XML 在性能和传输大小上都有明显的优势。
Thrift 主要由 5 部分组成:
- 语言系统以及 IDL 编译器:负责由用户给定的 IDL 文件生成相应语言的接口代码;
- TProtocol:RPC 的协议层,可以选择多种不同的对象序列化方式,如 JSON 和 Binary;
- TTransport:RPC 的传输层,同样可以选择不同的传输层实现,如 socket、NIO、MemoryBuffer 等;
- TProcessor:作为协议层和用户提供的服务实现之间的纽带,负责调用服务实现的接口;
- TServer:聚合 TProtocol、TTransport 和 TProcessor 等对象。
需要重点关注的是编解码框架,与之对应的就是 TProtocol。由于 Thrift 的 RPC 服务调用和编解码框架绑定在一起,所以,通常我们使用 Thrift 的时候会采取 RPC 框架的方式。但是,它的 TProtocol 编解码框架还是可以以类库的方式独立使用的。
与 Protobuf 比较类似的是,Thrift 通过 IDL 描述接口和数据结构定义,它支持 8 种 Java 基本类型、Map、Set 和 List,支持可选和必选定义,功能非常强大。因为可以定义数据结构中字段的顺序,所以它也可以支持协议的前向兼容。
Thrift 支持三种比较典型的编解码方式。
- 通用的二进制编解码
- 压缩二进制编解码
- 优化的可选字段压缩编解码
由于支持二进制压缩编解码,Thrift 的编解码性能表现也相当优异,远远超过 Java 序列化和 RMI 等。
——摘自《RPC 协议之争和选型要点》
二、实战
(一)目标效果描述
本文实现一个简单的计算器功能,传入两个参数,实现加减乘除。在返回值中,将传入的参数、所做操作和计算结果返回。服务端使用 Java 实现,客户端使用 Node.js 实现。
(二)步骤
1、生成 IDL
CalculatorService.thrift
:
namespace java io.mochasoft.rpc.thrift
enum CalcMethod {
ADD,
SUBTRACT,
MULTIPLY,
DIVISION
}
struct CalcResult {
1: double arg1;
2: double arg2;
3: CalcMethod calcMethod;
4: double result;
}
exception DivByZeroException {
1: double arg1;
2: double arg2;
3: string reason;
}
service CalculatorService {
CalcResult add(1: double arg1, 2: double arg2)
CalcResult sub(1: double arg1, 2: double arg2)
CalcResult multi(1: double arg1, 2: double arg2)
CalcResult div(1: double arg1, 2: double arg2) throws (1: DivByZeroException dze)
}
其中加、减、乘操作差不多,除法在除数为 0
的时候,抛出 DivByZeroException
异常。
2、安装 Thrift 编译工具
macOS 上
$ brew install thrift
3、生成代码
Java:
$ thrift --gen java CalculatorService.thrift
Node.js:
$ thrift --gen js:node CalculatorService.thrift
4、Java 实现服务端
(1)引用库
// https://mvnrepository.com/artifact/org.apache.thrift/libthrift
compile group: 'org.apache.thrift', name: 'libthrift', version: '0.13.0'
(2)功能实现
先将根据 IDL 生成的 Java 代码加入 Java 工程,然后实现 CalculatorService.Iface
接口:
public class CalculatorServiceHandler implements CalculatorService.Iface {
// 只演示抛出异常的除法,其余方法类似
@Override
public CalcResult div(double arg1, double arg2) throws DivByZeroException {
CalcResult resultInstance = new CalcResult();
resultInstance.setArg1(arg1);
resultInstance.setArg2(arg2);
resultInstance.setCalcMethod(CalcMethod.DIVISION);
if (arg2 == 0) {
DivByZeroException exception = new DivByZeroException(arg1, arg2, "arg2 is 0");
System.out.println(exception);
throw exception;
}
resultInstance.setResult(arg1 / arg2);
return resultInstance;
}
}
(3)启动服务
public static void runServer() {
try {
CalculatorService.Processor<CalculatorServiceHandler> processor = new CalculatorService.Processor<>(new CalculatorServiceHandler());
TServerTransport serverTransport = new TServerSocket(9090);
TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor));
server.serve();
} catch (Exception e) {
e.printStackTrace();
}
}
5、Node.js 实现客户端
(1)引用库
$ npm install thrift-hbase-client
(2)实现客户端调用
先将根据 IDL 生成的 Node.js 代码复制到 Node.js 工程,然后实现客户端:
// 加载库
let thrift = require('thrift');
let CalcService = require('./calc-service/CalculatorService');
let ttypes = require('./calc-service/CalculatorService_types');
// 初始化连接参数
let connection = thrift.createConnection("localhost", 9090);
let client = thrift.createClient(CalcService, connection);
// 开始连接
connection.on('error', function(err) {
console.error(err);
});
// 调用方法:div 除法
client.div(8, 2, function(err, data) {
console.log('8/2===================');
if (err == null) {
data.calcMethod = Object.keys(ttypes.CalcMethod)[data.calcMethod];
console.log(data);
connection.end();
} else {
console.log(err);
connection.end();
}
});
三、遗留问题
1、适用场景
对于 Thrift 这样的 RPC 框架,对于理想状态,也就是功能明确、接口稳定的情况,能够简化接口约定及开发过程。但是对于接口变动频繁的情况,反而会使开发更加繁琐。
更加适用于内网服务间的调用。
2、安全及认证
无法像 HTTP 的接口那样引入类似 OAuth 这样的认证鉴权框架,所以一般需要通过网络策略对访问方进行限制。
由于不具备 Cookie、Session 之类的机制,所以对于需要根据访问者身份进行的调用,要将这些信息作为参数传入。
3、异常处理
Thrift 框架提供了异常的定义和处理机制,但是对于上面的例子,发现抛出的自定义异常,无法完整返回到客户端,所以框架提供的异常处理机制有限。有需求的话,可能需要自行修改源码来对实现异常机制。
四、坑
1、序列化
按照 Thrift 官方的介绍及工作原理描述,Thrift 不仅可以用作 RPC 框架,还可以作为一套跨语言的序列化框架来使用。在我们的项目中,由于觉得 RPC 相对不够灵活,所以想只采用 Thrift 作为序列化工具,以实现跨语言的序列化,然后通过 HTTP 协议来做传输。后期在需要 RPC 的场景,再继续用 Thrift 进行 RPC 通信。所谓“理想很丰满,现实很骨感”。经验证,理论上的确是没有问题的,对于 Client 和 Server 都使用 Java 来实现的情况来看,的确是可以正常运行的,这可能是由于 Java 使用的广泛度,导致了对 Java 相关的实现完善度比较高。但是在结合 Node.js 和基于浏览器的 JS 来实现的过程中,出现了各种问题。这里记录一下遇到的问题。
(1)TJSONProtocol
和 TSimpleProtocol
由于我们希望通过 HTTP 进行数据传输,所以希望使用 JSON 格式,也就理所当然想到了 TJSONProtocol
和 TSimpleProtocol
。
先说 TSimpleJSONProtocol
,它可以将对象序列化成 JSON,并且带有字段名,看起来很自然。但实际上这个协议只是用于调试,只实现了序列化,用于查看信息的正确性,无法实现反序列化。 如果要实现完整的序列化/反序列化过程,还是需要 TJSONProtocol
。
但是在实际使用的过程中发现, TJSONProtocol
虽然做了完整实现,但是其“输出带字段 JSON”的选项也只是为了查看,要做完成的序列化/反序列化操作,是不能采用这个选项的。 TJSONProtocol
的序列化实现是按照 IDL 中定义的序号来完成的。这对于整个过程没有影响。但是 一定要注意,这里面的 JSON 并不是我们臆想中的 JSON 格式,并不是那么有利于调试。
(2)TBufferedTransport
和 TFramedTransport
选定了合适的协议之后,内容的序列化总要存储到一个地方才能进行后续传输。对于采用 HTTP 的场景,将序列化的内容保存到内存中是比较合适的选择,所以就想到了 TBufferedTransport
。在 Java 的实现中,这部分是没有问题的。但是在 Node.js 的实现中,不知道为什么,通过 TBufferedTransport
来存储的内容,在做反序列化的时候,总是不能被完整读取。而必须要采用 TFramedTransport
来进行存储,然后在使用时需要删掉前四个字节。详情参见《带有nodejs示例的Apache Thrift》。
虽然在这里得到了解决方案,但是这个方案是非官方的,我们并不确定整个框架中还有多少这样需要自行修补的问题。另外,对于基于 Web 的相关实现,更加不完整。
2、线程安全
根据官方的文档,Thrift Client 在多线程的情况,会存在线程安全问题。这个我们没有做细致测试,不再详述。需要的话自行查阅相关文档。
总之,Thrift 作为跨语言的 RPC 框架具备其先进性,官方的 Demo 也更加突出 RPC 部分。理论上 RPC 是基于序列化的,所以其序列化功能应该是没问题的。但这个过程使用的协议及传输,官方并没有明确承诺其打散使用的可靠性。对于 RPC 部分,线程安全问题也值得商榷。
所以我们试用后的感受:如果对于跨语言的使用场景,Thrift 具备一定的优势,而且也比较轻便。但是也给出两点建议:
(1)Thrift 看起来不错,但基于多语言的使用场景,不是每种语言的实现都一样的完善,所以在确认选型前,一定需要深入测试,确保没有问题;对于非跨语言的场景,还是可以充分考察是否有更加成熟、稳定的框架可以使用;
(2)对于只需要使用序列化的场景,只要不是需要太多的语言支持,只是常用语言,可以尝试 Google 的 Protocol Buffer,成熟、稳定且文档丰富。其 IDL 也提供更加丰富的基于语法解析的扩展机制。
五、参考资料
(完)
网友评论