Apache Thrift是什么?
Apache Thrift软件框架用于可扩展的跨语言服务开发,将软件堆栈与代码生成引擎相结合,构建可在C ++,Java,Python,PHP,Ruby,Erlang,Perl,Haskell,C#之间高效无缝工作的服务, Cocoa,JavaScript,Node.js,Smalltalk,OCaml和Delphi等语言。
Thrift最初由facebook研发,主要用于各个服务之间的RPC通信,支持跨语言,支持的语言有C++,Java,Python,PHP,Ruby,Erlang,PErl,Haskell,C#,Cocoa,JavaScript,Node.js,
Smalltalk,and OCaml都支持。
Thrift是一个典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。既然客户端和服务器端能使用不同的语言开发,那么一定就要有一种中间语言来关联客户端和服务器端的语言。这种语言就是IDL(Interface Description Language)。
Thrift不支持无符号类型,因为很多编程语言不存在无符号类型,比如说java。一个RPC框架如果支持多种语言,那么这个RPC框架所支持的数据类型一定是这个RPC框架多语言支持的数据类型的交集。
Apache Thrift 概念
Thrift支持的数据类型
- bool: 布尔类型(true或者false)
- byte: 有符号字节
- i16: 16位有符号整数
- i32: 32位有符号整数
- i64: 64位有符号整数
- double: 64位浮点数
- string: 字符串
集合中的元素可以是除了service之外的任何类型,包括exception。
Thrift支持三种组件分别是
- Structs(结构体,编译生成之后就是类)
- Service(客户端和服务端通信的接口)
- exception(客户端和服务端通信接口抛出的异常)
结构体(struct)
就像C语言一样,Thrift支持struct类型,目的就是将一些数据聚合在一起,方便传输管理,struct的定义形式如下:
struct People{ |
异常(exception)
Thrift支持自定义exception,规则与struct一样
exception RequestException{ |
服务(service)
Thrift定义服务相当于Java中创建Interface一样,创建的service经过代码生成命令之后就会生成客户端和服务器端的框架代码。定义形式如下:
service HelloWorldService{ |
枚举(enum)
枚举的定义形式和Java的Enum定义类似
enum Gender{ |
常量(const)
Thrift也支持常量定义,使用const关键字
const i32 MIN_GATE=30 |
类型定义/别名
Thrift支持类似C++一样的typedef定义,比如我们对i32不熟悉,我们就使用int类代替i32,比如我们对i64不熟悉,我们就使用long代替i64
typedef i32 int |
文件包含
Thrift也支持文件包含,相当于C/C++中的include,java中的import。使用关键字include定义:
include "thrift.generated" |
支持的传输格式(协议)
- TBinaryProtocol 二进制格式
- TCompactProtocol 压缩格式(优于二进制格式) 推荐使用
- TJSONProtocol JSON格式
- TSimpleJSONProtocol 提供JSON只写协议,生成的文件很容易通过脚本语言解析(无法通过程序很容易的读取,因为他却少必要的元数据信息(metadata),也就是对他解码的时候没有一个参照的标准不知道该怎么解析,‘只写’:他可以生成TSimpleJSONProtocol协议要求的格式(可以写出去),但是对端无法再将数据读取回来(无法解析))
- TDebugProtocol 使用易懂的可读的文本格式,以便于debug
支持的数据传输方式
- TSocket 阻塞式socket(类似Java中的ServerScoket)
- TFramedTransport 以frame为单位进行传输,非阻塞式服务中使用(他会将一端传输给另一端的数据分成一个一个的frame,类似于WebSocket) 推荐使用
- TFileTransport 以文件形式进行传输
- TMenoryTransport 将内存用于I/O Java实现时内部实际使用了简单的ByteArrayOutputStream
- TZlibTransport 使用zlib协议进行压缩,与其他传输方式联合使用,当前无Java实现
支持的服务模型
- TSimpleServer 简单的单线程服务模型,常用于测试
- TThreadPoolServer 多线程服务模型,使用标准的阻塞式IO
- TNonblockingServer 多线程服务模型,使用非阻塞式IO(需使用TFramedTransport数据传输方式)
- THsHaServer THsHa引入了线程池去处理,其模型把读写任务放到线程池去处理。Half-sync/Half-async(半同步半异步)的处理模式,Half-aysnc是在处理IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步处理(需使用TFramedTransport数据传输方式) 推荐使用
最佳结合方案
使用TCompactProtocol
作为传输格式,使用TFramedTransport
作为传输方式,使用THsHaServer
作为服务模型
Thrift支持的容器类型
list:一系列由T类型的数据组成的有序列表,元素可以重复。
set:一系列由T类型的数据组成的无序集合,元素不可重复。
map:一个字典结构,key为k类型,value为V类型,相当于java中的HashMap
以上集合容器都可以使用泛型的。
Thrift 架构
Thrift框架实际上实现了C/S通信模型
- 通过代码生成工具,生成客户端和服务端代码(可以为不同语言),实现跨语言支持
- 生成的代码主要完成数据结构化解析、发送和接收,通过processor调用服务端处理逻辑
- TProtocal为协议层,主要实现各种格式的序列化协议,如二进制、JSON和压缩格式等
- TTransport为传输层,主要实现了阻塞IO和非阻塞IO的实现
- 底层IO传输,主要使用socket、http等一些传输协议
Thrift 组件
Thrift的核心组件, 主要包含以下几个方面
- IDL服务描述组件,负责完成跨平台和跨语言(针对不同语言完成了Server层和Client代码的生成)
- TServer和Client,服务端和客户端组件的实现
- TProtocal 协议和解编码组件
- TTransport 传输组件
- TProcessor 服务调用组件,完成对服务实现的调用
Thrift Server
- Thrift Server的职责是将Thrift支持的各种特性结合起来。
- 创建传输Transport并为Transport创建输入或输出TProtocal
- 创建基于输入或输出的处理器processor(process调用服务端业务实现)
- 等待连接建立并将数据交给处理器processor,处理完成返回client
- Thrift服务端的实现,目前主要有TSimpleServer、TNonblockingServer、THsHaServer、TThreadPoolServer、TThreadSelectorServer的实现,当前生产环境中主要使用的是TThreadPoolServer的实现。
TSimpleServer
TSimpleServer的工作模式最简单地阻塞IO,一次只能接收和处理一个Socket连接,效率比较低,生产中并不会使用这种Server的实现
TNonblockingServer
非阻塞服务模式实现,对所有客户端的调用几乎是公平,该服务模式采用的是单线程工作,但采用的时NIO的实现方式。
- 该工作模式效率提升主要体现在IO多路复用上, 采用nio同时监听多个socket的状态变化
- 仍然采用单线程顺序执行,在业务处理复杂和耗时的情况下,效率仍然是不高的
THsHaServer
半同步半异步模式,THsHaServer是TNonblockingServer的子类,因为TNonblockingServer仍然采用一个县城完成socket的监听和业务处理,效率相对较低。THsHaServer引入了线程池专门进行业务处理
- 主线程只读取数据,业务处理交给线程池完成处理,主线程效率大大提升
- 主线程仍然要对所有的socket监听和读取,当并发大和发送数据较多的情况下,监听的socket请求不能及时接受
TThreadPoolServer
TThreadPoolServer模式采用阻塞socket方式工作,主线程负责阻塞监听新socket,业务处理交给线程池处理
- 线程池模式中,数据读取和业务处理都交给线程池处理,主线程只负责监听,因此在并发量较大情况下也能及时接受
- 线程池处理模式,比较适合服务端能够预知多少客户端并发的情况,这样每个请求都能够及时处理,性能也相对理想
- 线程池模式的处理能够受限于线程池的工作能力,在高并发情况下,新的请求只能够排队等待
TThreadSelectorServer
ThreadSelectorServer是目前Thrift提供的最高级的工作模式,其内部主要的工作流程如下
一个accept thread线程对象,专门用于处理监听socket新连接
若干个selector thread线程对象,专门用于处理业务socket上得IO,所有网络读写都由selector thread完成
一个负载均衡器(SelectorThreadLocadBalancer),主要用于accept thread接收到新socket请求时,决定分配请求到selector thread
ExecutorService工作线程池,用于业务处理,在selector thread 读取socket请求数据,交给业务线程池具体执行
专门的accept thread用于接收新socket请求,可以接受大量的请求
socket请求经过负载均衡器分散到selector thread,可以应对io读写较大的情况
executor工作线程池,具体执行业务逻辑,可以发挥服务端最大的工作能力
TTransport
- TTransport传输层提供了和网络之间交互的读写抽象,这使得Thrift能够将底层传输和系统其他部分(例如序列化和反序列化)分离开来。
- Transport暴露的接口主要有open、close、read、write、flush等
- 除了Transport提供的上卖弄接口,Thrift提供了用于接收和创建原始对象的ServerTransport接口,主要用于服务端为传入的链接创建新的传输对象。open、listen、accept和close等
- 同时Thrift还提供了文件传输和HTTP传输等传输实现
客户端Transport实现
- 客户端的传输实现主要分为两类,阻塞传输实现和非阻塞传输实现
- 阻塞传输实现主要在TIOStreamTransport和TSocket中实现
- TIOStreamTransport是最常用的传输层实现,它通过一个输入流和输出流实现了传输层的所有操作,其和Java的结构完美兼容(Java实现了各种IO流)
- TSocket是通过Socket完成对Thrift传输实现,是客户端Socket连接实现和服务端传输的连接实现
- 阻塞传输相关类TNonblockingTransport(接口定义)和TNonblockingSocket(java nio中SocketChannel的包装实现)
- THttpClient是http的传输实现,主要用于服务端是HTTP服务,作为thrift的客户端的请求读取实现
服务端Transport实现
- TServerSocket是通过包装ServerSocket的传输实现,是一种阻塞的传输实现
- TNonblockserServerSocket是一种通过包装nio的ServerSocketChannel的实现,基础传输还是ServerSocket
缓存传输实现
- TMemoryInputTransport 封装了字节数组byte[]作为输入流的封装,从系统缓冲区读取数据,不支持写缓存。TMemoryBuffer则通过TByteArrayOutputStream作为输出流的封装,支持缓存读也支持往缓冲区写入数据。
- TFrameTransport是一种缓冲的Transport实现,它通过在每个消息前都有一个4个字节的帧消息来保证每次读取完整的消息
- 封装TMemoryInputTransport作为输入流、TByteArrayOutputStream作为输出流,作为内存缓冲区的封装
- TFrameTransport的flush执行时,会先写4byte的消息头,然后写入消息体
- 在读取消息时,也会先读取4byte的长度,然后在读取消息体
- TFastFramedTransport是一种内存利用率更高的内存读写实现,它使用自动增长的
byte[](长度不够时才new)
,而不是每次都new一个byte[],从而提升了内存的使用率。其余实现和TFramedTransport一样,也会有消息头作为帧来记录消息的长度
其他传输实现介绍
- TFileTransport 文件传输实现,基于Event的异步实现
- TZlibTransport 基于zlib库的解压缩传输实现,通过压缩减少网络传输
- TSaslTransport 是基于Simple Authentication Security Layer的认证实现
传输层实现总结
- Thrift的传输层采用装饰器模式实现了包装IO流,可以通过包装流和节点流的概念区分各种Transport实现
- 节点流表示自身采用byte[]提供IO读写的实现,包装流表示封装类其他传输实现提供IO的读写
- 包装流主要是TFrame的传输实现,其实现是在写完消息flush时,回家上4byte的消息头,读消息的时候也会读取4byte的消息头
- Thrift协议和具体的传输对象绑定,协议使用具体的Transport来实现数据的读取
TProtocol
协议抽象定义了将内存数据映射到有线格式的机制。换句话说,协议规定了数据类型如何使用底层传输对自身进行编码/解码。因为,协议实现了管理编码方案并负责(反)序列化。这里指的序列化协议的例子包含JSON、XML、纯文本、紧凑二进制等。 Thrift实现的协议如下:
- 二进制,字段的长度和类型编码为字节码
- 压缩实现 THRIFT-110
- JSON实现
TBinaryProtocol
是一种字节流读取的实现,String类型读取是通过nio实现,其余类型通过原生数据直接读取实现。核心代码如下:
public ByteBuffer readBinary() throws TException { |
TCompactProtocol
TCompactProtocol协议作为TBinaryProtocol协议的升级强化版,都作为二进制编码传输方式,采用了一种乐器MIDI文件的编码方法。详细描述参见 THRIFT-110
- ZigZag——有符号数编码
编码前 | 编码后 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2 | 4 |
-3 | 5 |
其效果等效于正数等于原先 * 2,负数变正数
32bits int = (i « 1) ^ (i » 31), 64bits long = (l « 1) ^ (l » 63)
- VLQ——编码压缩 A variable-length quantity (VLQ) 是一种通用编码,使用任意数量的二进制八位字节(8bit字节)来表示一个任意大的整数,其没定义为MIDI格式以节省空间资源。这种编码也被用于表示表式扩展音乐格式(XMF)中。即VLQ本质上就是用一个无符号的最大128来表示一个无符号的整数,并增加了一个第八位来表示字节是否继续。 即一字节的最高位(MHB)为标志位,不参与具体的内容,意思数值的大小仅仅有其它七位来表示。当最高位bit为1时,表示下一个byte也是该数值的内容(下一个byte的低七位bits);当最高位bit为0时,下一个byte不参与其中。通过这样的方式,而不是int固定的4个bytes,long 8个bytes来讲,对于小数,能节约不少的空间大小;但凡事有利有弊,当数值比较大时,就要占用更多的空间,例如较大的int ,需要5bytes,较大的long需要10bytes. 编码假定八位位组(八位字节),其中最高有效位(MSB)(通常也称为符号位)被保留以指示是否有另一个VLQ八位组
VLQ 八位字节
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
2^7 | 2^6 | 2^5 | 2^4 | 2^3 | 2^2 | 2^1 | 2^0 |
A | Bn |
如果A是0,那么这是整数的最后一个VLQ八位字节。如果A是1,则接下来是另一个VLQ字节。 B是7位数字[0x00,0x7F],n是VLQ八位字节的位置,其中B 0是最不重要的。VLQ八位组在流中首先排列得最重要
两种编码的结合
当VLQ编码遇到负数时,例如:long -1; 0XFFFFFFFFFFFFFFFF,就需要10bytes了,通过和ZigZag的结合,把负数转变相应的正数。当正数,负数的 | 数值 | 较小时,都可以通过两者的结合,有效的压缩占用的空间大小。但同上,数值较大不可避免的占用比平常正常编码更多的空间。 |
---|---|---|
106903转化为VLQ字节码例子
其他转换例子
Integer | Variable-length quantity |
---|---|
0x00000000 | 0x00 |
0x0000007F | 0x7F |
0x00000080 | 0x81 0x00 |
0x00002000 | 0xC0 0x00 |
0x00003FFF | 0xFF 0x7F |
0x00004000 | 0x81 0x80 0x00 |
0x001FFFFF | 0xFF 0xFF 0x7F |
0x00200000 | 0x81 0x80 0x80 0x00 |
0x08000000 | 0xC0 0x80 0x80 0x00 |
0x0FFFFFFF | 0xFF 0xFF 0xFF 0x7F |
writeVarint32实现实现
private void writeVarint32(int n) throws TException { |
TJSONProtocal实现
- TJSONProtocol 和 TSimpleJSONProtocol 两种实现。
- 实现比较简单,不再赘述。
Processor
- Processor封装了从输入流中读取数据并写入输出流的能力。
- 输入流和输出流由协议对象表示,处理结构非常接单
interface TProcessor { |
- 服务响应的处理器由编译器生成的代码,并由服务端业务实现。
- 处理器实际上是从线路(通过协议输入流)读取数据,然后委托给处理程序(用户实现执行)
- 处理程序结果,通过线路(通过协议输出流),写入响应中,客户端得到结果
TBaseProcessor实现
public abstract class TBaseProcessor<I> implements TProcessor { |
TBaseAsyncProcessor
public class TBaseAsyncProcessor<I> implements TAsyncProcessor, TProcessor { |
以上两种Processor的实现细节都在 FrameBuffer 和 AsyncFrameBuffer
- FrameBuffer是Thrift NIO服务器端的一个核心组件,它一方面承担了NIO编程中的缓冲区的功能,另一方面还承担了RPC方法调用的职责。
- 实现了客户端和服务端交互的状态机
- 管理读取帧的大小和帧数据,将其作为一个包装数据进行数据传递,然后将响应数据写会客户端
- 在这个过程中,它管理为客户端管理翻动选中key区域数据的读写
- AsyncFrameBuffer是FrameBuffer的子类,主要功能和FrameBuffer,主要实现了异步的处理器的读写
Thrfit 服务过程解析
Server端
HelloServiceServer启动过程和客户端调用过程
过程详解
- 程序调用TheadPoolServer的serve方法后,server进入阻塞监听状态,阻塞在TServerSocket的accept方法上
- 当接收到客户端的调用请求,服务端创建新线程处理请求,原线程再次进入阻塞状态
- 新线程中同步TBinaryProtocol协议读取消息内容,调用HelloServerImpl的helloVoid方法,并将helloVoid_result中传回客户端
Client端
HelloServiceClient调用过程和接收返回结果过程
- 程序调用Hello.Client的helloVoid方法
- 在helloVoid中通过send_helloVoid发送对服务端请求,通过recv_helloVoid方法接收对服务请求后返回的结果
Thrift的安装
官方网站提供的下载安装地址,根据不同的操作系统选择自己的安装方式
Linux电脑可使用系统的包管理器安装,本次示例系统为 Archlinux
安装Apache Thrift:
sudo pacman -S thrift |
查看Thrift 信息
❯ thrift -version |
快速入门/第一个Thrift程序
定义IDL文件
#定义命名空间: namespace 语言名 路径 |
使用thrift编译器生成编译文件
格式:thrift –gen 要生成的语言 IDL文件
thrift --gen java src/thrift/data.thrift |
将生成的代码复制到src/main目录下,并加入thrift依赖,本案例使用gradle作为包管理工具
org.apache.thrift:libthrift:0.13.0 |
Java编写客户端与服务器端
编写接口实现类
实际开发中放在服务端
//thrift生成的接口文件的实现类 |
服务器端
/* |
客户端
public class ThriftClient { |
测试
启动服务器,再启动客户端
服务器端打印:
Thrift Server Started! |
客户端打印:
星空 |
于Google Protobuf相比,Google Protobuf只是进行编解码(序列化与反序列)操作,使用netty作为网络载体进行远程方法调用。而Thrift不仅仅既可以进行编解码工作,还提供传输对象功能,并且可以自己定义业务接口。
thrift等于
- 一个 code generator 代码生成器
- 序列化、反序列化
- server层。