从零开始基于go-thrift创建一个RPC服务

匿名 (未验证) 提交于 2019-12-02 23:48:02

Thrift 是一种被广泛使用的 rpc 框架,可以比较灵活的定义数据结构和函数输入输出参数,并且可以跨语言调用。为了保证服务接口的统一性和可维护性,我们需要在最开始就制定一系列规范并严格遵守,降低后续维护成本。

Thrift开发流程是:先定义IDL,使用thrift工具生成目标语言接口(interface)代码,然后进行开发。

官网: http://thrift.apache.org/
github:https://github.com/apache/thrift/

将Thrift IDL文件编译成目标代码需要安装Thrift二进制工具。

Mac

建议直接使用brew安装,节省时间:

brew install thrift

安装后查看版本:

$ thrift -version  Thrift version 0.12.0

也可以下载源码安装,参考:http://thrift.apache.org/docs/install/os_x。

源码地址:http://www.apache.org/dyn/closer.cgi?path=/thrift/0.12.0/thrift-0.12.0.tar.gz

CentOS

需下载源码安装,参考:http://thrift.apache.org/docs/install/centos。

Debian/Ubuntu

需下载源码安装,先安装依赖:http://thrift.apache.org/docs/install/debian,然后安装thrift:http://thrift.apache.org/docs/BuildingFromSource。

Windows

可以直接下载二进制包。地址:http://www.apache.org/dyn/closer.cgi?path=/thrift/0.12.0/thrift-0.12.0.exe。

ʵս

该小节我们通过一个例子,讲述如何使用Thrift快速开发出一个RPC微服务,涉及到Golang服务端、Golang客户端、PHP客户端、PHP服务端。项目名就叫做thrift-sample,代码托管在 https://github.com/52fhy/thrift-sample。

推荐使用Golang服务端实现微服务,PHP客户端实现调用。

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里是interfaceservice里定义的方法必须由服务端实现。
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的服务端,需要实现的接口我们简单实现。本节参考了官方的例子,做了删减,官方的例子代码量有点多,而且是好几个文件,对新手不太友好。建议看完本节再去看官方示例。官方例子:https://github.com/apache/thrift/tree/master/tutorial/go/src。

首先我们初始化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/thrift-sample/blob/master/lib-php/Thrift/ClassLoader/ThriftClassLoader.php

然后现在可以运行了:

$ 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) {   } }

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 }

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里是interfaceservice里定义的方法必须由服务端实现。

示例:

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?id=1623457037181175751&wfr=spider&for=pc。

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,字段选项)支持requiredoptional两种。一旦一个参数设置为 required,未来就一定不能删除或者改为 optional,否则就会出现版本不兼容问题,老客户端访问新服务会出现参数错误。不确定的情况可以都使用 optional
  • FieldType 就是字段类型。
  • Identifier 就是变量标识符,不能为数字开头。
  • 字段定义可以设置默认值,支持Const等。

示例:

struct User {     1:required i32 id = 0;     2:optional string name; }

1、JetBrains PhpStorm 可以在插件里找到Thrift Support安装,重启IDE后就支持Thrift格式语法了。

2、VScode 在扩展里搜索 Thrift,安装即可。

1、Apache Thrift - Index of tutorial/
http://thrift.apache.org/tutorial/
2、Apache Thrift - Interface Description Language (IDL)
http://thrift.apache.org/docs/idl
3、Thrift语法参考 - 流水殇 - 博客园
https://www.cnblogs.com/yuananyun/p/5186430.html
4、和 Thrift 的一场美丽邂逅 - cyfonly - 博客园
https://www.cnblogs.com/cyfonly/p/6059374.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!