Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是,Thrift并不仅仅是序列化协议,而是一个RPC框架。相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案;但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。

典型应用场景和非应用场景

对于需求为高性能,分布式的RPC服务,Thrift是一个优秀的解决方案。它支持众多语言和丰富的数据类型,并对于数据字段的增删具有较强的兼容性。所以非常适用于作为公司内部的面向服务构建(SOA)的标准RPC框架。

不过Thrift的文档相对比较缺乏,目前使用的群众基础相对较少。另外由于其Server是基于自身的Socket服务,所以在跨防火墙访问时,安全是一个顾虑,所以在公司间进行通讯时需要谨慎。 另外Thrift序列化之后的数据是Binary数组,不具有可读性,调试代码时相对困难。最后,由于Thrift的序列化和框架紧耦合,无法支持向持久层直接读写数据,所以不适合做数据持久化序列化协议。

IDL文件举例

struct Address
{ 
    1: required string city;
    2: optional string postcode;
    3: optional string street;
} 
struct UserInfo
{ 
    1: required string userid;
    2: required i32 name;
    3: optional list<Address> address;
}

IDL语法学习:https://my.oschina.net/helight/blog/195015

安装

先要下载安装相应的IDL文件编译工具。

官方网站:https://thrift.apache.org/download

吐槽:都多少年的东西了,版本号居然是0.13.0? Are you kidding me?看看Chrome的版本号,你们真是不会卖!

Windows

比如:http://www.apache.org/dyn/closer.cgi?path=/thrift/0.13.0/thrift-0.13.0.exe

安装 Thrift 的 IDL 编译工具。下载完成后改名为:thrift.exe 并将其放入到系统环境变量下即可使用。

Linux

# 从 github 上下载 thrift 0.13.0 的源码,解压后进入:thrift-0.13.0/compiler/cpp
# 目录执行如下命令完成编译后,将其放入到系统环境变量下即可使用:
$ mkdir cmake-build
$ cd cmake-build
$ cmake ..
$ make

Linux安装过程参考:https://www.cnblogs.com/apocelipes/p/9420825.html

验证是否安装成功:

thrift -version # 看是否看到:Thrift version 0.13.0

Go平台开发包

go get git.apache.org/thrift.git/lib/go/thrift

下面github上面的包最好不要安装,会有很多错误:

// 慎用这个
// go get github.com/apache/thrift/lib/go/thrift/

// 如果不小心装了就到 GOPATH\bin\pkg\mod\github.com 把他们删了。

Go Thrift 示例

本示例的目录结果如下

├─client
│      client.go
├─example
|      ......
├─server
│      server.go
└─thrift_file
       example.thrift

Thrift接口描述文件,我们使用 Thrift 定义一个接口,该接口实现对传入的数据进行大写的格式化处理。

// example.thrift
namespace py example

struct Data {
    1: string text
}

service format_data {
    Data do_format(1:Data data),
}

命令行控制台定位到thrift_file目录下,执行:

thrift -out .. --gen go example.thrift

thrift_file的同级目录下会生成包example,其中 format_data-remote是生成的测试代码可以不用特别关注。

服务器端代码:

 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
// server.go
package server

import (
	"context"
	"fmt"
	"git.apache.org/thrift.git/lib/go/thrift"
	"log"
	"strings"
	"yufa/demo/thrift_demo/example"
)

type FormatDataImpl struct{}

// DoFormat(ctx context.Context, data *Data) (r *Data, err error)
func (fdi *FormatDataImpl) DoFormat(ctx context.Context, data *example.Data) 
	(r *example.Data, err error) {
	var rData example.Data
	rData.Text = strings.ToUpper(data.Text)

	return &rData, nil
}

const (
	HOST = "localhost"
	PORT = "8080"
)

func ThriftDemoServer() {
	handler := &FormatDataImpl{}
	processor := example.NewFormatDataProcessor(handler)
	serverTransport, err := thrift.NewTServerSocket(HOST + ":" + PORT)
	if err != nil {
		log.Fatalln("Error: ", err)
	}
	transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
	protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()

	server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory)
	fmt.Println("Running at: ", HOST+":"+PORT)
	_ = server.Serve()
}

客户端代码:

 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
// client.go
package client

import (
	"fmt"
	"git.apache.org/thrift.git/lib/go/thrift"
	"log"
	"net"
	"yufa/demo/thrift_demo/example"
)

const (
	HOST = "localhost"
	PORT = "8080"
)

func ThriftDemoClient() {
	tSocket, err := thrift.NewTSocket(net.JoinHostPort(HOST, PORT))
	if err != nil {
		log.Fatalln("tSocket error: ", err)
	}
	transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
	transport, _ := transportFactory.GetTransport(tSocket)
	protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()

	client := example.NewFormatDataClientFactory(transport, protocolFactory)

	if err := transport.Open(); err != nil {
		log.Fatalln("Error opening: ", HOST+":"+PORT)
	}
	defer transport.Close()

	data := example.Data{Text: "hello world, chende!"}
	d, err := client.DoFormat(nil, &data)
	if d != nil {
		fmt.Println(d.Text)
	}
}

先启动服务器端,再启动客户端,在客户端控制台会看到:HELLO, WORLD, CHEN DE!全部转换成了大写。

Go Thrift示例2

接口和数据定义

// demo.thrift
namespace go compute

struct Result {
    1: i64 div;
    2: i64 mod;
}

service DivMod {
    Result DoDivMod(1:i64 arg1, 2:i64 arg2);
}

service MulRange {
    string BigRange(1:i64 max)
}
// 编译
thrift -out .. --gen go demo.thrift

实现服务端

步骤如下:

  1. 实现服务处理接口,在最新版本中,接口第一个参数需要是context.Context,用以支持取消或对RPC调用设置超时。
  2. 创建 Processor,也就是把实现的接口注册进RPC服务创建 Transport,Transport用于管理网络IO。它是一个interface,可以是TSocket,TServerSocket以及NewTHttpClient*或TTransportFactory.GetTransport返回的对象。
  3. 创建 Protocol,也就是传输协议,一般使用二进制协议来传输数据,当然也可以选择json或其他格式。
  4. 创建server,使用前面的Processor,Transport,Protocol以及ip地址来创建服务端。
  5. 运行server。
 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
import (
	"context"
	"fmt"
	"git.apache.org/thrift.git/lib/go/thrift"
	"math/big"
	"net"
	"os"
	"yufa/demo/thrift_demo/compute"
)

// computeThrift 实现service中定义的方法
type divmodThrift struct {}

// 每个方法除了定义的返回值之外还要返回一个error,包括定义成void的方法。自定义
// 类型会在名字之后加一条下划线;暂时用不到context,所以忽略
func (d *divmodThrift) DoDivMod(_ context.Context, arg1, arg2 int64) 
	(*compute.Result_, error) {
	divRes := int64(arg1 / arg2)
	modRes := int64(arg1 % arg2)
	// 生成的用于生成自定义数据对象的函数
	res := compute.NewResult_()
	res.Div = divRes
	res.Mod = modRes

	return res, nil
}

// 尽量一个struct对应一个service
type mulrangeThrift struct {}

func (m *mulrangeThrift) BigRange(_ context.Context, max int64) (string, error) {
	result := new(big.Int)
	result.SetString("1", 10)
	result = result.MulRange(2, max)

	return result.String(), nil
}

// 启动服务器
func ThriftDemoRPCServer() {
	// 创建服务器
	serverTransport, err := thrift.NewTServerSocket(net.JoinHostPort("127.0.0.1", "8081"))
	if err != nil {
		fmt.Println("Server Start Socket Error !", err)
		os.Exit(1)
	}

	// 创建二进制协议
	transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
	protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()

	// 创建Processor,用一个端口处理多个服务
	divmodProcessor := compute.NewDivModProcessor(new(divmodThrift))
	mulrangeProcessor := compute.NewMulRangeProcessor(new(mulrangeThrift))

	multiProcessor := thrift.NewTMultiplexedProcessor()
	// 给每个service起一个名字
	multiProcessor.RegisterProcessor("divmod", divmodProcessor)
	multiProcessor.RegisterProcessor("mulrange", mulrangeProcessor)

	// 启动服务器
	server := thrift.NewTSimpleServer4(multiProcessor, serverTransport,
                                       transportFactory, protocolFactory)
	server.Serve()
	// 退出时停止服务器
	defer server.Stop()
}

启动服务器这里也有坑点,网上有文章说使用NewTSimpleServer2可以直接启用二进制格式的数据传输,这是错的。想用二进制或其他格式传输数据,必须明确生成对应的ProtocolFactory并使用NewTSimpleServer4创建服务器。官方给的例子就是这样做的,我也是在被反复坑了数次之后才确认了这个问题。

实现客户端

要建立客户端,也要按照如下几个步骤:

  1. 创建 Transport
  2. 创建 Protocol
  3. 基于 Potocol 创建 Client
  4. 打开 Transport(不一定要在client创建后才打开,但必须在protocol创建后,接口调用前打开)
  5. 调用接口
 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
import (
	"context"
	"fmt"
	"git.apache.org/thrift.git/lib/go/thrift"
	"net"
	"os"
	"yufa/demo/thrift_demo/compute"
)

func ThriftDemoRPCClient() {
	// 先建立和服务器的连接的socket,再通过socket建立Transport
	socket, err := thrift.NewTSocket(net.JoinHostPort("127.0.0.1", "8081"))
	if err != nil {
		fmt.Println("Error opening socket:", err)
		os.Exit(1)
	}
	transport := thrift.NewTFramedTransport(socket)

	// 创建二进制协议
	protocol := thrift.NewTBinaryProtocolTransport(transport)
	// 打开Transport,与服务器进行连接
	if err := transport.Open(); err != nil {
		_, _ = fmt.Fprintln(os.Stderr, "Error opening 127.0.0.1:8081 ", err)
		os.Exit(1)
	}
	defer transport.Close()

	// 接口需要context,以便在长操作时用户可以取消RPC调用
	ctx := context.Background()

	// 使用divmod服务
	divmodProtocol := thrift.NewTMultiplexedProtocol(protocol, "divmod")
	// 创建代理客户端,使用TMultiplexedProtocol访问对应的服务
	c := thrift.NewTStandardClient(divmodProtocol, divmodProtocol)

	client := compute.NewDivModClient(c)
	res, err := client.DoDivMod(ctx, 100, 3)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println(res)

	// 使用mulrange服务
	// 步骤与上面的相同
	mulProtocol := thrift.NewTMultiplexedProtocol(protocol, "mulrange")
	c = thrift.NewTStandardClient(mulProtocol, mulProtocol)
	client2 := compute.NewMulRangeClient(c)
	num, err := client2.BigRange(ctx, 100)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println(num)
}

控制台中看到客户端执行结果:

Result_({Div:33 Mod:1})
933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565
18286253697920827223758251185210916864000000000000000000000000

(完)