作者 青鸟

Thrift的简介

Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。

Thrift的主要优点在于它支持多种编程语言,包括 C++, Java, Python, PHP, Ruby, Erlang, Perl, Go, C#, JavaScript, Node.js 等。

由于Thrift的跨语言特性,因此可以广泛地运用于各种远程Rpc调用

IDL的简单语法

Thrift之所以可以做到跨平台,是因为Thrift采用IDL(Interface Definition Language)来定义通用的服务接口,并通过生成不同的语言代理实现来达到跨语言、平台的功能。在thrift的IDL中可以定义以下一些类型:基本数据类型,结构体,容器,异常、服务

Thrift 中的类型包括基础类型、结构、容器、异常、服务等几个部分。官网介绍

  1. 基本类型

• bool :布尔型,1个字节

• byte :有符号整数,1个字节

• i16 :有符号16位整型

• i32 :有符号32位整型

• i64 :有符号64位整型

• double :64位浮点数值

• string :字符串类型,使用UTF-8编码

  1. 特殊类型

• binary :二进制数据类型(字节数组)

  1. 结构

Thrift 结构定义了一个公共对象,它们本质上等同于OOP语言中的类,但没有继承。结构有一组强类型字段,每个字段都具有唯一的名称标识符。字段可能有各种注释(数字字段ID、可选默认值等)。thrift IDL(Interface Definition Language)规范详见官网介绍

示例:

1
2
3
4
struct SharedStruct {
  1: i32 key = 0,
  2: string value,
}
  1. 结构体中每一个域都有一个正整数标识符,这个标识符并不要求连续,但一旦定义,不建议再进行修改。

  2. 每个域前都会有required或optional:

• required:必填域,如果设置了 required,在实际构造结构体时就必须给这个域赋值,否则 thrift 会认为这是一个异常。

• optional:可选域,如果设置了 optional,在实际构造结构体时没有给这个域赋值,则在使用这个结构体时,就会忽略掉这个 optional 域。

• 域是可以设置默认值的,如上key = 0。

  1. 容器

有三种容器类型:

• list :单类型有序列表,允许有重复元素,会被转换成C++中的vector,Java中的 ArrayLis等

• set :单类型无需集合,不允许有重复元素,会转换成C++中的set,Java中的HashSet、Python中的Set等

• map<t1,t2> :Map型(key:value),会被转换成C++中的map,Java中的HashMap,PHP中的关联数组等

容器元素可以是任何有效的Thrift 类型,注意:map 的key类型需要是基础类型,很多开发语言并不支持 map 的 key 类型为复杂数据类型。

  1. 异常

异常在语法上和结构的用法是完全一致的,不过 exception 是在远程调用发生异常时用来抛出异常用的。

  1. 服务

服务是由 Thrift 类型定义的。服务的定义在语义上等同于面向对象中定义接口(或纯虚拟抽象类)。Thrift 编译工具会根据定义的服务来生成对应的方法和函数。

  1. 示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * .thrift文件可以引用其他.thrift文件,这样可以方便地把一些公共结构和服务引入进来。
 * 在引用其他.thrift文件时,既可以直接引用当前文件夹下的文件,也可以引用其他路径下的文件,但后者需要在thrift编译工具编译时加上 -I 选项来设定路径。
 * 如果希望访问被包含的.thrift文件中的内容,则需要使用.thrift文件的文件名作为前缀,比如 shared.SharedObject
 * 本例中引用了文件 shared.thrift
*/
include "shared.thrift"
/**
 * thrift支持针对不同的语言设置不同的 namespacethrift会在生成不同语言代码时,进行相应的设置。
*/
namespace java tutorial
namespace go tutorial
// 使用 typedef 定义别名
typedef i32 MyInteger
/**
 * 定义常量. 复杂的类型和结构体使用JSON表示法. 
 */
const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}
/**
 * 枚举是32位整型数字, 如果没有显式指定值,默认从1开始编号并递增.
 */
enum Operation {
  ADD = 1,
  SUBTRACT = 2,
  MULTIPLY = 3,
  DIVIDE = 4
}
/**
 * 结构体由一组成员来组成, 一个成员包括数字标识符, 类型, 符号名, 及一个可选的默认值.
 * 每个域都可以设置为optional或required来表示是否为必填域
 * 以便thrift决定是否在数据传输时要包含这个域
 * 不指定时,默认为required
 */
struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}
// 结构体也可以作为异常,在语法上,异常的定义方式和结构体是完全一样的
exception InvalidOperation {
  1: i32 whatOp,
  2: string why
}
/**
 * 定义服务需要一个服务名, 加上一个可选的继承, 使用extends关键字 
 */
service Calculator extends shared.SharedService {
 
  /**
   * 服务中方法的定义非常类似于C语言的语法。包括一个返回值,
   * 一个参数列表以及一个可以抛出的异常列表(可选)
   * 定义参数列表的方法、定义异常列表的方法,和定义结构体的方法
   * 都是相似的,可以从下面的例子中看出。
   * 注意,除了最后一个方法,其他的方法最后都要有一个逗号。
   */
 
   void ping(),
 
   i32 add(1:i32 num1, 2:i32 num2),
   
   /**
   * 在异常列表前,需要加throws关键字
   */
   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
 
   /**
   * 这个方法有 oneway 修饰符, 表示客户段发送一个请求后会立即返回,
   * 不会等待回应(oneway在英语里就是“单向”的意思)。
   * oneway 方法的返回值必须是 void
   */
   oneway void zip()
 
}

在Go中使用Thrift

首先要做到是编写对应的IDL文件用于生成对应的语言代理,下面给出一个示例,hello.thrift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace go hello

struct HelloReq {
    1: string name;
}

struct HelloRes {
    1: string greeting;
}

service Hello {
    HelloRes greet(1: HelloReq req);
}

在控制台中执行下面的命令来生成对应的语言代理文件

1
thrift -r --gen go hello.thrift

执行成功后会生成对应的go-gen文件夹,里面便是对应的语言代理代码

然后是编写服务端的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
	"context"
	"fmt"

	"experiment/thrift/gen-go/hello"
	"github.com/apache/thrift/lib/go/thrift"
)

//定义一个接口,里面定义的函数便是需要远程调用的方法
type HelloHandler struct{}

//远程调用的函数Greet
func (h *HelloHandler) Greet(ctx context.Context, req *hello.HelloReq) (*hello.HelloRes, error) {
	fmt.Printf("Received request from %s\n", req.GetName())

	res := &hello.HelloRes{
		Greeting: fmt.Sprintf("Hello, %s!", req.GetName()),
	}

	return res, nil
}

func main() {
	handler := &HelloHandler{}
	processor := hello.NewHelloProcessor(handler)

	serverTransport, err := thrift.NewTServerSocket(":9090")
	if err != nil {
		panic(err)
	}

	transportFactory := thrift.NewTTransportFactory()
	protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()

	server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory)
	fmt.Println("Starting the server...")

	if err := server.Serve(); err != nil {
		panic(err)
	}
}

最后是客服端远程调用greet()的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"context"
	"fmt"
	"net"

	"experiment/thrift/gen-go/hello"
	"github.com/apache/thrift/lib/go/thrift"
)

func main() {
  //定义监听的远程端口和地址
	transport, err := thrift.NewTSocket(net.JoinHostPort("localhost", "9090"))
	if err != nil {
		panic(err)
	}
	defer transport.Close()

	if err := transport.Open(); err != nil {
		panic(err)
	}

	protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
	client := hello.NewHelloClientFactory(transport, protocolFactory)

	req := &hello.HelloReq{
		Name: "world",
	}

	res, err := client.Greet(context.Background(), req)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Response from server: %s\n", res.GetGreeting())
}

这样就是一次完整的thrift的远程调用

参考资料:

百度百科

Go语言 thrift 入门指南–thrift IDL介绍