本人是个初入互联网行业的菜鸟,本篇博客纯属个人记录学习笔记,以方便以后查找复习,其中部分内容是粘贴他人博客的内容,如有冒犯请见谅!

 

一、thrift 简介  

  它是一款RPC通信框架,采用C/S架构,且拥有高效的序列化机制。要使用Thrift,首先我们需要在远端服务器上开启Thrift服务,之后,服务器端进程保持睡眠状态,直到客户端代码的调用。 
  Thrift应用广泛的一个主要原因是它支持多种主流的语言,且使用它的用户不需要关注服务器和客户端是怎样实现通信,怎样实现序列化的,只需要去考虑怎样实现自己需要的业务逻辑。 
  Thrift使用接口语言定义数据结构和服务,包含了最常用的数据类型,并一一对应各种语言的基本类型,还可以定义枚举和异常等等。

1.1、  Thrift是什么?能做什么?

Thrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过Thrift的IDL(接口定义语言)来描述接口函数及数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。

例如,我想开发一个快速计算的RPC服务,它主要通过接口函数getInt对外提供服务,这个RPC服务的getInt函数使用用户传入的参数,经过复杂的计算,计算出一个整形值返回给用户;服务器端使用java语言开发,而调用客户端可以是java、c、python等语言开发的程序,在这种应用场景下,我们只需要使用Thrift的IDL描述一下getInt函数(以.thrift为后缀的文件),然后使用Thrift的多语言编译功能,将这个IDL文件编译成C、java、python几种语言对应的“特定语言接口文件”(每种语言只需要一条简单的命令即可编译完成),这样拿到对应语言的“特定语言接口文件”之后,就可以开发客户端和服务器端的代码了,开发过程中只要接口不变,客户端和服务器端的开发可以独立的进行。

Thrift为服务器端程序提供了很多的工作模式,例如:线程池模型、非阻塞模型等等,可以根据自己的实际应用场景选择一种工作模式高效地对外提供服务;

1.2、  Thrift的相关网址和资料:

(1)  Thrift的官方网站:http://thrift.apache.org/

(2)  Thrift官方下载地址:http://thrift.apache.org/download

(3)  Thrift官方的IDL示例文件(自己写IDL文件时可以此为参考):

https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=test/ThriftTest.thrift;hb=HEAD

(4)  各种环境下搭建Thrift的方法:

http://thrift.apache.org/docs/install/

该页面中共提供了CentOS\Ubuntu\OS X\Windows几种环境下的搭建Thrift环境的方法。

 

二、thrift 环境搭建

1.1 windows 环境搭建

thrift.exe下载地址:http://archive.apache.org/dist/thrift/0.9.0/,版本可自选。

下载*.exe后,新建文件夹thrift,将*.exe放入文件夹,将文件夹的路径添加到环境变量。(说明:文件夹名称随意,只是为了方便后期操作起名为thrift)

打开cmd窗口,输入thrift -version,显示如下图说明安装成功:

thrift 架构的应用场景 thrift详解_Thrift

 

1.2 java环境搭建

在java环境下开发thrift的客户端或者服务器程序非常简单,只需在工程文件中加上下面三个jar包

需要下载:

libthrift.jar 、slf4j-api.jar 、slf4j-simple.jar   

注意:libthrift.jar的版本要与windows环境变量添加的*.exe版本一致,否则运行时会出现错误。

下面以IDEA为例,说明如何添加jar:

file → Project Structure

thrift 架构的应用场景 thrift详解_服务器端_02

Modules → “+” → “JARs or Directories”

thrift 架构的应用场景 thrift详解_Thrift_03

 

1.3 python 环境搭建

安装thrift:pip install thrift

卸载thrift:pip uninstall thrift

安装特定版本的package

通过使用==, >=, <=, >, <来指定一个版本号。

$ pip install 'thrift<2.0'
$ pip install 'thrift>2.0,<2.0.3'

三、使用thrift

  Thrift把它定义的相当简洁,以致于我们的使用过程也是异常的方便,简单来说,使用Thrift的过程只是需要以下的四个步骤: 

  1.设计交互式的数据格式(strucy、enum等)和具体服务(servce),定义thrift接口描述文件。

  2.利用thrift工具,根据之前定义的接口文件生成目标语言文件。

  3.实现服务代码,并把实现的业务逻辑定义为thrift服务器的处理层,选择端口,服务器启动监听,等待客户端的连接请求。

  4.客户端使用相同的端口连接服务器请求服务

下面简单的介绍下thrift接口描述语言(IDL)的类型: 
IDL包含基础类型、结构、容器、异常和服务这样几种类型: 
  基础类型 : 包括了 bool,byte、i16,i32,i64,double,string,每一种都对应各种语言的基础类型 
  结构 : 在thrift中定义为struct,它类似于C语言中的结构体,是基础类型的集合体,每一个结构都会生成一个单独的类,在java中类似于class
  容器 : thrift中定义了常用的三种容器 – list,set,map,在Java中各自的对应实现是 ArrayList、HashSet、HashMap,其中模板类型可以是基础类型或者结构类型 
  异常 : 异常的定义类似于struct,只是换成了exception 
  服务 : 服务类似于java中的接口,需要对服务中的每一个方法签名定义返回类型、参数声明、抛出的异常,对于方法抛出的异常,除了自己声明的之外,每个方法还都会抛出TException,对于返回值是void类型的方法,我们可以在方法签名的前面加上oneway标识符,将这个方法标记为异步的模式,即调用之后会立即返回

 

为了更好的理解thrift,下面按照如上四个步骤,给出一个实例(java语言):

 

1.定义接口描述文件(.thrift)

定义服务描述文件 test_server.thrift

1 namespace java main
 2 
 3 include "thrift_datatype.thrift"
 4 service TestThriftService
 5 {
 6     /**
 7     *value 中存放两个字符串拼接之后的字符串
 8     */
 9     thrift_datatype.ResultStr getStr(1:string srcStr1,2:string srcStr2),
10     
11     thrift_datatype.ResultInt getInt(1:i32 val)
12         
13 }

定义数据结构描述文件 thrift_datatype.thrift

namespace java main

/**为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    /*内部错误*/  
}  
  
  
/*int类型返回结果*/  
struct ResultInt  
{  
  1: ThriftResult result,  
  2: i32 value  
}  
  
/*String类型返回结果*/  
struct ResultStr  
{  
  1: ThriftResult result,  
  2: string value  
}

编写IDL时需要注意的问题:

[1]函数的参数要用数字依序标好,序号从1开始,形式为:“序号:参数名”;

[2]每个函数的最后要加上“,”,最后一个函数不加;

[3]在IDL中可以使用/*……*/添加注释

[4]枚举类型里没有序号

[5]struct中可以设置默认值

 

2.使用thrift工具利用IDL生成目标代码

使用命令:thrift --gen <language> <Thrift filename>

注意:使用该命令时智能编译一个thrift文件,所以多个thrift文件时需要依次编译。

编译完成时生成如下目录及文件:

thrift 架构的应用场景 thrift详解_服务器端_04

目录是由接口定义文件中,命名空间 namespace定义的。TestThriftService.java 

3.实现服务业务逻辑并开始服务监听 

  实现业务逻辑只需要实现TestThriftService.java的接口,业务实现的逻辑文件是:TestThriftServiceImpl.java

package main;

import org.apache.thrift.TException;

public class TestThriftServiceImpl implements TestThriftService.Iface
{

    @Override
    public ResultStr getStr(String srcStr1, String srcStr2) throws TException {

        long startTime = System.currentTimeMillis();
        //System.out.println("test");
        String res = srcStr1 + srcStr2;

        long stopTime = System.currentTimeMillis();

        System.out.println("[getStr]time interval: " + (stopTime-startTime));

        ResultStr ret = new ResultStr(ThriftResult.SUCCESS,res);

        return ret;
    }

    @Override
    public ResultInt getInt(int val) throws TException {
        long startTime = System.currentTimeMillis();
        int res = val * 10;

        long stopTime = System.currentTimeMillis();

        System.out.println("[getInt]time interval: " + (stopTime-startTime));

        ResultInt rett = new ResultInt(ThriftResult.NO_CONTENT,res);
        return rett;
    }

}

接下来,我们服务器端需要做最后一步工作,开启服务器端的监听

package main;

import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TTransportException;
public class testMain {
    private static int m_thriftPort = 12358;
    private static TestThriftServiceImpl m_myService = new TestThriftServiceImpl();
    private static TServer m_server = null;
    private static void createNonblockingServer() throws TTransportException
    {
        TProcessor tProcessor = new TestThriftService.Processor<TestThriftService.Iface>(m_myService);
        TNonblockingServerSocket nioSocket = new TNonblockingServerSocket(m_thriftPort);
        TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(nioSocket);
        tnbArgs.processor(tProcessor);
        tnbArgs.transportFactory(new TFramedTransport.Factory());
        tnbArgs.protocolFactory(new TBinaryProtocol.Factory());
        // 使用非阻塞式IO,服务端和客户端需要指定TFramedTransport数据传输的方式
        m_server = new TNonblockingServer(tnbArgs);
    }
    public static boolean start()
    {
        try {
            createNonblockingServer();
        } catch (TTransportException e) {
            System.out.println("start server error!" + e);
            return false;
        }
        System.out.println("service at port: " + m_thriftPort);
        m_server.serve();
        return true;
    }
    public static void main(String[] args)
    {
        if(!start())
        {
            System.exit(0);
        }
    }

}

在服务器端启动thrift框架的部分代码比较简单,不过在写这些启动代码之前需要先确定服务器采用哪种工作模式对外提供服务,Thrift对外提供几种工作模式,例如:TSimpleServer、TNonblockingServer、TThreadPoolServer、TThreadedSelectorServer等模式,每种服务模式的通信方式不一样,因此在服务启动时使用了那种服务模式,客户端程序也需要采用对应的通信方式。

另外,Thrift支持多种通信协议格式:TCompactProtocol、TBinaryProtocol、TJSONProtocol等,因此,在使用Thrift框架时,客户端程序与服务器端程序所使用的通信协议一定要一致,否则便无法正常通信。

以上述代码采用的TNonblockingServer为例,说明服务器端如何使用Thrift框架,在服务器端创建并启动Thrift服务框架的过程为:

[1]为自己的服务实现类定义一个对象,如代码中的:

TestThriftServiceImpl  m_myService =newTestThriftServiceImpl();

这里的TestThriftServiceImpl类就是代码中我们自己定义的服务器端对各服务接口的实现类。

[2]定义一个TProcess对象,在根据Thrift文件生成java源码接口文件TestThriftService.java中,Thrift已经自动为我们定义了一个Processor;后续节中将对这个TProcess类的功能进行详细描述;如代码中的:

TProcessor tProcessor = NewTestThriftService.Processor<TestThriftService.Iface>(m_myService);

[3]定义一个TNonblockingServerSocket对象,用于tcp的socket通信,如代码中的:

TNonblockingServerSocketnioSocket = newTNonblockingServerSocket(m_thriftPort);

在创建server端socket时需要指明监听端口号,即上面的变量:m_thriftPort。

[4]定义TNonblockingServer所需的参数对象TNonblockingServer.Args;并设置所需的参数,如代码中的:

thrift 架构的应用场景 thrift详解_Thrift_05

在TNonblockingServer模式下我们使用二进制协议:TBinaryProtocol,通信方式采用TFramedTransport,即以帧的方式对数据进行传输。

 

[5]定义TNonblockingServer对象,并启动该服务,如代码中的:

m_server = new TNonblockingServer(tnbArgs);

m_server.serve();

4.客户端连接服务器请求服务 

 客户端的实现也非常的简单,我们只需要获得一个thrift为我们定义好的Client,然后调用需要的业务逻辑就可以了

package Client;
import main.ResultInt;
import main.ResultStr;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import main.TestThriftService;

import java.util.logging.Logger;
public class testClient {
    public static void main(String[] args) {

        TFramedTransport m_transport = new TFramedTransport(new TSocket("localhost", 12358, 5000));
        TProtocol protocol = new TBinaryProtocol(m_transport);
        TestThriftService.Client testClient = new TestThriftService.Client(protocol);
        //System.out.println("test");

        try {
            m_transport.open();

            ResultStr res = testClient.getStr("test1","test2");

            ResultInt ret = testClient.getInt(5);
            System.out.println("ret = " + ret);
            System.out.println("status = " + res.result + ' ' + "value = " + res.value);
            m_transport.close();
            //System.out.println("test3");
        } catch (TException e){

            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

  这样,我们就完成了thrift过程的四个步骤,接下来,可以开始测试RPC过程了,首先,我们需要运行服务器端代码,会看到控制台会打印出一条输出:Start server on port 12358,之后,运行客户端代码,等待客户端进程终结,我们回到服务器端的控制台,可以看到业务逻辑中定义的输出。 

thrift 架构的应用场景 thrift详解_apache_06

 

如下是python写的客户端请求:

# -*- coding: utf-8 -*-

from thrift.transport import TSocket
from test_service import TestThriftService

from test_service.constants import *



def get():
    transport = TSocket.TSocket("localhost",12358)
    mytransport = TTransport.TFramedTransport(transport)
    mytransport.open()

    protocol = TBinaryProtocol.TBinaryProtocol(mytransport)

    client = TestThriftService.Client(protocol)

    ret1 = client.getStr("test1","test2")
    #print "eeeee"
    ret2 = client.getInt(7)

    print ret1
    print ret2

if __name__=="__main__":
    get()

 

Thrift整体架构

  其实写这个部分难免有些心有余而力不足,这个部分是整个thrift框架的组成,我对它的理解也只是基础中的基础,不过,由于是学习笔记,还是记录在这里吧。 
  Thrift是由四层架构组成的,这样设计的优点是可以自由的选择每一层的实现方式应对不同的服务需求,比如我在上面的例子中服务器端采用的是单线程阻塞式IO模型(这个只是Thrift实现的玩具,生产过程不可能会使用这种服务模式),你也可以根据需要换成其他的实现模型,而且代码部分的变动也是微乎其微的,分离的架构设计使得每一层之间都是透明的,不用考虑底层的实现,只需要一个接口就可以完成调用。下面,我将从最底层开始粗略的介绍Thrift中的每一层。

  • TTransport层 
      传输层使用TCP、Http等协议实现,它包含了各种socket调用中的方法,如open,close,read,write。由于是框架中的最后一层,所以,最重要的实现部分当然是数据的读出和写入(read 和 write),它有阻塞和非阻塞的实现方式。
  • TProtocol层 
      协议层是定义数据会以怎样的形式到达传输层。它首先对IDL中的各个数据结构进行了定义,且对每一种类型都定义了read和write方法。我们需要在服务器端和客户端声明相同的实现协议来作为内存和网络传输格式之间的映射。 
      常用的协议有 TBinaryProtocol:它定义了数据会以二进制的形式传输,它是最简单的实现协议,同时也是最常用的实现协议,非常的高效;TCompactProtocol:它的名字叫做压缩二进制协议,与TBinaryProtocol相比,它会采用压缩算法对数据进行再压缩,减少实际传输的数据量,提高传输效率。
  • TProcessor层 
      处理层就是服务器端定义的处理业务逻辑,它的主要代码是**Service.java文件中的Iface接口和Processor类。 
      Iface接口:这个接口中的所有方法都是用户定义在IDL文件中的service的方法,它需要抛出TException这个检查异常,服务器端需要定义相应的实现类去 implements **.Iface 接口,完成服务的业务逻辑。 
      Processor类:这个类中定义了一个processMap,里面包含了service中定义的方法,服务器端在构造这个Processor对象的时候,唯一需要做的就是把实现service(Iface)的对象作为参数传递给Processor的构造函数。
  • Server层 
      server是Thrift框架中的最高层,它创建并管理下面的三层,同时提供了客户端调用时的线程调度逻辑。 
      服务层的基类是TServer,它相当于一个容器,里面包含了TProcessor,TTransport,TProtocol,并实现对它们的管理和调度。TServer有多种实现方式,对于本例中使用的是TSimpleServer,这是一个单线程阻塞式IO模型,实际的生产中大多用到的是TThreadSelectorServer – 多线程非阻塞式IO模型。

thrift调用过程

  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客户端的协议层将数据按照协议格式进行解封装,然后得到具体的函数执行结果,并将其交付给调用函数;

上述过程如图4.1所示:

 

  

thrift 架构的应用场景 thrift详解_apache_07