从零开始基于go-thrift创建一个RPC服务
source link: https://www.tuicool.com/articles/yIvMJ3E
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Thrift 是一种被广泛使用的 rpc 框架,可以比较灵活的定义数据结构和函数输入输出参数,并且可以跨语言调用。为了保证服务接口的统一性和可维护性,我们需要在最开始就制定一系列规范并严格遵守,降低后续维护成本。
Thrift开发流程是:先定义IDL,使用thrift工具生成目标语言接口( interface
)代码,然后进行开发。
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 。
(本文完)
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK