Thrift最初由Facebook研发,主要用于各个服务之间的RPC通信,支持跨语言,常用的语言比如C++, Java, golang,Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml都支持。Thrift是一个典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。
Thrift 是一种被广泛使用的 rpc 框架,可以比较灵活的定义数据结构和函数输入输出参数,并且可以跨语言调用。为了保证服务接口的统一性和可维护性,我们需要在最开始就制定一系列规范并严格遵守,降低后续维护成本。
我们公司主要用于C++,Java,golang跨语言调用。
本节介绍Thrift的接口定义语言,Thrift IDL支持的数据类型包含:
thrift不支持无符号类型,因为很多编程语言不存在无符号类型,比如java
集合中的元素可以是除了service之外的任何类型,包括exception。
就像C语言一样,thrift也支持struct类型,目的就是将一些数据聚合在一起,方便传输管理。struct的定 义形式如下:
struct People {
1: string name;
2: i32 age;
3: string sex;
}
枚举的定义形式和Java的Enum定义差不多,例如:
enum Sex {
MALE, FEMALE
}
thrift支持自定义exception,规则和struct一样,如下:
exception RequestException {
1: i32 code;
2: string reason;
}
thrift定义服务相当于Java中创建Interface一样,创建的service经过代码生成命令之后就会生成客户端和服务端的框架代码。定义形式如下:
service HelloWordService { // service中定义的函数,相当于Java interface中定义的函数 string doAction(1: string name, 2: i32 age); }
thrift支持类似C++一样的typedef定义,比如:
typedef i32 Integer
typedef i64 Long
注意,末尾没有逗号或者分号
thrift也支持常量定义,使用const关键字,例如:
const i32 MAX_RETRIES_TIME = 10
const string MY_WEBSITE = "http://qifuguang.me";
末尾的分号是可选的,可有可无,并且支持16进制赋值
thrift的命名空间相当于Java中的package的意思,主要目的是组织代码。thrift使用关键字namespace定义命名空间,例如:
namespace java com.winwill.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;
}
表示name是必填的,age是可选的。
interface
)代码,然后进行开发。官网: http://thrift.apache.org/
github:https://github.com/apache/thr...
将Thrift IDL文件编译成目标代码需要安装Thrift二进制工具。
建议直接使用brew
安装,节省时间:
brew install thrift
安装后查看版本:
$ thrift -version
Thrift version 0.12.0
也可以下载源码安装,参考:http://thrift.apache.org/docs...。
源码地址:http://www.apache.org/dyn/clo...
需下载源码安装,参考:http://thrift.apache.org/docs...。
需下载源码安装,先安装依赖:http://thrift.apache.org/docs...,然后安装thrift:http://thrift.apache.org/docs...。
可以直接下载二进制包thrift-0.12.0.exe;更多下载地址:https://www.apache.org/dyn/closer.cgi
该小节我们通过一个例子,讲述如何使用Thrift快速开发出一个RPC微服务,涉及到Golang服务端、Golang客户端。项目名就叫做T_go_RPC,代码托管在码云:https://gitee.com/chenthe1/T_go_RPC
推荐使用Golang服务端实现微服务,客户端实现调用。后续有时间会增加与Java的交互,敬请期待。
thrift
├── Service.thrift
└── User.thrift
User.thrift
namespace go T_go_RPC.models.rpc
struct User {
1:required i32 id;
2:required string name;
3:required string avatar;
4:required string address;
5:required string mobile;
}
struct UserList {
1:required list userList;
2:required i32 page;
3:required i32 limit;
}
Service.thrift
include "User.thrift"
namespace go T_go_RPC.models.rpc
typedef map Data
struct Response {
1:required i32 errCode; //错误码
2:required string errMsg; //错误信息
3:required Data data;
}
//定义服务
service Greeter {
Response SayHello(
1:required User.User user
)
Response GetUser(
1:required i32 uid
)
}
说明:
1、namespace
用于标记各语言的命名空间或包名。每个语言都需要单独声明。
2、struct
在Java、PHP里相当于class
,golang里还是struct
。
3、service
在Java、PHP里相当于interface
,golang里是interface
。service
里定义的方法必须由服务端实现。
4、typedef
和c语言里的用法一致,用于重新定义类型的名称。
5、struct
里每个都是由1:required i32 errCode;
结构组成,分表代表标识符、是否可选、类型、名称。单个struct
里标识符不能重复,required
表示该属性不能为空,i32
表示int32。
接下来我们生产目标语言的代码:
#编译
$ ./thrift-0.12.0.exe --gen go ./Service.thrift
其它语言请参考上述示例编写。
编译成功后,生成的代码文件有:
gen-go
└── T_go_RPC\models\rpc
├── GoUnusedProtection__.go
├── Service-consts.go
├── Service.go
├── User-consts.go
├── User.go
└── greeter-remote
└── greeter-remote.go
注:根据自己项目的路径,修改(T_go_RPC\models\rpc)对于自己的项目。
本节我们实行golang的服务端,需要实现的接口我们简单实现。本节参考了官方的例子,做了删减,官方的例子代码量有点多,而且是好几个文件,对新手不太友好。建议看完本节再去看官方示例。官方例子:https://github.com/apache/thr...。
然后编写服务端代码: main.go
package main
import (
"github.com/astaxie/beego"
"T_go_RPC/models/rpc"
"context"
"encoding/json"
"flag"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"os"
)
func Usage() {
fmt.Fprint(os.Stderr, "Usage of ", os.Args[0], ":\n")
flag.PrintDefaults()
fmt.Fprint(os.Stderr, "\n")
}
//定义服务
type Greeter struct {
}
//实现IDL里定义的接口
//SayHello
func (this *Greeter) SayHello(ctx context.Context, u *rpc.User) (r *rpc.Response, err error) {
strJson, _ := json.Marshal(u)
return &rpc.Response{ErrCode: 0, ErrMsg: "success", Data: map[string]string{"User:": string(strJson)}}, nil
}
//GetUser
func (this *Greeter) GetUser(ctx context.Context, uid int32) (r *rpc.Response, err error) {
return &rpc.Response{ErrCode: 1, ErrMsg: "this user not exist."}, nil
}
func main() {
//命令行参数
flag.Usage = Usage
protocol := flag.String("P", "binary", "Specify the protocol (binary, compact, json, simplejson)")
framed := flag.Bool("framed", false, "Use framed transport")
buffered := flag.Bool("buffered", false, "Use buffered transport")
addr := flag.String("addr", "localhost:9090", "Address to listen to")
flag.Parse()
//protocol
var protocolFactory thrift.TProtocolFactory
switch *protocol {
case "compact":
protocolFactory = thrift.NewTCompactProtocolFactory()
case "simplejson":
protocolFactory = thrift.NewTSimpleJSONProtocolFactory()
case "json":
protocolFactory = thrift.NewTJSONProtocolFactory()
case "binary", "":
protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()
default:
fmt.Fprint(os.Stderr, "Invalid protocol specified", protocol, "\n")
Usage()
os.Exit(1)
}
//buffered
var transportFactory thrift.TTransportFactory
if *buffered {
transportFactory = thrift.NewTBufferedTransportFactory(8192)
} else {
transportFactory = thrift.NewTTransportFactory()
}
//framed
if *framed {
transportFactory = thrift.NewTFramedTransportFactory(transportFactory)
}
//handler
handler := &Greeter{}
//transport,no secure
var err error
var transport thrift.TServerTransport
transport, err = thrift.NewTServerSocket(*addr)
if err != nil {
fmt.Println("error running server:", err)
}
//processor
processor := rpc.NewGreeterProcessor(handler)
fmt.Println("Starting the simple server... on ", *addr)
//start tcp server
server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
err = server.Serve()
if err != nil {
fmt.Println("error running server:", err)
}
beego.Run()
}
编译并运行,或者直接使用JetBrains客户端工具运行:
$ go run main.go
Starting the simple server... on localhost:9090
我们先使用go test写客户端代码:client_test.go
package main
import (
"context"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"testing"
"T_go_RPC/models/rpc"
)
var ctx = context.Background()
func GetClient() *rpc.GreeterClient {
addr := ":9090"
var transport thrift.TTransport
var err error
transport, err = thrift.NewTSocket(addr)
if err != nil {
fmt.Println("Error opening socket:", err)
}
//protocol
var protocolFactory thrift.TProtocolFactory
protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()
//no buffered
var transportFactory thrift.TTransportFactory
transportFactory = thrift.NewTTransportFactory()
transport, err = transportFactory.GetTransport(transport)
if err != nil {
fmt.Println("error running client:", err)
}
if err := transport.Open(); err != nil {
fmt.Println("error running client:", err)
}
iprot := protocolFactory.GetProtocol(transport)
oprot := protocolFactory.GetProtocol(transport)
client := rpc.NewGreeterClient(thrift.NewTStandardClient(iprot, oprot))
return client
}
//GetUser
func TestGetUser(t *testing.T) {
client := GetClient()
rep, err := client.GetUser(ctx, 100)
if err != nil {
t.Errorf("thrift err: %v\n", err)
} else {
t.Logf("Recevied: %v\n", rep)
}
}
//SayHello
func TestSayHello(t *testing.T) {
client := GetClient()
user := &rpc.User{}
user.ID = 2
user.Name = "thrift"
user.Address = "address"
rep, err := client.SayHello(ctx, user)
if err != nil {
t.Errorf("thrift err: %v\n", err)
} else {
t.Logf("Recevied: %v\n", rep)
}
}
首先确保服务端已运行,然后运行测试用例,或者直接使用JetBrains客户端工具运行:
$ go test -v
client_test.go:53: Recevied: Response({ErrCode:1 ErrMsg:this user not exist. Data:map[]})
client_test.go:70: Recevied: Response({ErrCode:0 ErrMsg:success Data:map[User::{"id":2,"name":"thrift","avatar":"","address":"address","mobile":""}]})
可以看到,客户端成功将请求发到了服务端,服务端成功地将请求结果返回给客户端,整个通信过程完成。