Thrift是什么?
Thrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过Thrift的IDL(接口定义语言)来描述接口函数及数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。
Thrift为服务器端程序提供了很多的工作模式,例如:线程池模型、非阻塞模型等等,可以根据自己的实际应用场景选择一种工作模式高效地对外提供服务;
(1)支持的传输格式
TBinaryProtocol – 二进制格式.
TCompactProtocol – 压缩格式
TJSONProtocol – JSON格式
TSimpleJSONProtocol –提供JSON只写协议, 生成的文件很容易通过脚本语言解析。
TDebugProtocol – 使用易懂的可读的文本格式,以便于debug(2) 支持的数据传输方式
TSocket -阻塞式socker
TFramedTransport – 以frame为单位进行传输,非阻塞式服务中使用。
TFileTransport – 以文件形式进行传输。
TMemoryTransport – 将内存用于I/O. java实现时内部实际使用了简单的ByteArrayOutputStream。
TZlibTransport – 使用zlib进行压缩, 与其他传输方式联合使用。当前无java实现。(3)支持的服务模型
TSimpleServer – 简单的单线程服务模型,常用于测试
TThreadPoolServer – 多线程服务模型,使用标准的阻塞式IO。
TNonblockingServer – 多线程服务模型,使用非阻塞式IO(需使用TFramedTransport数据传输方式)
Thrift的使用
Thrift提供跨语言的服务框架,这种跨语言主要体现在它对多种语言的编译功能的支持,用户只需要使用IDL描述好接口函数,只需要一条简单的命令,Thrift就能够把按照IDL格式描述的接口文件翻译成各种语言版本。其实,说搭建Thrift环境的时候,实际上最麻烦的就是搭建Thrift的编译环境,Thrift的编译和通常的编译一样经过词法分析、语法分析等等最终生成对应语言的源码文件,为了能够支持对各种语言的编译,你需要下载各种语言对应的编译时使用的包。
编写IDL文件
使用Thrift开发程序,首先要做的事情就是使用IDL对接口进行描述, 然后再使用Thrift的多语言编译能力将接口的描述文件编译成对应语言的版本,本文中将IDL对接口的描述文件称为“Thrift文件”
使用IDL对接口进行描述的thrift文件命名一般都是以“.thrift”作为后缀:XXX.thrift,可以在该文件的开头为该文件加上命名空间限制,格式为:namespace语言 命名空间的名字;例如:
namespace javacom.test.service
IDL文件中对所有接口函数的描述都放在service中,service的名字可以自己指定,该名字也将被用作生成的特定语言接口文件的名字,接口函数需要对参数使用序号标号,除最后一个接口函数外,要以“,”结束对函数的描述。
例如,下面一个IDL描述的Thrift文件(该Thrift文件的文件名为:test_service.thrift)的全部内容:
namespace java com.test.service
include "thrift_datatype.thrift"
service TestThriftService
{
/**
*value 中存放两个字符串拼接之后的字符串
*/
thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),
thrift_datatype.ResultInt getInt(1:i32 val)
}
这里的TestThriftService就被用作生成的特定语言的文件名,例如我想用该Thrift文件生成一个java版本的接口文件,那么生成的java文件名就是:TestThriftService.java。
- (1) 编写IDL文件时需要注意的问题
[1]函数的参数要用数字依序标好,序号从1开始,形式为:“序号:参数名”;
[2]每个函数的最后要加上“,”,最后一个函数不加;
[3]在IDL中可以使用/……/添加注释
- (2) IDL支持的数据类型
IDL大小写敏感,它共支持以下几种基本的数据类型:
[1]string, 字符串类型,注意是全部小写形式;例如:string aString
[2]i16, 16位整形类型,例如:i16 aI16Val;
[3]i32,32位整形类型,对应C/C++/java中的int类型;例如: I32 aIntVal
[4]i64,64位整形,对应C/C++/java中的long类型;例如:I64 aLongVal
[5]byte,8位的字符类型,对应C/C++中的char,java中的byte类型;例如:byte aByteVal
[6]bool, 布尔类型,对应C/C++中的bool,java中的boolean类型; 例如:bool aBoolVal
[7]double,双精度浮点类型,对应C/C++/java中的double类型;例如:double aDoubleVal
[8]void,空类型,对应C/C++/java中的void类型;该类型主要用作函数的返回值,例如:void testVoid(),
除上述基本类型外,ID还支持以下类型:
[1]map,map类型,例如,定义一个map对象:map<i32, i32> newmap;
[2]set,集合类型,例如,定义set<i32>对象:set<i32> aSet;
[3]list,链表类型,例如,定义一个list<i32>对象:list<i32> aList;
- (3) 在Thrift文件中自定义数据类型
在IDL中支持两种自定义类型:枚举类型和结构体类型,具体如下:
[1]enum, 枚举类型
[2]struct,自定义结构体类型,在IDL中可以自己定义结构体,对应C中的struct,c++中的struct和class,java中的class。例如:
struct TestV1 {
1: i32 begin_in_both,
3: string old_string,
12: i32 end_in_both
}
注意,在struct定义结构体时需要对每个结构体成员用序号标识:“序号: ”。
生成Thrift服务接口文件
搭建Thrift编译环境之后,使用下面命令即可将IDL文件编译成对应语言的接口文件:
thrift --gen <language> <Thrift filename>
例如:如果使用上面的thrift文件(见上面的代码2.1):test_service.thrift生成一个java语言的接口文件,则只需在搭建好thrift编译环境的机子上,执行如下命令即可:
thrift --gen java test_service.thrift
编写服务器端的java代码
- 将生成的java接口文件TestThriftService.java拷贝到自己的工程文件中;
- 访问器程序需实现TestThriftService.Iface接口,在实现接口中完成自己要提供的服务:
- 服务器端启动thrift服务框架的程序
Thrift对外提供几种工作模式:
TSimpleServer、TNonblockingServer、TThreadPoolServer、TThreadedSelectorServer等模式,每种服务模式的通信方式不一样,因此在服务启动时使用了那种服务模式,客户端程序也需要采用对应的通信方式。
Thrift支持多种通信协议格式:TCompactProtocol、TBinaryProtocol、TJSONProtocol等,因此,在使用Thrift框架时,客户端程序与服务器端程序所使用的通信协议一定要一致,否则便无法正常通信。
服务器端创建并启动Thrift服务框架的过程为:
- [1]为自己的服务实现类定义一个对象,如代码2.3中的:
TestThriftServiceImplm_myService =newTestThriftServiceImpl();
这里的TestThriftServiceImpl类就是代码2.2中我们自己定义的服务器端对各服务接口的实现类。 - [2]定义一个TProcess对象,在根据Thrift文件生成java源码接口文件TestThriftService.java中,Thrift已经自动为我们定义了一个Processor;后续节中将对这个TProcess类的功能进行详细描述;如代码2.3中的:
TProcessor tProcessor = NewTestThriftService.Processor<TestThriftService.Iface>(m_myService); - [3]定义一个TNonblockingServerSocket对象,用于tcp的socket通信,如代码2.3中的:
TNonblockingServerSocketnioSocket = newTNonblockingServerSocket(m_thriftPort);
在创建server端socket时需要指明监听端口号,即上面的变量:m_thriftPort。 - [4]定义TNonblockingServer所需的参数对象TNonblockingServer.Args;并设置所需的参数,如:
TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
tnbArgs.processor(tProcessor);
tnbArgs.transportFactory(new TFramedTransport.Factory());
tnbArgs.protocolFactory(new TBinaryProtocol.Factory());
在TNonblockingServer模式下我们使用二进制协议:TBinaryProtocol,通信方式采用TFramedTransport,即以帧的方式对数据进行传输。
- [5]定义TNonblockingServer对象,并启动该服务,如代码2.3中的:
m_server = new TNonblockingServer(tnbArgs);
…
m_server.serve();
编写客户端代码
m_transport = new TSocket(THRIFT_HOST, THRIFT_PORT,2000);
TProtocol protocol = new TBinaryProtocol(m_transport);
TestThriftService.Client testClient = new TestThriftService.Client(protocol);
try {
m_transport.open();
String res = testClient.getStr("test1", "test2");
System.out.println("res = " + res);
m_transport.close();
} catch (TException e){
// TODO Auto-generated catch block
e.printStackTrace();
}
注意:
- [1]在同步方式使用客户端和服务器的时候,socket是被一个函数调用独占的,不能多个调用同时使用一个socket,例如通过m_transport.open()打开一个socket,此时创建多个线程同时进行函数调用,这时就会报错,因为socket在被一个调用占着的时候不能再使用;
- [2]可以分时多次使用同一个socket进行多次函数调用,即通过m_transport.open()打开一个socket之后,你可以发起一个调用,在这个次调用完成之后,再继续调用其他函数而不需要再次通过m_transport.open()打开socket;
应用技巧
(1) 为调用加上一个事务ID
在分布式服务开发过程中,一次事件(事务)的执行可能跨越位于不同机子上多个服务程序,在后续维护过程中跟踪log将变得非常麻烦,因此在系统设计的时候,系统的一个事务产生之处应该产生一个系统唯一的事务ID,该ID在各服务程序之间进行传递,让一次事务在所有服务程序输出的log都以此ID作为标识。
在使用Thrift开发服务器程序的时候,也应该为每个接口函数提供一个事务ID的参数,并且在服务器程序开发过程中,该ID应该在内部函数调用过程中也进行传递,并且在日志输出的时候都加上它,以便问题跟踪。(2) 封装返回结果
Thrift提供的RPC方式的服务,使得调用方可以像调用自己的函数一样调用Thrift服务提供的函数;在使用Thrift开发过程中,尽量不要直接返回需要的数据,而是将返回结果进行封装,例如上面的例子中的getStr函数就是直接返回了结果string,见Thrift文件test_service.thrift中对该函数的描述:
stringgetStr(1:string srcStr1, 2:string srcStr2)
在实际开发过程中,这是一种很不好的行为,在返回结果为null的时候还可能造成调用方产生异常,需要对返回结果进行封装,例如:
/*String类型返回结果*/
struct ResultStr
{
1: ThriftResult result,
2: string value
}
其中ThriftResult是自己定义的枚举类型的返回结果,在这里可以根据自己的需要添加任何自己需要的返回结果类型:
enum ThriftResult
{
SUCCESS, /*成功*/
SERVER_UNWORKING, /*服务器处于非Working状态*/
NO_CONTENT, /*请求结果不存在*/
PARAMETER_ERROR, /*参数错误*/
EXCEPTION, /*内部出现异常*/
INDEX_ERROR, /*错误的索引或者下标值*/
UNKNOWN_ERROR, /*未知错误*/
DATA_NOT_COMPLETE, /*数据不完全*/
INNER_ERROR, /*内部错误*/
}
此时可以将上述定义的getStr函数修改为:
ResultStr getStr(1:string srcStr1, 2:string srcStr2)
在此函数中,任何时候都会返回一个ResultStr对象,无论异常还是正常情况,在出错时还可以通过ThriftResult返回出错的类型。
- (3) 将服务与数据类型分开定义
在使用Thrift开发一些中大型项目的时候,很多情况下都需要自己封装数据结构,例如前面将返回结果进行封装的时候就定义了自己的数据类型ResultStr,此时,将数据结构和服务分开定义到不通的文件中,可以增加thrift文件的易读性。例如:
在thrift文件:thrift_datatype.thrift中定义数据类型,如:
namespace java com.browan.freepp.thriftdatatype
const string VERSION = "1.0.1"
/**为ThriftResult添加数据不完全和内部错误两种类型
*/
/****************************************************************************************************
* 定义返回值,
* 枚举类型ThriftResult,表示返回结果,成功或失败,如果失败,还可以表示失败原因
* 每种返回类型都对应一个封装的结构体,该结构体其命名遵循规则:"Result" + "具体操作结果类型",结构体都包含两部分内容:
* 第一部分为枚举类型ThriftResult变量result,表示操作结果,可以 表示成功,或失败,失败时可以给出失败原因
* 第二部分的变量名为value,表示返回结果的内容;
*****************************************************************************************************/
enum ThriftResult
{
SUCCESS, /*成功*/
SERVER_UNWORKING, /*服务器处于非Working状态*/
NO_CONTENT, /*请求结果不存在*/
PARAMETER_ERROR, /*参数错误*/
EXCEPTION, /*内部出现异常*/
INDEX_ERROR, /*错误的索引或者下标值*/
UNKNOWN_ERROR /*未知错误*/
DATA_NOT_COMPLETE /*数据不完全*/
INNER_ERROR /*内部错误*/
}
/*bool类型返回结果*/
struct ResultBool
{
1: ThriftResult result,
2: bool value
}
/*int类型返回结果*/
struct ResultInt
{
1: ThriftResult result,
2: i32 value
}
/*String类型返回结果*/
struct ResultStr
{
1: ThriftResult result,
2: string value
}
/*long类型返回结果*/
struct ResultLong
{
1: ThriftResult result,
2: i64 value
}
/*double类型返回结果*/
struct ResultDouble
{
1: ThriftResult result,
2: double value
}
/*list<string>类型返回结果*/
struct ResultListStr
{
1: ThriftResult result,
2: list<string> value
}
/*Set<string>类型返回结果*/
struct ResultSetStr
{
1: ThriftResult result,
2: set<string> value
}
/*map<string,string>类型返回结果*/
struct ResultMapStrStr
{
1: ThriftResult result,
2: map<string,string> value
}
在另外一个文件test_service.thrift中定义服务接口函数,如下所示:
namespace java com.test.service
include "thrift_datatype.thrift"
service TestThriftService
{
/**
*value 中存放两个字符串拼接之后的字符串
*/
thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),
thrift_datatype.ResultInt getInt(1:i32 val)
}
- (4) 为Thrift文件添加版本号
在实际开发过程中,还可以为Thrift文件加上版本号,以方便对thrift的版本进行控制。
工作原理
普通的本地函数调用过程
Thrift的RPC调用过程
源码分析
在thrift生成的服务接口文件中,共包含以下几部分:
(1)异步客户端类AsyncClient和异步接口AsyncIface,本节暂不涉及这些异步操作相关内容;
(2)同步客户端类Client和同步接口Iface,Client类继承自TServiceClient,并实现了同步接口Iface;Iface就是根据thrift文件中所定义的接口函数所生成;Client类是在开发Thrift的客户端程序时使用,Client类是Iface的客户端存根实现, Iface在开发Thrift服务器的时候要使用,Thrift的服务器端程序要实现接口Iface。
(3)Processor类,该类主要是开发Thrift服务器程序的时候使用,该类内部定义了一个map,它保存了所有函数名到函数对象的映射,一旦Thrift接到一个函数调用请求,就从该map中根据函数名字找到该函数的函数对象,然后执行它;
(4)参数类,为每个接口函数定义一个参数类,例如:为接口getInt产生一个参数类:getInt_args,一般情况下,接口函数参数类的命名方式为:接口函数名_args;
(5)返回值类,每个接口函数定义了一个返回值类,例如:为接口getInt产生一个返回值类:getInt_result,一般情况下,接口函数返回值类的命名方式为:接口函数名_result;
参数类和返回值类中有对数据的读写操作,在参数类中,将按照协议类将调用的函数名和参数进行封装,在返回值类中,将按照协议规定读取数据。
Thrift调用过程中,Thrift客户端和服务器之间主要用到传输层类、协议层类和处理类三个主要的核心类,这三个类的相互协作共同完成rpc的整个调用过程。在调用过程中将按照以下顺序进行协同工作:
(1) 将客户端程序调用的函数名和参数传递给协议层(TProtocol),协议层将函数名和参数按照协议格式进行封装,然后封装的结果交给下层的传输层。此处需要注意:要与Thrift服务器程序所使用的协议类型一样,否则Thrift服务器程序便无法在其协议层进行数据解析;
(2) 传输层(TTransport)将协议层传递过来的数据进行处理,例如传输层的实现类TFramedTransport就是将数据封装成帧的形式,即“数据长度+数据内容”,然后将处理之后的数据通过网络发送给Thrift服务器;此处也需要注意:要与Thrift服务器程序所采用的传输层的实现类一致,否则Thrift的传输层也无法将数据进行逆向的处理;
(3) Thrift服务器通过传输层(TTransport)接收网络上传输过来的调用请求数据,然后将接收到的数据进行逆向的处理,例如传输层的实现类TFramedTransport就是将“数据长度+数据内容”形式的网络数据,转成只有数据内容的形式,然后再交付给Thrift服务器的协议类(TProtocol);
(4) Thrift服务端的协议类(TProtocol)将传输层处理之后的数据按照协议进行解封装,并将解封装之后的数据交个Processor类进行处理;
(5) Thrift服务端的Processor类根据协议层(TProtocol)解析的结果,按照函数名找到函数名所对应的函数对象;
(6) Thrift服务端使用传过来的参数调用这个找到的函数对象;
(7) Thrift服务端将函数对象执行的结果交给协议层;
(8) Thrift服务器端的协议层将函数的执行结果进行协议封装;
(9) Thrift服务器端的传输层将协议层封装的结果进行处理,例如封装成帧,然后发送给Thrift客户端程序;
(10) Thrift客户端程序的传输层将收到的网络结果进行逆向处理,得到实际的协议数据;
(11) Thrift客户端的协议层将数据按照协议格式进行解封装,然后得到具体的函数执行结果,并将其交付给调用函数;
开发thrift客户端和服务器端程序时需要用到三个类:传输类(TTransport)、协议接口(TProtocol)和处理类(Processor);在Thrift生成代码的内部,还需要将待传输的内容封装成消息类TMessage。
TMessage
Thrift在客户端和服务器端传递数据的时候(包括发送调用请求和返回执行结果),都是将数据按照TMessage进行组装,然后发送;TMessage包括三部分:消息的名称、消息的序列号和消息的类型,消息名称为字符串类型,消息的序列号为32位的整形,消息的类型为byte类型,消息的类型共有如下17种。传输类(TTransport)
传输类或其各种实现类,都是对I/O层的一个封装,可更直观的理解为它封装了一个socket,不同的实现类有不同的封装方式,例如TFramedTransport类,它里面还封装了一个读写buf,在写入的时候,数据都先写到这个buf里面,等到写完调用该类的flush函数的时候,它会将写buf的内容,封装成帧再发送出去;
TFramedTransport是对TTransport的继承,由于tcp是基于字节流的方式进行传输,因此这种基于帧的方式传输就要求在无头无尾的字节流中每次写入和读出一个帧,TFramedTransport是按照下面的方式来组织帧的:每个帧都是按照4字节的帧长加上帧的内容来组织,帧内容就是我们要收发的数据,如下:
+---------------+---------------+
| 4字节的帧长 | 帧的内容 |
+---------------+---------------+协议接口(TProtocol)
提供了一组操作协议接口,主要用于规定采用哪种协议进行数据的读写,它内部包含一个传输类(TTransport)成员对象,通过TTransport对象从输入输出流中读写数据;它规定了很多读写方式,例如:
readByte()
readDouble()
readString()
…
每种实现类都根据自己所实现的协议来完成TProtocol接口函数的功能,例如实现了TProtocol接口的TBinaryProtocol类,对于readDouble()函数就是按照二进制的方式读取出一个Double类型的数据。
Thrift服务器端几种工作模式
Thrift为服务器端提供了多种工作模式,本文中将涉及以下5中工作模式:TSimpleServer、TNonblockingServer、THsHaServer、TThreadPoolServer、TThreadedSelectorServer,这5中工作模式的详细工作原理如下:
TSimpleServer模式
TSimpleServer的工作模式只有一个工作线程,循环监听新请求的到来并完成对请求的处理,它只是在简单的演示时候使用,它的工作方式如图:
TSimpleServer的工作模式采用最简单的阻塞IO,实现方法简洁明了,便于理解,但是一次只能接收和处理一个socket连接,效率比较低,主要用于演示Thrift的工作过程,在实际开发过程中很少用到它。
TNonblockingServer模式
TNonblockingServer工作模式,该模式也是单线程工作,但是该模式采用NIO的方式,所有的socket都被注册到selector中,在一个线程中通过seletor循环监控所有的socket,每次selector结束时,处理所有的处于就绪状态的socket,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送,对于监听socket则产生一个新业务socket并将其注册到selector中,如下图5.2所示:
- TNonblockingServer模式优点:
相比于TSimpleServer效率提升主要体现在IO多路复用上,TNonblockingServer采用非阻塞IO,同时监控多个socket的状态变化; - TNonblockingServer模式缺点:
TNonblockingServer模式在业务处理上还是采用单线程顺序来完成,在业务处理比较复杂、耗时的时候,例如某些接口函数需要读取数据库执行时间较长,此时该模式效率也不高,因为多个调用请求任务依然是顺序一个接一个执行。
THsHaServer模式(半同步半异步)
THsHaServer类是TNonblockingServer类的子类,在5.2节中的TNonblockingServer模式中,采用一个线程来完成对所有socket的监听和业务处理,造成了效率的低下,THsHaServer模式的引入则是部分解决了这些问题。THsHaServer模式中,引入一个线程池来专门进行业务处理,如下图5.3所示;
- THsHaServer的优点:
与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升; - THsHaServer的缺点:
由图5.3可以看出,主线程需要完成对所有socket的监听以及数据读写的工作,当并发请求数较大时,且发送数据量较多时,监听socket上新连接请求不能被及时接受。
TThreadPoolServer模式
TThreadPoolServer模式采用阻塞socket方式工作,,主线程负责阻塞式监听“监听socket”中是否有新socket到来,业务处理交由一个线程池来处理,如下图5.4所示:
TThreadPoolServer模式优点:
线程池模式中,数据读取和业务处理都交由线程池完成,主线程只负责监听新连接,因此在并发量较大时新连接也能够被及时接受。线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池及时处理,性能也非常高。TThreadPoolServer模式缺点:
线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只能排队等待。
TThreadedSelectorServer
TThreadedSelectorServer模式是目前Thrift提供的最高级的模式,它内部有如果几个部分构成:
(1) 一个AcceptThread线程对象,专门用于处理监听socket上的新连接;
(2) 若干个SelectorThread对象专门用于处理业务socket的网络I/O操作,所有网络数据的读写均是有这些线程来完成;
(3) 一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程。
(4) 一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到有业务socket中有调用请求过来,则将请求读取之后,交个ExecutorService线程池中的线程完成此次调用的具体执行;
TThreadedSelectorServer模式中有一个专门的线程AcceptThread用于处理新连接请求,因此能够及时响应大量并发连接请求;另外它将网络I/O操作分散到多个SelectorThread线程中来完成,因此能够快速对网络I/O进行读写操作,能够很好地应对网络I/O较多的情况;TThreadedSelectorServer对于大部分应用场景性能都不会差,因此,如果实在不知道选择哪种工作模式,使用TThreadedSelectorServer就可以。
Ref:
http://blog.csdn.net/houjixin/article/details/42778335
http://dongxicheng.org/search-engine/thrift-framework-intro/