Thrift 是一种被广泛使用的 rpc 框架,可以比较灵活的定义数据结构和函数输入输出参数,并且可以跨语言调用。为了保证服务接口的统一性和可维护性,我们需要在最开始就制定一系列规范并严格遵守,降低后续维护成本。
Thrift 开发流程是:先定义 IDL,使用 thrift 工具生成目标语言接口 (interface
) 代码,然后进行开发。
官网:http://thrift.apache.org/
github:https://github.com/apache/thr…
安装 Thrift
将 Thrift IDL 文件编译成目标代码需要安装 Thrift 二进制工具。
Mac
建议直接使用 brew
安装,节省时间:
brew install thrift
安装后查看版本:
$ thrift -version
Thrift version 0.12.0
也可以下载源码安装,参考:http://thrift.apache.org/docs…。
源码地址:http://www.apache.org/dyn/clo…
CentOS
需下载源码安装,参考:http://thrift.apache.org/docs…。
Debian/Ubuntu
需下载源码安装,先安装依赖:http://thrift.apache.org/docs…,然后安装 thrift:http://thrift.apache.org/docs…。
Windows
可以直接下载二进制包。地址:http://www.apache.org/dyn/clo…。
实战
该小节我们通过一个例子,讲述如何使用 Thrift 快速开发出一个 RPC 微服务,涉及到 Golang 服务端、Golang 客户端、PHP 客户端、PHP 服务端。项目名就叫做thrift-sample
,代码托管在 https://github.com/52fhy/thri…。
推荐使用 Golang 服务端实现微服务,PHP 客户端实现调用。
编写 thrift IDL
thrift
├── Service.thrift
└── User.thrift
User.thrift
namespace go Sample
namespace php Sample
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<User> userList;
2:required i32 page;
3:required i32 limit;
}
Service.thrift
include "User.thrift"
namespace go Sample
namespace php Sample
typedef map<string, string> 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
在 PHP 里相当于 class
,golang 里还是struct
。
3、service
在 PHP 里相当于 interface
,golang 里是interface
。service
里定义的方法必须由服务端实现。
4、typedef
和 c 语言里的用法一致,用于重新定义类型的名称。
5、struct
里每个都是由 1:required i32 errCode;
结构组成,分表代表标识符、是否可选、类型、名称。单个 struct
里标识符不能重复,required
表示该属性不能为空,i32
表示 int32。
接下来我们生产目标语言的代码:
mkdir -p php go
#编译
thrift -r --gen go thrift/Service.thrift
thrift -r --gen php:server thrift/Service.thrift
其它语言请参考上述示例编写。
编译成功后,生成的代码文件有:
gen-go
└── Sample
├── GoUnusedProtection__.go
├── Service-consts.go
├── Service.go
├── User-consts.go
├── User.go
└── greeter-remote
└── greeter-remote.go
gen-php
└── Sample
├── GreeterClient.php
├── GreeterIf.php
├── GreeterProcessor.php
├── Greeter_GetUser_args.php
├── Greeter_GetUser_result.php
├── Greeter_SayHello_args.php
├── Greeter_SayHello_result.php
├── Response.php
├── User.php
└── UserList.php
注:如果 php 编译不加
:server
则不会生成GreeterProcessor.php
文件。如果无需使用 PHP 服务端,则该文件是不需要的。
golang 服务端
本节我们实行 golang 的服务端,需要实现的接口我们简单实现。本节参考了官方的例子,做了删减,官方的例子代码量有点多,而且是好几个文件,对新手不太友好。建议看完本节再去看官方示例。官方例子:https://github.com/apache/thr…。
首先我们初始化 go mod:
$ go mod init sample
然后编写服务端代码:main.go
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"os"
"sample/gen-go/Sample"
)
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 *Sample.User) (r *Sample.Response, err error) {strJson, _ := json.Marshal(u)
return &Sample.Response{ErrCode: 0, ErrMsg: "success", Data: map[string]string{"User": string(strJson)}}, nil
}
//GetUser
func (this *Greeter) GetUser(ctx context.Context, uid int32) (r *Sample.Response, err error) {return &Sample.Response{ErrCode: 1, ErrMsg: "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 := Sample.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)
}
}
编译并运行:
$ 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"
"sample/gen-go/Sample"
"testing"
)
var ctx = context.Background()
func GetClient() *Sample.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 := Sample.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 := &Sample.User{}
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)
}
}
首先确保服务端已运行,然后运行测试用例:
$ go test -v
=== RUN TestGetUser
--- PASS: TestGetUser (0.00s)
client_test.go:53: Recevied: Response({ErrCode:1 ErrMsg:user not exist. Data:map[]})
=== RUN TestSayHello
--- PASS: TestSayHello (0.00s)
client_test.go:69: Recevied: Response({ErrCode:0 ErrMsg:success Data:map[User:{"id":0,"name":"thrift","avatar":"","address":"address","mobile":""}]})
PASS
ok sample 0.017s
接下来我们使用 php 实现客户端:client.php
<?php
error_reporting(E_ALL);
$ROOT_DIR = realpath(dirname(__FILE__) . '/lib-php/');
$GEN_DIR = realpath(dirname(__FILE__)) . '/gen-php/';
require_once $ROOT_DIR . '/Thrift/ClassLoader/ThriftClassLoader.php';
use Thrift\ClassLoader\ThriftClassLoader;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\TBufferedTransport;
use \Thrift\Transport\THttpClient;
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', $ROOT_DIR);
$loader->registerDefinition('Sample', $GEN_DIR);
$loader->register();
try {if (array_search('--http', $argv)) {$socket = new THttpClient('localhost', 8080, '/server.php');
} else {$socket = new TSocket('localhost', 9090);
}
$transport = new TBufferedTransport($socket, 1024, 1024);
$protocol = new TBinaryProtocol($transport);
$client = new \Sample\GreeterClient($protocol);
$transport->open();
try {$user = new \Sample\User();
$user->id = 100;
$user->name = "test";
$user->avatar = "avatar";
$user->address = "address";
$user->mobile = "mobile";
$rep = $client->SayHello($user);
var_dump($rep);
$rep = $client->GetUser(100);
var_dump($rep);
} catch (\tutorial\InvalidOperation $io) {print "InvalidOperation: $io->why\n";}
$transport->close();} catch (TException $tx) {print 'TException:' . $tx->getMessage() . "\n";
}
?>
在运行 PHP 客户端之前,需要引入 thrift 的 php 库文件。我们下载下来的 thrift 源码包里面就有:
~/Downloads/thrift-0.12.0/lib/php/
├── Makefile.am
├── Makefile.in
├── README.apache.md
├── README.md
├── coding_standards.md
├── lib
├── src
├── test
└── thrift_protocol.ini
我们在当前项目里新建 lib-php
目录,并需要把整个 php
下的代码复制到 lib-php
目录:
$ cp -rp ~/Downloads/thrift-0.12.0/lib/php/* ./lib-php/
然后需要修改 /lib-php/
里的 lib
目录名为Thrift
,否则后续会一直提示Class 'Thrift\Transport\TSocket' not found
。
然后还需要修改 /lib-php/Thrift/ClassLoader/ThriftClassLoader.php
,将findFile()
方法的 $className . '.php';
改为$class . '.php';
,大概在 197 行。修改好的参考:https://github.com/52fhy/thri…
然后现在可以运行了:
$ php client.php
object(Sample\Response)#9 (3) {["errCode"]=>
int(0)
["errMsg"]=>
string(7) "success"
["data"]=>
array(1) {["User"]=>
string(80) "{"id":100,"name":"test","avatar":"avatar","address":"address","mobile":"mobile"}"
}
}
object(Sample\Response)#10 (3) {["errCode"]=>
int(1)
["errMsg"]=>
string(15) "user not exist."
["data"]=>
array(0) {}}
php 服务端
thrift 实现的服务端不能自己起 server 服务独立运行,还需要借助 php-fpm
运行。代码思路和 golang 差不多,先实现 interface
里实现的接口,然后使用 thrift 对外暴露服务:
server.php
<?php
/**
* Created by PhpStorm.
* User: yujc@youshu.cc
* Date: 2019-07-07
* Time: 08:18
*/
error_reporting(E_ALL);
$ROOT_DIR = realpath(dirname(__FILE__) . '/lib-php/');
$GEN_DIR = realpath(dirname(__FILE__)) . '/gen-php/';
require_once $ROOT_DIR . '/Thrift/ClassLoader/ThriftClassLoader.php';
use Thrift\ClassLoader\ThriftClassLoader;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\TBufferedTransport;
use \Thrift\Transport\TPhpStream;
$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', $ROOT_DIR);
$loader->registerDefinition('Sample', $GEN_DIR);
$loader->register();
class Handler implements \Sample\GreeterIf {
/**
* @param \Sample\User $user
* @return \Sample\Response
*/
public function SayHello(\Sample\User $user)
{$response = new \Sample\Response();
$response->errCode = 0;
$response->errMsg = "success";
$response->data = ["user" => json_encode($user)
];
return $response;
}
/**
* @param int $uid
* @return \Sample\Response
*/
public function GetUser($uid)
{$response = new \Sample\Response();
$response->errCode = 1;
$response->errMsg = "fail";
return $response;
}
}
header('Content-Type', 'application/x-thrift');
if (php_sapi_name() == 'cli') {echo "\r\n";}
$handler = new Handler();
$processor = new \Sample\GreeterProcessor($handler);
$transport = new TBufferedTransport(new TPhpStream(TPhpStream::MODE_R | TPhpStream::MODE_W));
$protocol = new TBinaryProtocol($transport, true, true);
$transport->open();
$processor->process($protocol, $protocol);
$transport->close();
这里我们直接使用 php -S 0.0.0.0:8080
启动 httpserver,就不使用 php-fpm
演示了:
$ php -S 0.0.0.0:8080
PHP 7.1.23 Development Server started at Sun Jul 7 10:52:06 2019
Listening on http://0.0.0.0:8080
Document root is /work/git/thrift-sample
Press Ctrl-C to quit.
我们使用 php 客户端,注意需要加参数,调用 http
协议连接:
$ php client.php --http
object(Sample\Response)#9 (3) {["errCode"]=>
int(0)
["errMsg"]=>
string(7) "success"
["data"]=>
array(1) {["user"]=>
string(80) "{"id":100,"name":"test","avatar":"avatar","address":"address","mobile":"mobile"}"
}
}
object(Sample\Response)#10 (3) {["errCode"]=>
int(1)
["errMsg"]=>
string(4) "fail"
["data"]=>
NULL
}
thrift IDL 语法参考
1、类型定义
(1) 基本类型
bool:布尔值(true 或 false)byte:8 位有符号整数
i16:16 位有符号整数
i32:32 位有符号整数
i64:64 位有符号整数
double:64 位浮点数
string:使用 UTF- 8 编码编码的文本字符串
注意没有无符号整数类型。这是因为许多编程语言中没有无符号整数类型(比如 java)。
(2) 容器类型
list<t1>:一系列 t1 类型的元素组成的有序列表,元素可以重复
set<t1>:一些 t1 类型的元素组成的无序集合,元素唯一不重复
map<t1,t2>:key/value 对,key 唯一
容器中的元素类型可以是除 service
以外的任何合法的 thrift 类型,包括结构体和异常类型。
(3) Typedef
Thrift 支持 C /C++ 风格的类型定义:
typedef i32 MyInteger
(4) Enum
定义枚举类型:
enum TweetType {
TWEET,
RETWEET = 2,
DM = 0xa,
REPLY
}
注意:编译器默认从 0 开始赋值,枚举值可以赋予某个常量,允许常量是十六进制整数。末尾没有逗号。
不同于 protocol buffer,thrift 不支持枚举类嵌套,枚举常量必须是 32 位正整数。
示例里,对于 PHP 来说,会生成 TweetType
类;对于 golang 来说,会生成 TweetType_
开头的常量。
(5) Const
Thrift 允许用户定义常量,复杂的类型和结构体可以使用 JSON 形式表示:
const i32 INT_CONST = 1234
const map<string,string> MAP_CONST = {"hello": "world", "goodnight": "moon"}
示例里,对于 PHP 来说,会生成 Constant
类;对于 golang 来说,会生成名称一样的常量。
(6) Exception
用于定义异常。示例:
exception BizException {
1:required i32 code
2:required string msg
}
示例里,对于 PHP 来说,会生成 BizException
类,继承自 TException
;对于 golang 来说,会生成BizException
结构体及相关方法。
(7) Struct
结构体 struct
在 PHP 里相当于class
,golang 里还是struct
。示例:
struct User {
1:required i32 id = 0;
2:optional string name;
}
结构体可以包含其他结构体,但不支持继承结构体。
(8) Service
Thrift 编译器会根据选择的目标语言为 server 产生服务接口代码,为 client 产生桩 (stub) 代码。
service
在 PHP 里相当于 interface
,golang 里是interface
。service
里定义的方法必须由服务端实现。
示例:
service Greeter {
Response SayHello(1:required User.User user)
Response GetUser(1:required i32 uid)
}
// 继承
service ChildGreeter extends Greeter{}
注意:
- 参数可以是基本类型或者结构体,参数只能是只读的(const),不可以作为返回值
- 返回值可以是基本类型或者结构体,返回值可以是 void
- 支持继承,一个 service 可使用 extends 关键字继承另一个 service
(9) Union
定义联合体。查看联合体介绍 https://baijiahao.baidu.com/s…。
struct Pixel{
1:required i32 Red;
2:required i32 Green;
3:required i32 Blue;
}
union Pixel_TypeDef {
1:optional Pixel pixel
2:optional i32 value
}
联合体要求字段选项都是 optional
的,因为同一时刻只有一个变量有值。
2、注释
支持 shell 注释风格、C/C++ 语言中的单行或多行注释风格。
# 这是注释
// 这是注释
/*
* 这是注释
*/
3、namespace
定义命名空间或者包名。格式示例:
namespace go Sample
namespace php Sample
需要支持多个语言,则需要定义多行。命名空间或者包名是多层级,使用 .
号隔开。例如 Sample.Model
最终生成的代码里面 PHP 的命名空间是\Sample\Model
,golang 则会生成目录Sample/Model
,包名是Model
。
4、文件包含
thrift 支持引入另一个 thrift 文件:
include "User.thrift"
include "TestDefine.thrift"
注意:
(1) include 引入的文件使用的使用,字段必须带文件名前缀:
1:required User.User user
不能直接写 User user
,这样会提示找不到User
定义。
(2)假设编译的时候 A 里引入了 B,那么编译 A 的时候,B 里面定义的也会被编译。
5、Field
字段定义格式:
FieldID? FieldReq? FieldType Identifier ('= ConstValue)? XsdFieldOptions ListSeparator?
其中:
-
FieldID
必须是IntConstant
类型,即整型常量。 -
FieldReq
(Field Requiredness,字段选项)支持required
、optional
两种。一旦一个参数设置为required
,未来就一定不能删除或者改为optional
,否则就会出现版本不兼容问题,老客户端访问新服务会出现参数错误。不确定的情况可以都使用optional
。 -
FieldType
就是字段类型。 -
Identifier
就是变量标识符,不能为数字开头。 - 字段定义可以设置默认值,支持
Const
等。
示例:
struct User {
1:required i32 id = 0;
2:optional string name;
}
IDE 插件
1、JetBrains PhpStorm 可以在插件里找到 Thrift Support
安装,重启 IDE 后就支持 Thrift
格式语法了。
2、VScode 在扩展里搜索 Thrift
,安装即可。
参考
1、Apache Thrift – Index of tutorial/
http://thrift.apache.org/tuto…
2、Apache Thrift – Interface Description Language (IDL)
http://thrift.apache.org/docs…
3、Thrift 语法参考 – 流水殇 – 博客园
https://www.cnblogs.com/yuana…
4、和 Thrift 的一场美丽邂逅 – cyfonly – 博客园
https://www.cnblogs.com/cyfon…
本文首发于公众号 ” 飞鸿影的博客(fhyblog)”,欢迎关注。博客地址:https://52fhy.cnblogs.com。
(本文完)