场景

Thrift

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)。

RPC

RPC, 远程过程调用,直观说法就是A通过网络调用B的过程方法。

简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。

RPC 会隐藏底层的通讯细节(不需要直接处理Socket通讯或Http通讯) RPC 是一个请求响应模型。

客户端发起请求,服务器返回响应(类似于Http的工作方式) RPC 在使用形式上像调用本地函数(或方法)一样去调用远程的函数(或方法)。

 

实现

Thrift数据类型

Thrift不支持无符号类型,因为很多编程语言不存在无符号类型,比如Java。

byte:有符号字节
i16:16位有符号整数
i32:32位有符号整数
i64:64位有符号整数
double:64位浮点数
string:字符串类型

Thrift容器类型

list:一系列由T类型的数据组成的有序列表,元素可以重复

set:一系列由T类型的数据组成的无序集合,元素不可重复

map:一个字典结构,key为K类型,value为V类型,相当于Java中的HashMap

Thrift工作原理

如何实现多语言之间的通信?

数据传输使用socket(多种语言均支持),数据再以特定的格式(String等)发送,接收方语言进行解析。

定义thrift的文件,由thrift文件(IDL)生成双方语言的接口、model,在生成的model以及接口中会有解码编码的代码。

Thrift结构体

就像C语言一样,Thrift支持struct类型,目的就是将
一些数据聚合在一起,方便传输管理。struct的定义
形式如下:

struct People{
1:string name;
2: i32 age;
3: string gender;
}

Thrift枚举

枚举的定义形式和Java的Enum定义类似

enum Gender{
MALE,
FEMALE
}

Thrift异常

Thrift支持自定义exception,规则与struct一样

exception RequestException{
1:i32 code;
2: string reason:
}

Thrift服务

Thrift定义服务相当于Java中创建Interface一样,创建的service经过代码生成命令之后就会生成客户端和服务端的框架代码。定义形式如下:、

service HelloWordService {
// service中定义的函数,相当于Java interface中定义的方法
string doAction(1:string name,2: i32 age);
}

Thrift类型定义

Thrift支持类似C++一样的typedef定义:

typedef i32 int
typedef i64 long

定义别名

Thrift常量

thrift也支持常量定义,使用const关键字:

const i32 MAX_RETRIES_TIME=10
const string MY_WEBSITE=
"https://blog.c .net/BADAO_LIUMANG_QIZHI'

命名空间

Thrift的命名空间相当于Java中的package的意思,主要目的是组织代码。thrift使用关键字namespace 定义命名空间:

namespace java com.bdao.thrift

格式是: namespace 语言名 路径

文件包含

Thrift也支持文件包含,相当于C/C++中的include,Java中的import。使用关键字include定义:

include "global.thrift"

注释

Thrift注释方式支持shell风格的注释,支持C/C++风格的注释,即#和//开头的语句都当做注释,/**/包裹的语句也是注释。

可选与必选

Thrift提供两个关键字required, optional,分别用于表示对应的字段是必填的还是可选的

struct People{
  1:required string name;
  2:optional i32 age;
}

Thrift IDL示例文件

namespace c_glib TTest
namespace cpp thrift.test
namespace delphi Thrift.Test
namespace go thrifttest
namespace java thrift.test
namespace js ThriftTest
namespace lua ThriftTest
namespace netstd ThriftTest
namespace perl ThriftTest
namespace php ThriftTest
namespace py ThriftTest
namespace py.twisted ThriftTest
namespace rb Thrift.Test
namespace st ThriftTest
namespace xsd test (uri = 'http://thrift.apache.org/ns/ThriftTest')

// Presence of namespaces and sub-namespaces for which there is
// no generator should compile with warnings only
namespace noexist ThriftTest
namespace cpp.noexist ThriftTest

namespace * thrift.test

/**
 * Docstring!
 */
enum Numberz
{
  ONE = 1,
  TWO,
  THREE,
  FIVE = 5,
  SIX,
  EIGHT = 8
}

const Numberz myNumberz = Numberz.ONE;
// the following is expected to fail:
// const Numberz urNumberz = ONE;

typedef i64 UserId

struct Bonk
{
  1: string message,
  2: i32 type
}

typedef map<string,Bonk> MapType

struct Bools {
  1: bool im_true,
  2: bool im_false,
}

struct Xtruct
{
  1:  string string_thing,
  4:  i8     byte_thing,
  9:  i32    i32_thing,
  11: i64    i64_thing
}

struct Xtruct2
{
  1: i8     byte_thing,  // used to be byte, hence the name
  2: Xtruct struct_thing,
  3: i32    i32_thing
}

struct Xtruct3
{
  1:  string string_thing,
  4:  i32    changed,
  9:  i32    i32_thing,
  11: i64    i64_thing
}


struct Insanity
{
  1: map<Numberz, UserId> userMap,
  2: list<Xtruct> xtructs
} (python.immutable= "")

struct CrazyNesting {
  1: string string_field,
  2: optional set<Insanity> set_field,
  // Do not insert line break as test/go/Makefile.am is removing this line with pattern match
  3: required list<map<set<i32> (python.immutable = ""), map<i32,set<list<map<Insanity,string>(python.immutable = "")> (python.immutable = "")>>>> list_field,
  4: binary binary_field
}

union SomeUnion {
  1: map<Numberz, UserId> map_thing,
  2: string string_thing,
  3: i32 i32_thing,
  4: Xtruct3 xtruct_thing,
  5: Insanity insanity_thing
}

exception Xception {
  1: i32 errorCode,
  2: string message
}

exception Xception2 {
  1: i32 errorCode,
  2: Xtruct struct_thing
}

struct EmptyStruct {}

struct OneField {
  1: EmptyStruct field
}

service ThriftTest
{
  /**
   * Prints "testVoid()" and returns nothing.
   */
  void         testVoid(),

  /**
   * Prints 'testString("%s")' with thing as '%s'
   * @param string thing - the string to print
   * @return string - returns the string 'thing'
   */
  string       testString(1: string thing),

  /**
   * Prints 'testBool("%s")' where '%s' with thing as 'true' or 'false'
   * @param bool  thing - the bool data to print
   * @return bool  - returns the bool 'thing'
   */
  bool         testBool(1: bool thing),

  /**
   * Prints 'testByte("%d")' with thing as '%d'
   * The types i8 and byte are synonyms, use of i8 is encouraged, byte still exists for the sake of compatibility.
   * @param byte thing - the i8/byte to print
   * @return i8 - returns the i8/byte 'thing'
   */
  i8           testByte(1: i8 thing),

  /**
   * Prints 'testI32("%d")' with thing as '%d'
   * @param i32 thing - the i32 to print
   * @return i32 - returns the i32 'thing'
   */
  i32          testI32(1: i32 thing),

  /**
   * Prints 'testI64("%d")' with thing as '%d'
   * @param i64 thing - the i64 to print
   * @return i64 - returns the i64 'thing'
   */
  i64          testI64(1: i64 thing),

  /**
   * Prints 'testDouble("%f")' with thing as '%f'
   * @param double thing - the double to print
   * @return double - returns the double 'thing'
   */
  double       testDouble(1: double thing),

  /**
   * Prints 'testBinary("%s")' where '%s' is a hex-formatted string of thing's data
   * @param binary  thing - the binary data to print
   * @return binary  - returns the binary 'thing'
   */
  binary       testBinary(1: binary thing),

  /**
   * Prints 'testStruct("{%s}")' where thing has been formatted into a string of comma separated values
   * @param Xtruct thing - the Xtruct to print
   * @return Xtruct - returns the Xtruct 'thing'
   */
  Xtruct       testStruct(1: Xtruct thing),

  /**
   * Prints 'testNest("{%s}")' where thing has been formatted into a string of the nested struct
   * @param Xtruct2 thing - the Xtruct2 to print
   * @return Xtruct2 - returns the Xtruct2 'thing'
   */
  Xtruct2      testNest(1: Xtruct2 thing),

  /**
   * Prints 'testMap("{%s")' where thing has been formatted into a string of 'key => value' pairs
   *  separated by commas and new lines
   * @param map<i32,i32> thing - the map<i32,i32> to print
   * @return map<i32,i32> - returns the map<i32,i32> 'thing'
   */
  map<i32,i32> testMap(1: map<i32,i32> thing),

  /**
   * Prints 'testStringMap("{%s}")' where thing has been formatted into a string of 'key => value' pairs
   *  separated by commas and new lines
   * @param map<string,string> thing - the map<string,string> to print
   * @return map<string,string> - returns the map<string,string> 'thing'
   */
  map<string,string> testStringMap(1: map<string,string> thing),

  /**
   * Prints 'testSet("{%s}")' where thing has been formatted into a string of values
   *  separated by commas and new lines
   * @param set<i32> thing - the set<i32> to print
   * @return set<i32> - returns the set<i32> 'thing'
   */
  set<i32>     testSet(1: set<i32> thing),

  /**
   * Prints 'testList("{%s}")' where thing has been formatted into a string of values
   *  separated by commas and new lines
   * @param list<i32> thing - the list<i32> to print
   * @return list<i32> - returns the list<i32> 'thing'
   */
  list<i32>    testList(1: list<i32> thing),

  /**
   * Prints 'testEnum("%d")' where thing has been formatted into its numeric value
   * @param Numberz thing - the Numberz to print
   * @return Numberz - returns the Numberz 'thing'
   */
  Numberz      testEnum(1: Numberz thing),

  /**
   * Prints 'testTypedef("%d")' with thing as '%d'
   * @param UserId thing - the UserId to print
   * @return UserId - returns the UserId 'thing'
   */
  UserId       testTypedef(1: UserId thing),

  /**
   * Prints 'testMapMap("%d")' with hello as '%d'
   * @param i32 hello - the i32 to print
   * @return map<i32,map<i32,i32>> - returns a dictionary with these values:
   *   {-4 => {-4 => -4, -3 => -3, -2 => -2, -1 => -1, }, 4 => {1 => 1, 2 => 2, 3 => 3, 4 => 4, }, }
   */
  map<i32,map<i32,i32>> testMapMap(1: i32 hello),

  /**
   * So you think you've got this all worked out, eh?
   *
   * Creates a map with these values and prints it out:
   *   { 1 => { 2 => argument,
   *            3 => argument,
   *          },
   *     2 => { 6 => <empty Insanity struct>, },
   *   }
   * @return map<UserId, map<Numberz,Insanity>> - a map with the above values
   */
  map<UserId, map<Numberz,Insanity>> testInsanity(1: Insanity argument),

  /**
   * Prints 'testMulti()'
   * @param i8 arg0 -
   * @param i32 arg1 -
   * @param i64 arg2 -
   * @param map<i16, string> arg3 -
   * @param Numberz arg4 -
   * @param UserId arg5 -
   * @return Xtruct - returns an Xtruct with string_thing = "Hello2, byte_thing = arg0, i32_thing = arg1
   *    and i64_thing = arg2
   */
  Xtruct testMulti(1: i8 arg0, 2: i32 arg1, 3: i64 arg2, 4: map<i16, string> arg3, 5: Numberz arg4, 6: UserId arg5),

  /**
   * Print 'testException(%s)' with arg as '%s'
   * @param string arg - a string indication what type of exception to throw
   * if arg == "Xception" throw Xception with errorCode = 1001 and message = arg
   * else if arg == "TException" throw TException
   * else do not throw anything
   */
  void testException(1: string arg) throws(1: Xception err1),

  /**
   * Print 'testMultiException(%s, %s)' with arg0 as '%s' and arg1 as '%s'
   * @param string arg - a string indicating what type of exception to throw
   * if arg0 == "Xception" throw Xception with errorCode = 1001 and message = "This is an Xception"
   * else if arg0 == "Xception2" throw Xception2 with errorCode = 2002 and struct_thing.string_thing = "This is an Xception2"
   * else do not throw anything
   * @return Xtruct - an Xtruct with string_thing = arg1
   */
  Xtruct testMultiException(1: string arg0, 2: string arg1) throws(1: Xception err1, 2: Xception2 err2)

  /**
   * Print 'testOneway(%d): Sleeping...' with secondsToSleep as '%d'
   * sleep 'secondsToSleep'
   * Print 'testOneway(%d): done sleeping!' with secondsToSleep as '%d'
   * @param i32 secondsToSleep - the number of seconds to sleep
   */
  oneway void testOneway(1:i32 secondsToSleep)
}

service SecondService
{
  /**
   * Prints 'testString("%s")' with thing as '%s'
   * @param string thing - the string to print
   * @return string - returns the string 'thing'
   */
  string secondtestString(1: string thing)
}

struct VersioningTestV1 {
       1: i32 begin_in_both,
       3: string old_string,
       12: i32 end_in_both
}

struct VersioningTestV2 {
       1: i32 begin_in_both,

       2: i32 newint,
       3: i8 newbyte,
       4: i16 newshort,
       5: i64 newlong,
       6: double newdouble
       7: Bonk newstruct,
       8: list<i32> newlist,
       9: set<i32> newset,
       10: map<i32, i32> newmap,
       11: string newstring,
       12: i32 end_in_both
}

struct ListTypeVersioningV1 {
       1: list<i32> myints;
       2: string hello;
}

struct ListTypeVersioningV2 {
       1: list<string> strings;
       2: string hello;
}

struct GuessProtocolStruct {
  7: map<string,string> map_field,
}

struct LargeDeltas {
  1: Bools b1,
  10: Bools b10,
  100: Bools b100,
  500: bool check_true,
  1000: Bools b1000,
  1500: bool check_false,
  2000: VersioningTestV2 vertwo2000,
  2500: set<string> a_set2500,
  3000: VersioningTestV2 vertwo3000,
  4000: list<i32> big_numbers
}

struct NestedListsI32x2 {
  1: list<list<i32>> integerlist
}
struct NestedListsI32x3 {
  1: list<list<list<i32>>> integerlist
}
struct NestedMixedx2 {
  1: list<set<i32>> int_set_list
  2: map<i32,set<string>> map_int_strset
  3: list<map<i32,set<string>>> map_int_strset_list
}
struct ListBonks {
  1: list<Bonk> bonk
}
struct NestedListsBonk {
  1: list<list<list<Bonk>>> bonk
}

struct BoolTest {
  1: optional bool b = true;
  2: optional string s = "true";
}

struct StructA {
  1: required string s;
}

struct StructB {
  1: optional StructA aa;
  2: required StructA ab;
}

struct OptionalSetDefaultTest {
  1: optional set<string> with_default = [ "test" ]
}

Thrift传输

 

Thrift介绍以及Java中使用Thrift实现RPC示例_Thrift

thrift通过一个中间语言IDL(接口定义语言)来定义RPC的数据类型和接口,这些内容写在以.thrift结尾的文件中,然后通过特殊的编译器来生成不同语言的代码

,以满足不同需要的开发者,比如java开发者,就可以生成java代码,c++开发者可以生成c++代码,生成的代码中不但包含目标语言的接口定义,方法,数据类型,

还包含有RPC协议层和传输层的实现代码。

图中,TProtocol(协议层),定义数据传输格式,例如:

TBinaryProtocol:二进制格式;
TCompactProtocol:压缩格式;
TJSONProtocol:JSON格式;
TSimpleJSONProtocol:提供JSON只写协议, 生成的文件很容易通过脚本语言解析;
TDebugProtocol:使用易懂的可读的文本格式,以便于debug
TTransport(传输层),定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。

TSocket:阻塞式socker;
TFramedTransport:以frame为单位进行传输,非阻塞式服务中使用;
TFileTransport:以文件形式进行传输;
TMemoryTransport:将内存用于I/O,java实现时内部实际使用了简单的ByteArrayOutputStream;
TZlibTransport:使用zlib进行压缩, 与其他传输方式联合使用,当前无java实现;
 

Thrift支持的服务模型

TSimpleServer:简单的单线程服务模型,常用于测试;
TThreadPoolServer:多线程服务模型,使用标准的阻塞式IO;
TNonblockingServer:多线程服务模型,使用非阻塞式IO(需使用TFramedTransport数据传输方式);

 

Java中使用Thrift实现RPC示例

首先需要下载并配置thrift的编译器,下载地址

http://thrift.apache.org/download

这里下载Windows版本为例

 

Thrift介绍以及Java中使用Thrift实现RPC示例_Thrift_02

下载thrift的exe文件

 

Thrift介绍以及Java中使用Thrift实现RPC示例_thrift_03

将其下载到磁盘中某目录,然后在环境变量的的系统变量的Path中将exe所在的路径添加。

 

Thrift介绍以及Java中使用Thrift实现RPC示例_Thrift_04

这样就能在任何地方使用thrift-0.13.0.exe了,为了使用命令方便,将此exe命名为thrift.exe

然后打开cmd ,输入

thrift -version

 

Thrift介绍以及Java中使用Thrift实现RPC示例_RPC_05

出现如上则配置成功。

这里使用Gradle作为依赖管理,当然也可以使用Maven,只是管理方式不同而已。

Gradle在Windows下的下载安装与配置以及在IDEA中配置以及修改jar包位置:

 

参照上面在IDEA中进行搭建Gradle项目

然后在build.gradle中引入thrift的相关依赖,在Maven中央仓库中搜搜thrift

 

Thrift介绍以及Java中使用Thrift实现RPC示例_RPC_06

选择与编译器对应的版本这里是0.13.0

 

Thrift介绍以及Java中使用Thrift实现RPC示例_thrift_07

选择gradle的依赖配置进行复制

然后复制到build.gradle中

dependencies {
    compile (
            [group:'org.apache.thrift', name: 'libthrift', version: '0.13.0']
    )
}

因为设置了自动导入,所以会在项目中自动引入thrift相关的依赖

 

Thrift介绍以及Java中使用Thrift实现RPC示例_thrift_08

然后在src下新建thrift目录,在此目录下新建MyData.thrift文件

namespace java thrift.generated

typedef i16 short
typedef i32 int
typedef i64 long
typedef bool boolean
typedef string String

struct Person {
    1:optional String username,
    2:optional int age,
    3:optional boolean married
}

exception DataException {
    1:optional String message,
    2:optional String callback,
    3:optional String date
}

service PersonService {
    Person getPersonByUsername(1:required String username) throws (1:DataException dataException),
    void savePerson(1:required Person person) throws(1:DataException dataException)
}

然后在IDEA中新建Terminal

thrift --gen java src/thrift/MyData.thrift

后面跟的目录就是thrift文件所在的路径

 

Thrift介绍以及Java中使用Thrift实现RPC示例_RPC_09

此时会在项目的根目录下生成代码,然后在src/main/java/com.badao下新建一个目录Thrift,然后将上面生成的代码复制到此路径下。

此时代码就已经生成好,

在此目录下新建PersonServiceImpl并实现生成的PersonService接口

package com.badao.Thrift;

import org.apache.thrift.TException;

public class PersonServiceImpl implements PersonService.Iface {
    @Override
    public Person getPersonByUsername(String username) throws DataException, TException {
        System.out.println("getPersonByUsername被调用并收到参数:" + username);
        Person person = new Person();
        person.setUsername("公众号:霸道的程序猿");
        person.setAge(100);
        person.setMarried(true);

        return person;
    }

    @Override
    public void savePerson(Person person) throws DataException, TException {
        System.out.println("savePerson方法被调用,接收到的参数为:");
        System.out.println(person.getUsername());
        System.out.println(person.getAge());
        System.out.println(person.isMarried());
    }
}

并且实现其两个方法,在根据用户名获取Person实体的方法中

输出接收到的username参数并将一个Person对象返回。

在保存Person的方法中将接收到的Person对象进行输出。

然后在此目录下新建服务端类ThriftServer

package com.badao.Thrift;

import org.apache.thrift.TProcessorFactory;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.server.THsHaServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;


public class ThriftServer {
    public static void main(String[] args) throws Exception
    {
        //设置服务器端口  TNonblockingServerSocket-非堵塞服务模型
        TNonblockingServerSocket serverSocket = new TNonblockingServerSocket(8899);
        //参数设置
        THsHaServer.Args arg = new THsHaServer.Args(serverSocket).minWorkerThreads(2).maxWorkerThreads(4);
        //处理器
        PersonService.Processor<PersonServiceImpl> processor = new PersonService.Processor<>(new PersonServiceImpl());
        arg.protocolFactory(new TCompactProtocol.Factory());
        arg.transportFactory(new TFramedTransport.Factory());
        arg.processorFactory(new TProcessorFactory(processor));

        TServer server = new THsHaServer(arg);
        System.out.println("Thrift 服务端启动成功");
        server.serve();
    }


}

绑定端口并启动服务端。

然后在此路径下新建客户端ThriftClient

package com.badao.Thrift;

import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

public class ThriftClient {
    public static void main(String[] args) {
        TTransport transport = new TFramedTransport(new TSocket("localhost", 8899), 600);
        TProtocol protocol = new TCompactProtocol(transport);
        PersonService.Client client = new PersonService.Client(protocol);

        try {
            transport.open();

            Person person = client.getPersonByUsername("公众号:霸道的程序猿");
            System.out.println(person.getUsername());
            System.out.println(person.getAge());
            System.out.println(person.isMarried());

            System.out.println("------------------------");

            Person person1 = new Person();
            person1.setUsername("公众号:霸道的程序猿");
            person1.setAge(50);
            person1.setMarried(true);

            client.savePerson(person1);

        }catch (Exception ex){
            throw new RuntimeException(ex.getMessage(),ex);
        }finally {
            transport.close();
        }
    }
}

与服务端建立连接并调用服务端的两个方法,然后运行服务端

 

Thrift介绍以及Java中使用Thrift实现RPC示例_RPC_10

然后再运行客户端

 

Thrift介绍以及Java中使用Thrift实现RPC示例_Thrift_11

此时客户端已经调用getPersonByUsername并输出数据并且调用了服务端的savePerson方法

 

Thrift介绍以及Java中使用Thrift实现RPC示例_thrift_12