[RPC] Thrift学习——跨语言分布式系统远程调用
1. Thrift是什么?
Thrift 是一个跨语言的远程服务调用框架,由 Facebook 开发并开源。它可以用来定义服务接口以及跨多种编程语言生成代码,使得不同语言的程序可以相互调用。Thrift 支持多种语言,包括 C++, Java, Python 等,使得不同语言的系统可以方便地进行通信和交互。
远程调用(Remote Procedure Call,RPC)是一种计算机通信协议,允许一个程序(客户端)通过网络请求另一个程序(服务器)的服务,就像调用本地函数一样。在分布式系统中,远程调用使得不同计算机上的程序能够相互协作,共享资源和功能。
Thrift 的核心概念是通过一个**接口描述语言(IDL)**来定义数据类型和服务接口,然后根据这个接口生成不同语言的代码,使得客户端和服务端可以通过生成的代码进行通信。它支持多种传输协议(如TCP、HTTP等)和序列化格式(如二进制、JSON等),可以根据具体需求选择合适的传输方式和数据格式。
Thrift 框架优点:
- 跨语言支持:Thrift 可以根据定义的接口描述语言(IDL)生成多种编程语言的代码,使得不同语言的系统可以轻松地进行通信和交互。这种跨语言支持极大地方便了分布式系统中多语言环境下的开发和部署。
- 高效的序列化:Thrift 提供了高效的二进制序列化协议(如Compact Protocol),可以将数据对象快速编码为字节流并进行传输,从而提高了数据传输的效率和性能。
- 多种传输协议支持:Thrift 支持多种传输协议,如原生 TCP、HTTP 等,开发者可以根据具体场景选择合适的传输方式,灵活性较高。
- 多种数据类型支持:Thrift 支持丰富的数据类型,包括基本类型、容器类型(如列表、集合、映射等)和结构体等,可以很方便地定义复杂的数据结构。
- 代码生成和维护:Thrift 自动生成的代码可以极大地减少手动编写重复代码的工作量,同时也有利于代码的维护和统一性。
- 可扩展性:Thrift 框架本身设计良好,具备良好的扩展性,可以通过插件和定制化实现更多特定需求,例如定制化的传输协议、序列化格式等。
- 开源社区支持:Thrift 是一个开源项目,拥有活跃的开发社区,可以获取到丰富的文档、示例和技术支持,有助于开发者更好地理解和使用该框架。
2. Thrift基础知识
2.1 IDL语法
基本类型:
布尔类型(bool)
:表示真(true)或假(false)
bool flag; # A boolean value, true or false(对应java的boolean)
整数类型
:有符号整型
byte b; # A signed byte(对应java的byte)
i16 i16_value; # A 16-bit signed integer(对应java的short)
i32 i32_value; # A 32-bit signed integer(对应java的int)
i64 i64_value; # A 64-bit signed integer(对应java的long)
浮点数类型(double)
:64位浮点数
double d; # A 64-bit floating point number(对应java的double)
字符串类型(string)
:表示一个文本字符串
string name; # An encoding-agnostic text or binary string(对应java的String)
要注意的是没有无符号整数类型。这些类型不能直接转换为许多语言中原生类型,因此它们提供的优势就丧失了。此外,在像Python这样的语言中,没有办法阻止应用程序开发人员将负值赋给整数变量,从而导致不可预测的行为。从设计的角度来看,我们注意到无符号整数很少(如果有的话)用于算术目的,但在实践中更多地用作键或标识符。在这种情况下,符号是无关的。有符号整数也有同样的用途,在绝对必要的时候可以安全地转换为无符号整数(在c++中最常见)。
结构体:
结构体struct
是一个复杂的数据类型,可以包含多个字段,每个字段可以是任意的 Thrift 类型。结构体在 Thrift 中用于定义复杂的数据结构。
struct声明语法:
struct <结构体名称> {
<序号>:[字段性质] <字段类型> <字段名称> [= <默认值>] [;|,]
}
required
关键字用于声明字段为必选字段,即在创建结构体实例时,必须为这些字段指定一个值。如果在实例化结构体时没有为required
字段赋值,Thrift 将会报错。optional
关键字用于声明字段为可选字段,即在创建结构体实例时可以选择性地为这些字段指定值。如果在实例化结构体时没有为optional
字段赋值,Thrift 不会报错,而是将该字段的值设为默认值(通常是 null 或相应类型的默认值)。- 成员编号需要使用正整数,且不能重复,为了传输过程编码使用
struct Person {
1: string name;
2: i32 age;
3: required string location; //location字段被声明为required,因此在创建Person结构体的实例时,必须为该字段提供值。
4: optional string city; //字段被声明为 optional,创建实例时该字段可赋值可不赋值,如果不赋值则设为相应类型的默认值(null)
5: optional string country = "Unknown"; // 字段被声明为 optional,创建实例时该字段可赋值可不赋值,如果不赋值则设为设置好的默认值
};
容器:
Thrift 还支持容器类型,可以包含多个元素,包括列表、集合和映射。
列表 (list<type>)
:有序集合,可以包含重复元素。
list<string> names;
集合(set<type>)
:无序集合,不包含重复元素。
set<i32> numbers;
映射 (map<type1, type2>)
:键值对的集合。
map<string, i32> ages;
常量:
在 Thrift 的IDL(接口定义语言)中,可以使用 const
关键字来定义常量。常量在 Thrift 中可以用于定义不变的数值或字符串,通常用于定义一些固定的配置值、枚举的数值等。
const i32 MAX_RETRY = 3;
const string DEFAULT_HOST = "localhost";
struct Configuration {
1: string host = DEFAULT_HOST;
2: i32 max_retry = MAX_RETRY;
};
常量在 Thrift 的IDL中是不可更改的,一旦定义后其值不可变。
常量可以使用各种支持的数据类型,包括整数、浮点数、布尔值、字符串等。
常量的命名通常遵循大写字母和下划线的命名风格,以提高可读性。
枚举
使用 enum
关键字来定义枚举类型。枚举类型允许列出一组命名常量,每个常量可以有一个可选的初始值。
枚举类型可以作为结构体(struct)的字段类型,用于表示结构体中的某个属性的状态或类型。
Thrift 不支持嵌套的枚举类型(也不支持嵌套的结构体),枚举常量的值必须是32位的正整数。
enum Status {
OK, // 默认值为 0
ERROR = 1, // 指定初始值为 1
WARNING // 自动分配下一个整数值,这里为 2
};
struct Book {
1: string title;
2: i32 year_published;
3: Status status;
};
注意:
初始值分配:如果在枚举常量中指定了初始值,后续的常量会自动分配比前一个常量大
1
的整数值。命名规范:通常建议使用大写字母命名枚举常量,以便与普通字段区分开来。
跨语言兼容性:Thrift 的枚举类型在生成代码时会根据目标语言的规范进行转换,因此在使用时应注意目标语言对枚举的支持和处理方式。
类型别名:
在 Thrift 的IDL(接口定义语言)中,可以使用 typedef
关键字来定义类型别名。类型别名允许为已有的数据类型定义一个新的名称,使得代码更加清晰和可维护。
typedef i32 UserId;
struct User {
1: UserId id;
2: string name;
3: i32 age;
};
typedef list<string> StringList;
struct Configuration {
1: StringList keywords;
2: map<string, string> settings;
};
异常:
在 Thrift 的IDL(接口定义语言)中,可以使用 exception
关键字来定义异常。异常在 Thrift 中用于描述可能在服务调用过程中发生的错误或异常情况,使得客户端能够捕获并处理这些异常情况。
使用 exception
关键字后面跟着异常名称和异常结构体的定义来定义异常。
exception NotFoundException {
1: string message; // 异常消息
2: i32 code; // 异常代码
};
可以在服务接口中声明可能抛出的异常,以便客户端可以根据具体的异常情况进行处理。
service FileService {
bool fileExists(1: string filename) throws (1: NotFoundException ex);
};
异常的定义与结构体类似,可以包含多个字段来描述异常的具体信息。
可以在服务接口的方法定义中使用
throws
关键字来声明方法可能抛出的异常类型。Thrift 生成的客户端代码通常会生成对应的异常类或结构体,以便在实际调用中捕获和处理异常。
服务:
在 Thrift 中,服务是指一组定义了可以远程调用的方法集合。这些方法可以被客户端调用,而服务端则实现这些方法并提供具体的功能。使用 service
关键字来定义一个服务。服务定义了一组可以远程调用的方法。
服务中每个方法的定义包括方法的返回类型、方法名、参数列表和可能抛出的异常列表(可选)。
- 返回类型:方法的返回值类型。
- 方法名:方法的名称,用于标识和调用方法。
- 参数列表:方法接收的参数及其类型。
- 异常列表:方法可能抛出的异常类型。
语法:
service <name> {
<returntype> <name>(<arguments>)
[throws (<exceptions>)]
...
}
举例:
service CalculatorService {
i32 add(1: i32 a, 2: i32 b),
i32 subtract(1: i32 a, 2: i32 b),
i32 multiply(1: i32 a, 2: i32 b),
double divide(1: double a, 2: double b) throws (1: InvalidOperation ex);
};
Thrift 的服务定义只是方法的声明,不包括具体的实现。实现服务需要编写具体的服务处理器(Processor)和服务实现类,服务处理器负责将客户端的请求分派给实现类中的对应方法,并将结果返回给客户端。
使用 Thrift 自动生成的客户端和服务端代码,客户端可以通过远程调用来访问服务端提供的方法。Thrift 提供了多种传输协议(如TCP、HTTP等)和序列化格式(如二进制、JSON等),使得客户端和服务端能够在不同平台和语言之间进行通信和数据交换。
Thrift 的服务概念支持跨语言和跨平台的特性,服务的接口定义可以在不同的编程语言中生成相应的客户端和服务端代码,使得不同语言的系统能够互相调用和交互。
2.2 传输层
在 Apache Thrift 中,传输层负责管理客户端和服务端之间的通信传输。Thrift 提供了多种传输层协议和选项,以便在不同的网络环境和应用需求下进行选择和配置。
2.2.1 传输方式
在使用 Thrift 时,可以根据具体的需求配置传输层的选项。
- TSocket:基于 TCP 的传输方式,用于传输二进制数据。
- THttpClient:基于 HTTP 的传输方式,允许通过 HTTP POST 请求进行通信。
- TZlibTransport:基于 zlib 的压缩传输方式,用于压缩数据以减少带宽消耗。
- TFileTransport:文件传输方式,用于本地文件系统操作。
2.2.1 传输协议
Thrift 提供了多种传输协议,用于在客户端和服务端之间传输数据。每种传输协议都有其特定的优点和适用场景。
- TBinaryProtocol:二进制协议,效率高,适合传输二进制数据。
特点:
- 二进制协议,将数据序列化为紧凑的二进制格式。
- 效率高,序列化和反序列化速度快,适合在性能要求较高的环境中使用。
- 支持所有 Thrift 数据类型,包括基本数据类型、结构体、列表、映射、异常等。
适用场景:
- 内部服务之间的高性能通信。
- 需要最小化数据传输量的场景。
- TCompactProtocol:紧凑协议,数据压缩,适合在网络带宽有限的环境中使用。
特点:
- 紧凑协议,对数据进行压缩,减少了传输时的数据量。
- 比
TBinaryProtocol
更加节省带宽。- 支持所有 Thrift 数据类型,与
TBinaryProtocol
兼容。适用场景:
- 网络带宽有限的环境,如移动网络或低速网络。
- 需要减少数据传输量的应用场景。
- TJSONProtocol:JSON 格式的协议,可读性好,适合调试和与其他系统集成。
特点:
- JSON 格式的协议,数据以易于理解和交换的 JSON 格式进行序列化和反序列化。
- 可读性强,便于调试和与其他系统集成。
- 支持所有 Thrift 数据类型。
适用场景:
- 跨平台、跨语言的数据交换,与非 Thrift 系统集成。
- 需要使用 JSON 格式进行数据交换的场景。
- TSimpleJSONProtocol:简化的 JSON 协议,可读性更强,但不支持所有 Thrift 数据类型。
特点:
- 简化的 JSON 协议,相对于
TJSONProtocol
更加简洁。- 不支持所有 Thrift 数据类型,但支持基本数据类型和部分复杂数据类型。
- 适用于基本数据类型的简单数据交换场景。
适用场景:
- 需要使用 JSON 进行数据交换,但对性能要求不高的场景。
- TDebugProtocol:调试协议,生成易于阅读的文本格式,用于调试和诊断。
特点:
- 调试协议,生成易于阅读和理解的文本格式。
- 主要用于开发和调试阶段,用于观察和检查数据传输的细节。
- 不适合用于生产环境,由于其文本格式可能会影响性能。
适用场景:
- 开发和测试阶段的数据交换和调试。
除了以上列出的协议外,Thrift 还允许用户通过扩展 TProtocol
类来实现自定义的协议。这种灵活性使得用户可以根据具体的需求和环境,设计和优化适合特定场景的数据传输协议。
service FileService {
string readFile(1: string filename) throws (1: FileException ex);
};
// 在服务端配置传输层
struct FileServiceHandler : FileService {
string readFile(1: string filename) {
// TODO 实现文件读取逻辑
}
};
// 在服务端设置传输层和协议
int main() {
TThreadedServer server(
std::make_shared<FileServiceProcessor>(
std::make_shared<FileServiceHandler>()),
std::make_shared<TServerSocket>(9090),
std::make_shared<TBufferedTransportFactory>(),
std::make_shared<TBinaryProtocolFactory>()
);
server.serve();
return 0;
}
2.3 服务端类型
在 Apache Thrift 中,有多种类型的服务端可以选择,每种类型都适用于不同的使用场景和需求。
以下是 Thrift 中常见的几种服务端类型:
- TSimpleServer:单线程服务端,使用阻塞式I/O
特点:
- 最简单的服务端实现。
- 每次请求都会创建一个新的线程来处理,适合低并发、小规模的应用场景。
- 使用单线程或者多线程来处理客户端请求。
适用场景:
- 对并发要求不高,或者并发量较小的应用场景。
- 快速实现和调试阶段。
- TThreadedServer:多线程服务端,使用阻塞式I/O
特点:
- 多线程服务端,为每个客户端连接创建一个新的线程来处理请求,提高并发处理能力。
- 每个连接都独立于其他连接,互不影响。
- 管理线程池,用于处理客户端的请求。
适用场景:
- 高并发的应用场景,可以同时处理多个客户端请求。
- 需要充分利用多核 CPU 的应用场景。
- TThreadPoolServer:多线程服务端,使用阻塞式I/O
特点:
- 线程池服务端,预先创建一组线程池来处理客户端请求。
- 控制线程数量,防止线程过多导致资源浪费。
- 每个请求分配一个空闲线程来处理,处理完毕后线程返回线程池等待下一个请求。
适用场景:
- 高并发且长时间处理请求的应用场景。
- 需要灵活控制线程数量和资源使用的场景。
- TNonblockingServer:多线程服务端,使用非阻塞式I/O
特点:
- 非阻塞服务端,采用异步 IO 模型处理客户端请求。
- 使用少量的线程来处理大量的并发连接,提高系统的资源利用率。
- 适合高并发、高吞吐量的网络应用场景。
适用场景:
- 需要处理大量并发连接和请求的网络应用。
- 对系统资源(如 CPU 和内存)有较高的利用要求。
- 自定义服务端
特点:
- Thrift 提供了灵活的接口和扩展点,允许开发者根据具体需求自定义服务端的实现。
- 可以根据特定的业务逻辑和性能需求,实现定制化的服务端行为和处理逻辑。
- 通过继承和实现 Thrift 的相关接口,可以实现特定的服务端行为和性能优化。
3. Thrift安装
官网:https://thrift.apache.org/download
3.1 Windows版安装
下载exe文件
下载好的exe文件改名为thrift.exe,放在某一目录下(后续配置环境变量使用)
配置环境变量
验证
3.2 Linux版安装
下载源码包:
tar -zxvf thrift-0.20.0.tar.gz
解压:
tar -zxvf thrift-0.20.0.tar.gz
安装相关依赖库:
yum -y install automake libtool flex bison pkgconfig gcc-c++ boost-devel libevent-devel zlib-devel Python-devel ruby-devel crypto-utils
cd进入根目录,进行配置:
cd thrift-0.20.0
./configure --with-cpp --with-boost --with-python --without-csharp --with-java --without-erlang --without-perl --with-php --without-php_extension --without-ruby --without-haskell --without-go
开始编译:
make
make install
3.3 Mac版安装
brew install thrift
4. QuickStart(Java)
4.0 添加相关pom依赖
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.20.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version> <!-- 替换为最新版本 -->
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version> <!-- 替换为最新版本 -->
</dependency>
4.1 定义IDL文件
定义IDL文件ThriftService.thrift
namespace java com.cxj.thrift
service ThriftService {
string hello(1: string name)
i32 add(1: i32 param1, 2: i32 param2)
}
4.2 生产服务接口文件
thrift --gen <language> <Thrift filename>
执行命令
thrift --gen java ThriftService.thrift
生成ThriftService.java文件
public class ThriftService {
public interface Iface {
public java.lang.String hello(java.lang.String name) throws org.apache.thrift.TException;
public int add(int param1, int param2) throws org.apache.thrift.TException;
}
public interface AsyncIface {
public void hello(java.lang.String name, org.apache.thrift.async.AsyncMethodCallback<java.lang.String> resultHandler) throws org.apache.thrift.TException;
public void add(int param1, int param2, org.apache.thrift.async.AsyncMethodCallback<java.lang.Integer> resultHandler) throws org.apache.thrift.TException;
}
public static class Client extends org.apache.thrift.TServiceClient implements Iface {
public static class Factory implements org.apache.thrift.TServiceClientFactory<Client> {
public Factory() {}
@Override
public Client getClient(org.apache.thrift.protocol.TProtocol prot) {
return new Client(prot);
}
@Override
public Client getClient(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) {
return new Client(iprot, oprot);
}
}
public Client(org.apache.thrift.protocol.TProtocol prot)
{
super(prot, prot);
}
public Client(org.apache.thrift.protocol.TProtocol iprot, org.apache.thrift.protocol.TProtocol oprot) {
super(iprot, oprot);
}
@Override
public java.lang.String hello(java.lang.String name) throws org.apache.thrift.TException
{
send_hello(name);
return recv_hello();
}
public void send_hello(java.lang.String name) throws org.apache.thrift.TException
{
hello_args args = new hello_args();
args.setName(name);
sendBase("hello", args);
}
public java.lang.String recv_hello() throws org.apache.thrift.TException
{
hello_result result = new hello_result();
receiveBase(result, "hello");
if (result.isSetSuccess()) {
return result.success;
}
throw new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.MISSING_RESULT, "hello failed: unknown result");
}
@Override
public int add(int param1, int param2) throws org.apache.thrift.TException
{
send_add(param1, param2);
return recv_add();
}
public void send_add(int param1, int param2) throws org.apache.thrift.TException
{
add_args args = new add_args();
args.setParam1(param1);
args.setParam2(param2);
sendBase("add", args);
}
public int recv_add() throws org.apache.thrift.TException
{
add_result result = new add_result();
receiveBase(result, "add");
if (result.isSetSuccess()) {
return result.success;
}
throw new org.apache.thrift.TApplicationException(org.apache.thrift.TApplicationException.MISSING_RESULT, "add failed: unknown result");
}
}
4.3 服务端实现接口
package com.cxj.thrift;
import org.apache.thrift.TException;
public class ThriftServiceImpl implements ThriftService.Iface{
@Override
public String hello(String name) throws TException {
return "Hello " + name + "!";
}
@Override
public int add(int param1, int param2) throws TException {
return param1 + param2;
}
}
4.4 编写服务端Server
启动服务端,等待客户端调用
public class ThriftServer {
public static final int SERVER_PORT = 8090;
public static final String SERVER_IP = "localhost";
public static final int TIMEOUT = 1000;
public static void main(String[] args) {
try {
//1. 创建Transport(传输层)
TServerSocket transport = new TServerSocket(SERVER_PORT);
TServer.Args tArgs = new TServer.Args(transport);
//2. 为Transport创建Protocol
tArgs.protocolFactory(new TBinaryProtocol.Factory());
//3. 为Protocol创建Processor
TProcessor tProcessor = new ThriftService.Processor<ThriftService.Iface>(new ThriftServiceImpl());
tArgs.processor(tProcessor);
//4. 创建Server并启动
TServer tServer = new TSimpleServer(tArgs);
System.out.println("Thrift Server Starts...");
tServer.serve();
} catch (TTransportException e) {
e.printStackTrace();
}
}
}
4.4 客户端调用接口
public class ThriftClient {
public static final int SERVER_PORT = 8090;
public static final String SERVER_IP = "localhost";
public static final int TIMEOUT = 1000;
public static void main(String[] args) {
TTransport transport = null;
try {
//1. 创建TTransport(传输层)
transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
//2. 创建TProtocol(协议和服务端要保持一致)
TProtocol protocol= new TBinaryProtocol(transport);
//3. 创建客户端
ThriftService.Client client = new ThriftService.Client(protocol);
//4. 打开TTransport
transport.open();
//5. 调用服务方法
System.out.println(client.hello("Chandler"));
System.out.println(client.add(100, 200));
} catch (TTransportException e) {
e.printStackTrace();
} catch (TException e) {
e.printStackTrace();
} finally {
if(null != transport){
transport.close();
}
}
}
}
参考资料
https://thrift.apache.org/