哔哩哔哩:分布式系统间通信最优方案——RPC&Thrift16集完整教程
官网:https://thrift.apache.org/
如果是纯 Java 场景,那就没有使用 Thrift 的必要了,直接使用 Openfeign 即可;但如果是多语言之间的调用,可以使用 Thrift 来进行跨语言 RPC 调用。
Thrift 的使用案例:
1.编写 IDL ;
service HelloService{
string sayHello(1: string word);
}
2.编译:
有两种选择,使用thrift官方提供的编译器:http://thrift.apache.org/docs/install,也可以使用maven的编译插件等;
3.打成 jar 包
将编译后生成的HelloService打一个jar包,这一步的作用是将这个jar包同时提供给服务端和客户端使用,作为他们的处理层,通过这种方式,保证了服务端和客户端在使用同一个接口时,schema的一致性
4.实现接口
作为服务端,我们要实现BoardService提供的Iface接口,在这里我们实现我们的功能:
import org.apache.thrift.TException;
public class HelloServiceImpl implements HelloService.Iface {
@Override
public String sayHello(String word) throws TException {
return String.format("%s,Hello!", word);
}
}
5.实现服务端的IO
接下来,我们要实现一个简单的服务器,来实现io操作:
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import java.io.IOException;
import java.net.ServerSocket;
public class HelloServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);// 监听9000端口
TServerSocket tServerSocket = new TServerSocket(serverSocket);
HelloService.Processor<HelloService.Iface> processor =
new HelloService.Processor<>(new HelloServiceImpl());
TSimpleServer.Args tArgs = new TSimpleServer.Args(tServerSocket);
tArgs.processor(processor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
TSimpleServer server = new TSimpleServer(tArgs);
System.out.println("server started");
server.serve();
}
}
6.实现客户端
最后,我们实现一个简单的客户端:
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class HelloClient {
public static void main(String[] args) {
TTransport transport = null;
try {
transport = new TSocket("127.0.0.1", 9000, 100);
TProtocol protocol = new TBinaryProtocol(transport);
HelloService.Client client = new HelloService.Client(protocol);
transport.open();
String result = client.sayHello("Thrift");
System.out.println("Result =: " + result);
} catch (TException e) {
e.printStackTrace();
} finally {
if (null != transport) {
transport.close();
}
}
}
}
7.启动服务端和客户端
看一下效果:
服务端启动了:
客户端调用服务端接口并打印了结果:
1.Thrift介绍
1.1 Thrift 的简介
Thrift 是一种轻量级的、跨语言的CS架构的RPC框架,主要用于服务之间的RPC通信,最初由 Facebook 于2007年开发,2008年进入 Apache开源项目。它通过自身的 IDL (Interface Description Langrauge接口描述语言),并借助代码生成引擎生成各种主流语言的 RPC 服务的/ 客户端模板代码。Thrift支持多种不同的变成语言,包括 C++, java, Python, Ruby, Erlang等。
RPC,全程 Remote Procedure Call —— 远程过程访问。为了解决远程调用服务的一种技术,使得使用者像调用本地服务一样。这里主要从Java 语言来进行学习。
1.2 Thrift 的架构
Thrift 技术栈分层从下向上分别为:传输层 、协议层、处理层、服务层。
传输层(Transport Layer):负责直接从网络中读取和写入数据,定义了具体的网络传输协议,如TCP/IP
协议层(Protocol Layer):定义了数据传输格式,负责网络传输数据的序列化和反序列化(解析),如JSON, xml, 二进制数据等。
处理层(Processor Layer):由具体的 IDL (接口描述语言) 生成的,封装了具体的底层网络传输和序列化方式,并委托给用户实现 Handler 进行处理。
服务层(Server Layer):整合上述组件,提供具体的网络 IO 模型(单线程/多线程/ 事件驱动),形成最终的服务。
若采用TCP/IP 作为底层的通信协议,整个通信过程如下图所示:
1.3 Thrift 的特性和优势
1.3.1 开发速度快
(1)通过编写 RPC接口的 Thrift IDL 文件,通过编译生成器自动生成服务端骨架(Skeletors) 和客户端的桩(Stubs),从而省去开发者自定义和维护接口编解码、消息传输、服务器多线程模型等基础工作。
(2)服务端:只需要按照服务骨架(即接口),编写和具体的业务处理程序(Handler,即实现类)即可。
(3)客户端:只需要拷贝 IDL 定义好的客户端桩和服务对象,然后就像调用本地方法一样调用远程服务。
1.3.2 接口维护简单
通过维护 Thrift 格式的IDL文件,即可作为给 Client 使用的接口文档使用,也自动生成接口代码,始终保持代码和文档的一致性。
1.3.3 多语言支持
OpenFeign 不支持跨语言,Thrift 支持跨语言。
- IDL 语法
2.1 数据类型
2.1.1 基本类型
主要有如下7种:
Type Desc Java
i8 有符号8位整数 byte
i16 有符号16位整数 float
i32 有符号32位整数 int
i64 有符号64位整数 long
double 64位浮点数 double
bool 布尔值 boolean
string 文本字符串(UTF-8编码格式) java.lang.String
2.1.2 特殊类型
binary:未编码的字节序列,是string 的一种特殊形式;这种类型主要是方便某些场景下 Java 调用。对应的是 java.nio.Bytebuffer 类型,Go中是[]byte。
2.2 集合容器(Containers)
三种:list, set, map<K,V>
在使用容器类型时,必须指定泛型,否则无法编译 idl 文件。其次,泛型中的基本类型,java 语言都会被替换成对应的包装类型。集合中的元素可以是除service之外的任何类型,包括 exception。
struct Test{
1: map<string, User> usermap,
2: set<i32> intset,
3: list<double> doublelist
}
2.3 常量及类型别名(const & Typedef)
// 常量定义
const i32 MALE_INT = 1
const map<i32, string> GENDER_MAP = {1: "male", 2: "female"}
// 某些数据类型比较长可以用别名简化
typedef map<i32, string> gmp
2.4 结构体 (struct)
结构体的定义规则如下:
struct <结构体名称> {
<序号>: [字段性质] <字段类型> <字段名称> [= <默认值>] [;|,]
}
struct User{
1: required string name, // 该字段必须填写
2: optional i32 age = 0; // 默认值
3: bool gender // 默认字段类型为 optional
}
struct bean{
1: i32 number = 10,
2: i64 bigNumber,
3: double decimals,
4: string name = "thrifty"
}
struct 有以下一些约束:
1.struct 不能继承,但是可以嵌套,不能嵌套自己;
2.其他成员都是有明确类型;
3.成员是被正整数编号过的,其中的编号不能重复,这个是为了在传输过程中编码使用;
4.成员分隔符可以是逗号 , 或者是分号 ; 而且可以混用;
5.字段会有optional 和 required 之分,和protobuf 一样,但是如果不指定则为无类型,可以不填充该值,但是在序列化传输的时候也会序列化进去,optional 是不填充则不序列化,required 是必须填充也必须序列化;
6.每个字段可以设置默认值;
7.同一文件可以定义多个 struct,也可以定义在不同的文件,进行 include 引入。
2.5 枚举(enum)
Thrift 不支持枚举类嵌套,枚举常量必须是 32 位的正整数。
enum HttpStatus{
OK = 200,
NOTFOUND = 404
}
2.6 异常(Exceptions)
异常在语法和功能上类似于结构体,差别是异常使用关键字 exception,而且异常是继承每种语言的基础异常类。
exception MyException {
1: i32 errorCode,
2: string message
}
service ExampleService {
string GetName() throws (1: MyException e)
}
2.7 Service (服务定义类型)
服务定义方法在语义上等同于面向对象语言中的接口。
service HelloService {
i32 sayInt(1:i32 param),
string sayString(1:string param),
bool sayBoolean(1:bool param),
void sayVoid();
}
2.8 NameSpace(命名空间)
类似于C++中的namespace 和 java 中的 package,它们提供了一种组织隔离代码的简便方式。由于每种语言都有自己的命名空间定义方式(如python 有 module),Thrift 允许开发者针对特定语言定义 namespace。
namespace java com.example.test
转化为
package com.example.test
2.9 Comment (注释)
Thrift 支持 C 多行风格和 Java/C++ 单行风格。
多行注释: /** */
单行注释: //
2.10 Include
include "test.thrift"
3.编译安装
thrift编译器的安装
参考文档:https://thrift.apache.org/docs/install/
windows 安装
下载地址:https://thrift.apache.org/download
centos 安装
参考文档:https://thrift.apache.org/docs/install/centos.html
安装好后,配置好环境变量,测试安装是否成功:
#可以通过以下命令查看生成命令的格式
thrift -help
创建Thrift IDL文件
namespace java com.test
namespace py com.test
struct User{
1:i32 id
2:string name
3:i32 age=0
}
service UserService {
User getById(1:i32 id)
bool isExist(1:string name)
}
通过编译器编译user.thrift文件,生成java接口类文件
编译user.thrift
thrift -gen java user.thrift
生成User.java、UserService.java
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.15.0)", date = "2022-01-14")
public class User implements org.apache.thrift.TBase<User, User._Fields>, java.io.Serializable, Cloneable, Comparable<User> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("User");
private static final org.apache.thrift.protocol.TField ID_FIELD_DESC = new org.apache.thrift.protocol.TField("id", org.apache.thrift.protocol.TType.I32, (short)1);
private static final org.apache.thrift.protocol.TField NAME_FIELD_DESC = new org.apache.thrift.protocol.TField("name", org.apache.thrift.protocol.TType.STRING, (short)2);
private static final org.apache.thrift.protocol.TField AGE_FIELD_DESC = new org.apache.thrift.protocol.TField("age", org.apache.thrift.protocol.TType.I32, (short)3);
private static final org.apache.thrift.scheme.SchemeFactory STANDARD_SCHEME_FACTORY = new UserStandardSchemeFactory();
private static final org.apache.thrift.scheme.SchemeFactory TUPLE_SCHEME_FACTORY = new UserTupleSchemeFactory();
...
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.15.0)", date = "2022-01-14")
public class UserService {
public interface Iface {
public User getById(int id) throws org.apache.thrift.TException;
public boolean isExist(java.lang.String name) throws org.apache.thrift.TException;
}
...
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.15.0</version>
</dependency>
实现UserServiceService.Iface的定义方法
将生成类的.java源文件拷贝进项目源文件目录中,并实现UserServiceService.Iface的定义的getById()方法。
package com.test.service;
import org.apache.thrift.TException;
import com.test.User;
import com.test.UserService;
public class UserServiceImpl implements UserService.Iface {
@Override
public User getById(int id) throws TException {
System.out.println("=====调用getById=====");
//todo 模拟业务调用
User user = new User();
user.setId(id);
user.setName("fox");
user.setAge(30);
return user;
}
@Override
public boolean isExist(String name) throws TException {
return false;
}
}
Java 服务器端程序编写
public class SimpleService {
public static void main(String[] args) {
try {
TServerTransport serverTransport = new TServerSocket(9090);
//获取processor
UserService.Processor processor = new UserService.Processor(new UserServiceImpl());
//指定TBinaryProtocol
TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
TSimpleServer.Args targs = new TSimpleServer.Args(serverTransport);
targs.processor(processor);
targs.protocolFactory(protocolFactory);
//单线程服务模型,一般用于测试
TServer server = new TSimpleServer(targs);
System.out.println("Starting the simple server...");
//暴露服务
server.serve();
} catch (Exception e) {
e.printStackTrace();
}
}
}
python 客户端代码编写
通过编译器编译user.thrift文件,生成python代码然后将生成的 python 代码 和 文件,放到新建的 python 项目中
thrift -gen py user.thrift
python中使用thrift需要安装thrift模块
pip install thrift
创建python客户端程序
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from com.tuling import UserService
Make socket
transport = TSocket.TSocket('localhost', 9090)
transport.setTimeout(600)
Buffering is critical. Raw sockets are very slow
transport = TTransport.TBufferedTransport(transport)
Wrap in a protocol
protocol = TBinaryProtocol.TBinaryProtocol(transport)
Create a client to use the protocol encoder
client = UserService.Client(protocol)
Connect!
transport.open()
result = client.getById(1)
print(result)
4.Thrift的协议
Thrift可以让用户选择客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本(text)和二进制(binary)传输协议。为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目/产品中的实际需求。常用协议有以下几种:
TBinaryProtocol:二进制编码格式进行数据传输
TCompactProtocol:高效率的、密集的二进制编码格式进行数据传输
TJSONProtocol: 使用JSON文本的数据编码协议进行数据传输
TSimpleJSONProtocol:只提供JSON只写的协议,适用于通过脚本语言解析
Thrift的传输层
常用的传输层有以下几种:
TSocket:使用阻塞式I/O进行传输,是最常见的模式
TNonblockingTransport:使用非阻塞方式,用于构建异步客户端
TFramedTransport:使用非阻塞方式,按块的大小进行传输,类似于Java中的NIO
Thrift的服务端类型
TSimpleServer:单线程服务器端,使用标准的阻塞式I/O
TThreadPoolServer:多线程服务器端,使用标准的阻塞式I/O
TNonblockingServer:单线程服务器端,使用非阻塞式I/O
THsHaServer:半同步半异步服务器端,基于非阻塞式IO读写和多线程工作任务处理
TThreadedSelectorServer:多线程选择器服务器端,对THsHaServer在异步IO模型上进行增强
开发人员关注项
对于开发人员而言,使用原生的Thrift框架,仅需要关注以下四个核心内部接口/类:Iface, AsyncIface, Client和AsyncClient。
Iface:服务端通过实现HelloWorldService.Iface接口,向客户端的提供具体的同步业务逻辑。
AsyncIface:服务端通过实现HelloWorldService.Iface接口,向客户端的提供具体的异步业务逻辑。
Client:客户端通过HelloWorldService.Client的实例对象,以同步的方式访问服务端提供的服务方法。
AsyncClient:客户端通过HelloWorldService.AsyncClient的实例对象,异步访问服务端提供的服务方法。
5.网络服务模型
Thrift提供的网络服务模型:单线程、多线程、事件驱动,从另一个角度划分为:阻塞服务模型、非阻塞服务模型。
单线程阻塞IO
Thrift的TSimpleServer 就是单线程阻塞IO。
TSimpleServer 的工作模式采用最简单的阻塞IO,实现方法简洁明了,便于理解,但是一次只能接收和处理一个Socket连接,效率比较低。
启动一个服务监听socket,由于是单线程处理而且是阻塞IO,所以要等完成业务处理后,才能重新accept等待一个新的连接。
多线程阻塞IO
TThreadPoolServer 采用阻塞socket方式工作,主线程负责阻塞式监听是否有新socket到来,具体的业务处理交由一个线程池来处理。
ThreadPoolServer解决了TSimpleServer不支持并发和多连接的问题,引入了线程池。在accept一个业务socket之后,立马把业务socket封装成一个任务交给线程池处理。
优点:
拆分了监听线程(Accept Thread)和处理客户端连接的工作线程(Worker Thread),数据读取和业务处理都交给线程池处理。因此在并发量较大时新连接也能够被及时接受。
线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池及时处理,性能也非常高。
缺点:
线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只能排队等待。默认线程池允许创建的最大线程数量为Integer.MAX_VALUE,可能会创建出大量线程,导致OOM(内存溢出)
单线程非阻塞IO
TNonblockingServer 是单线程工作,但是采用NIO的模式,利用io多路复用模型(select、epoll)处理socket就绪事件,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送操作,对于监听socket则处理连接并产生一个新业务socket并将其注册到selector上。selector当没有就绪事件为阻塞的,有就绪事件为非阻塞,会往下执行。
优点:
相比于TSimpleServer效率提升主要体现在IO多路复用上,TNonblockingServer采用非阻塞IO,对accept/read/write等IO事件进行监控和处理,同时监控多个socket的状态变化。
缺点:
TNonblockingServer模式在业务处理上还是采用单线程顺序来完成。在业务处理比较复杂、耗时的时候,例如某些接口函数需要读取数据库执行时间较长,会导致整个服务被阻塞住,此时该模式效率也不高,因为多个调用请求任务依然是顺序一个接一个执行。
多线程非阻塞IO
鉴于TNonblockingServer的缺点,Thrift的THsHaServer继承于TNonblockingServer,引入了线程池提高了任务处理的并发能力。针对读操作,单独引入线程池处理。也是Reactor的实现。
优点:
THsHaServer与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升。
缺点:
主线程仍然需要完成所有socket的监听接收、数据读取和数据写入操作。当并发请求数较大时,且发送数据量较多时,监听socket上新连接请求不能被及时接受。
多Reactor模型
TThreadedSelectorServer 是对THsHaServer的一种扩充,它将selector中的读写IO事件(read/write)从主线程中分离出来。同时引入worker工作线程池。
TThreadedSelectorServer 模式是目前Thrift提供的最高级的线程服务模型,它内部有如果几个部分构成:
- 一个AcceptThread(相当于多Reactor的mainReactor)专门用于处理监听socket上的新连接。
- 若干个SelectorThread(相当于多Reactor的subReactor)专门用于处理业务socket的网络I/O读写操作,所有网络数据的读写均是有这些线程来完成。
- 一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程。
- 一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到有业务socket中有调用请求过来,则将请求数据读取之后,交给ExecutorService线程池中的线程完成此次调用的具体执行。主要用于处理每个rpc请求的handler回调处理。即具体业务处理线程。