Thrift框架

  Thrift是啥?

        Thrift 最初是由 Facebook 开发用做系统内各语言之间的RPC通信的一个可扩展且跨语言的软件框架,在2007年提交Apache基金会作为一个开源项目。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。Thrift 允许定义一个简单的定义文件中的数据类型和服务接口,以作为输入文件,编译器生成代码用来方便地生成RPC客户端和服务器通信的无缝跨编程语言。(本段摘至度娘,看完是不是觉得好厉害!)

        Thrift 是 IDL (interface definition language) 描述性语言的一个具体实现,适用于程序对程序静态的数据交换,需要先确定好数据结构。Thrift 是完全静态化的,当数据结构发生变化时,必须重新编辑IDL文件、代码生成再编译载入的流程,跟其他IDL工具相比较可以视为是 Thrift 的弱项。Thrift 适用于搭建大型数据交换及存储的通用工具,在大型系统中的内部数据传输上相对于JSON和xml无论在性能、传输大小上有明显的优势。

        堆栈结构

        如下图所示,Thrift包含一个完整的堆栈结构用于构建客户端和服务器端。

        1a01974f-2ac0-42f5-9bcc-70800407c8ae.jpg

                                            图2

        根据上图从上往下的结构来看,黄色部分是用户实现的业务逻辑,褐色部分是根据 Thrift 定义的服务接口描述文件生成的客户端和服务器端代码框架,红色部分是根据 Thrift 文件生成代码实现数据的读写操作,紫色部分是 Thrift协议层,蓝色部分是 Thrift 的传输体系,再往下就是其底层 I/O 通信实现。

        client/server调用流程

        首先来看下 Thrift 服务端是如何启动并提供服务的,如下时序图所示(点击此处看大图):

        image004.png

                                                                图3

        上图所示是 HelloServiceServer 启动的过程,以及服务被客户端调用时服务器的响应过程。我们可以看到,程序调用了 TThreadPoolServer 的 serve() 方法后,server 进入阻塞监听状态,其阻塞在 TServerSocket 的 accept()方法上。当接收到来自客户端的消息后,服务器发起一个新线程处理这个消息请求,原线程再次进入阻塞状态。在新线程中,服务器通过 TBinaryProtocol 协议读取消息内容,调用 HelloServiceImpl 的 helloVoid() 方法,并将结果写入 helloVoid_result 中传回客户端。

        在服务启动后,客户端就开始调用其服务,如下图所示(点击此处看大图):

        image006.png

                                                                          图4

        上图展示的是 HelloServiceClient 调用服务的过程,以及接收到服务器端的返回值后处理结果的过程。我们可以看到,程序调用了 Hello.Client 的 helloVoid() 方法,在 helloVoid() 方法中,通过 send_helloVoid() 方法发送对服务的调用请求,通过 recv_helloVoid() 方法接收服务处理请求后返回的结果。

        数据类型

        上一节我们已经大致了解了 Thrift 的 server 和 client 的工作流程,现在就来讲讲 Thrift 可定义的数据类型。Thrift 支持几大类数据结构:基本类型、结构体和异常类型、容器类型、服务类型。

        1. 基本类型

1
2
3
4
5
6
7
bool:布尔值 (true or false), one byte
byte:有符号字节
i16:16位有符号整型
i32:32位有符号整型
i64:64位有符号整型
double:64位浮点型
string:未知编码或者二进制的字符串

        2. 结构体和异常类型

        Thrift 结构体 (struct) 在概念上类似于C语言结构体类型,在 java 中 Thrift 结构体将会被转换成面向对象语言的类。struct 的定义如下:

1
2
3
4
5
6
struct UserDemo {
    1: i32 id;
    2: string name;
    3: i32 age = 25;
    4: string phone;
}

        struct具有以下特性:  

1
2
3
4
5
6
7
struct不能继承,但是可以嵌套,不能嵌套自己;
其成员都是有明确类型;
成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用(详情往下看备注1);
成员分割符可以是逗号(,)或是分号(;),而且可以混用,但是为了清晰期间,建议在定义中只使用一种,比如java学习者可以就使用逗号(;);
字段会有optional和required之分(详情往下看备注2);
每个字段可以设置默认值
同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。

        备注1:数字标签作用非常大,随着项目开发的不断发展,也许字段会有变化,但是建议不要轻易修改这些数字标签,修改之后如果没有同步客户端和服务器端会让一方解析出问题。

        备注2:关于 struct 字段类型,规范的 struct 定义中的每个域均会使用 required 或者 optional 关键字进行标识,但是如果不指定则为无类型,可以不填充该值,但是在序列化传输的时候也会序列化进去。其中 optional 是不填充则不序列化,required 是必须填充也必须序列化。如果 required 标识的域没有赋值,Thrift 将给予提示;如果 optional 标识的域没有赋值,该域将不会被序列化传输;如果某个 optional 标识域有缺省值而用户没有重新赋值,则该域的值一直为缺省值;如果某个 optional 标识域有缺省值或者用户已经重新赋值,而不设置它的 __isset 为 true,也不会被序列化传输。

        异常在语法和功能上相当于结构体,差别是异常使用关键字 exception 而不是 struct 声明。它在语义上不同于结构体:当定义一个 RPC 服务时,开发者可能需要声明一个远程方法抛出一个异常。

        3. 容器类型

        Thrift 容器与目前流行编程语言的容器类型相对应,有3种可用容器类型:

1
2
3
list<t>:元素类型为t的有序表,容许元素重复。对应java的ArrayList
set<t>:元素类型为t的无序表,不容许元素重复。对应java的HashSet
map<t,t>:键类型为t,值类型为t的kv对,键不容许重复。对对应Java的HashMap

        其中容器中元素类型可以是除了service外的任何合法 Thrift 类型(包括结构体和异常)。

        4. 服务类型

        服务的定义方法在语义上等同于面向对象语言中的接口。Thrift 编译器会产生执行这些接口的 client 和 server stub(详情下一节会具体描述)。下面我们就举个简单的例子解释 service 如何定义:

1
2
3
4
5
6
7
8
9
10
11
service QuerySrv{
    /**
     * 本方法实现根据名字和年龄来找到对应用户的所有信息
     */
    UserDemo qryUser(1:string name, 2:i32 age);
    /**
     * 本方法实现根据id找到对应用户的手机号码
     */
    string  queryPhone(1:i32 id);
     
}

        在上面的例子中我们定义了一个 service 类型的结构,里面包含两个方法的定义。

        在定义 services 的时候,我们还需要了解一下规则:

1
2
3
4
5
6
继承类必须实现这些方法;
参数可以是基本类型或者结构体;
所有的参数都是const类型,不能作为返回值;
返回值可以是void(oneway的返回值一定是void);
服务支持继承,一个service可使用extends关键字继承另一个service;
服务不支持重载

        除上面所提到的四大数据类型外,Thrift 还支持枚举类型(enum)和常量类型(const)。

        命名空间

        Thrift 中的命名空间类似于 java 中的 package,它们提供了一种组织(隔离)代码的简便方式。名字空间也可以用于解决类型定义中的名字冲突。

        

        传输体系

        传输协议

        Thrift 支持多种传输协议,用户可以根据实际需求选择合适的类型。Thrift传输协议上总体可划分为文本 (text) 和二进制 (binary) 传输协议两大类,一般在生产环境中使用二进制类型的传输协议为多数(相对于文本和JSON具有更高的传输效率)。常用的协议包含:

1
2
3
4
TBinaryProtocol:是Thrift的默认协议,使用二进制编码格式进行数据传输,基本上直接发送原始数据。
TCompactProtocol:压缩的、密集的数据传输协议,基于Variable-length quantity的zigzag 编码格式。
TJSONProtocol:以JSON (JavaScript Object Notation)数据编码协议进行数据传输。
TDebugProtocol:常常用以编码人员测试,以文本的形式展现方便阅读。

        关于以上几种类型的传输协议,如果想更深入更具体的了解其实现及工作原理,可以参考站外相关文章《thrift源码研究》。

        传输方式

        与传输协议一样,Thrift 也支持几种不同的传输方式。

        TSocket:阻塞型 socket,用于客户端,采用系统函数 read 和 write 进行读写数据。

        TServerSocket:非阻塞型 socket,用于服务器端,accecpt 到的 socket 类型都是 TSocket(即阻塞型 socket)。

        TBufferedTransport 和 TFramedTransport 都是有缓存的,均继承TBufferBase,调用下一层 TTransport 类进行读写操作吗,结构极为相似。其中 TFramedTransport 以帧为传输单位,帧结构为:4个字节(int32_t)+传输字节串,头4个字节是存储后面字节串的长度,该字节串才是正确需要传输的数据,因此 TFramedTransport 每传一帧要比 TBufferedTransport 和 TSocket 多传4个字节。

        TMemoryBuffer 继承 TBufferBase,用于程序内部通信用,不涉及任何网络I/O,可用于三种模式:(1)OBSERVE模式,不可写数据到缓存;(2)TAKE_OWNERSHIP模式,需负责释放缓存;(3)COPY模式,拷贝外面的内存块到TMemoryBuffer。

        TFileTransport 直接继承 TTransport,用于写数据到文件。对事件的形式写数据,主线程负责将事件入列,写线程将事件入列,并将事件里的数据写入磁盘。这里面用到了两个队列,类型为 TFileTransportBuffer,一个用于主线程写事件,另一个用于写线程读事件,这就避免了线程竞争。在读完队列事件后,就会进行队列交换,由于由两个指针指向这两个队列,交换只要交换指针即可。它还支持以 chunk(块)的形式写数据到文件。

        TFDTransport 是非常简单地写数据到文件和从文件读数据,它的 write 和 read 函数都是直接调用系统函数 write 和 read 进行写和读文件。

        TSimpleFileTransport 直接继承 TFDTransport,没有添加任何成员函数和成员变量,不同的是构造函数的参数和在 TSimpleFileTransport 构造函数里对父类进行了初始化(打开指定文件并将fd传给父类和设置父类的close_policy为CLOSE_ON_DESTROY)。

        TZlibTransport 跟 TBufferedTransport 和 TFramedTransport一样,调用下一层 TTransport 类进行读写操作。它采用<zlib.h>提供的 zlib 压缩和解压缩库函数来进行压解缩,写时先压缩再调用底层 TTransport 类发送数据,读时先调用 TTransport 类接收数据再进行解压,最后供上层处理。

        TSSLSocket 继承 TSocket,阻塞型 socket,用于客户端。采用 openssl 的接口进行读写数据。checkHandshake()函数调用 SSL_set_fd 将 fd 和 ssl 绑定在一起,之后就可以通过 ssl 的 SSL_read和SSL_write 接口进行读写网络数据。

        TSSLServerSocket 继承 TServerSocket,非阻塞型 socket, 用于服务器端。accecpt 到的 socket 类型都是 TSSLSocket 类型。

        THttpClient  THttpServer 是基于 http1.1 协议的继承 Transport 类型,均继承 THttpTransport,其中 THttpClient 用于客户端,THttpServer 用于服务器端。两者都调用下一层 TTransport 类进行读写操作,均用到TMemoryBuffer 作为读写缓存,只有调用 flush() 函数才会将真正调用网络I/O接口发送数据。

        TTransport是所有Transport类的父类,为上层提供了统一的接口而且通过TTransport即可访问各个子类不同实现,类似多态。

Thrift的体系架构如下:


实际上Thrift提供了一个支持多语言的Lib库,包括C++、Java、PHP、Python、Ruby等。同时它自定义了一种中间语言(Thrift IDL),

用于编写统一格式的thrift配置文件,定义数据类型。然后根据此配置文件调用lib库中的生成器来生成指定语言的服务,这正体现其跨语言的特性。

 

Thrift支持五种数据类型:
Base Types:基本类型
Struct:结构体类型
Container:容器类型,即List、Set、Map
Exception:异常类型
Service:类似于面向对象的接口定义,里面包含一系列的方法

下面是Thrift中所支持的类型(前三种类型)

 

Thrift通过两种抽象机制来完成底层客户端和服务器端的通信,分别是:

1)  Transport:抽象了底层网络通信部分的接口
TTransport对Java I/O层进行封装,抽象了底层网络通信部分的接口。主要是对以下6种接口进行了抽象:open, close, isopen, read, write, flush。

2)  Protocol:抽象了对象在网络中传输部分的接口,它规定了thrift的RPC可以调用的接口。这些接口是为了传输不同类型的数据而设计的。

   Transport在传输数据时是不区分所传的数据类型的,一律以流的形式传输。所以Protocol类在Transport类之上实现了按数据类型来传输数据。

Transport的类结构实现:

socket的封装

 

TTransport是一个抽象的基类,对socket进行了封装,提供了对流的一系列操作接口。thrift提供了两种常用类型的Socket,一种是阻塞式的,两外一种是非阻塞式的。

这两种socket的区别实际上就是传统IO和NIO的区别,这里就不累述了。通过close()和open()来建立和关闭连接;通过read()和write()来对流进行读写。

 

ServerSocket的封装

 

TServerSocket同样是一个抽象的基类,对ServerSocket进行了封装。它是以非阻塞的IO方式进行通讯的。通过listen()来监听连接请求,通过accept()来获取。

 

Protocol的类结构实现:

 

TProtocol提供了一系列按类型进行读写的接口。并持有一个TTransport对象的引用,所以TProtocol类是依赖于TTransport类的。

其中定义了一系列的write和read方法,应该说每种类型对应一套write和read方法。在write和read方法中会分别调用TTransport

中的write和read方法进行实际的读写操作。

 

RPC的具体实现:

1. TProcessor

它是thrift中最简单的一个接口了,仅包含一个方法:
bool process(TProtocol in, TProtocol out)throws TException

此方法有两个参数in和out,分别用于从socket中读取数据和向scoket中写数据。每个service对象中都会包含一个实现了此接口的类对象(Process),

这个类就是处理RPC请求的核心。

 

2. TServer

TServer相当于一个容器,拥有生产TProcessor、TTransport、TProtocol的工厂对象。它的工作流如下:

1) 使用TServerTransport来获取一个TTransport(客户端的连接)

2) 使用TTransportFactory来将获取到的原始的transport转化为特定的transport

3) 使用TProtocolFactory来为获取TTransport的输入输出,即TProtocol

4) 唤醒TProcessor的process方法,处理请求并返回结果

 

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值