从零开始基于go-thrift创建一个RPC服务
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: [email protected] * 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 。
(本文完)