zoukankan      html  css  js  c++  java
  • 从零开始基于go-thrift创建一个RPC服务

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

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

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

    安装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 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里是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服务端

    本节我们实行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], ":
    ")
    	flag.PrintDefaults()
    	fmt.Fprint(os.Stderr, "
    ")
    }
    
    //定义服务
    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, "
    ")
    		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
    ", err)
    	} else {
    		t.Logf("Recevied: %v
    ", 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
    ", err)
    	} else {
    		t.Logf("Recevied: %v
    ", 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 ThriftClassLoaderThriftClassLoader;
    use ThriftProtocolTBinaryProtocol;
    use ThriftTransportTSocket;
    use ThriftTransportTBufferedTransport;
    use ThriftTransportTHttpClient;
    
    $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 SampleGreeterClient($protocol);
    
        $transport->open();
    
        try {
            $user = new SampleUser();
            $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 (	utorialInvalidOperation $io) {
            print "InvalidOperation: $io->why
    ";
        }
    
        $transport->close();
    
    } catch (TException $tx) {
        print 'TException: ' . $tx->getMessage() . "
    ";
    }
    
    ?>
    

    在运行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 'ThriftTransportTSocket' 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(SampleResponse)#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(SampleResponse)#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 ThriftClassLoaderThriftClassLoader;
    use ThriftProtocolTBinaryProtocol;
    use ThriftTransportTSocket;
    use ThriftTransportTBufferedTransport;
    use ThriftTransportTPhpStream;
    
    $loader = new ThriftClassLoader();
    $loader->registerNamespace('Thrift', $ROOT_DIR);
    $loader->registerDefinition('Sample', $GEN_DIR);
    $loader->register();
    
    class Handler implements SampleGreeterIf {
    
        /**
         * @param SampleUser $user
         * @return SampleResponse
         */
        public function SayHello(SampleUser $user)
        {
            $response = new SampleResponse();
            $response->errCode = 0;
            $response->errMsg = "success";
            $response->data = [
                "user" => json_encode($user)
            ];
    
            return $response;
        }
    
        /**
         * @param int $uid
         * @return SampleResponse
         */
        public function GetUser($uid)
        {
            $response = new SampleResponse();
            $response->errCode = 1;
            $response->errMsg = "fail";
            return $response;
        }
    }
    
    
    header('Content-Type', 'application/x-thrift');
    if (php_sapi_name() == 'cli') {
        echo "
    ";
    }
    
    $handler = new Handler();
    $processor = new SampleGreeterProcessor($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(SampleResponse)#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(SampleResponse)#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里是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的命名空间是SampleModel,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;
    }
    

    IDE插件

    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

  • 相关阅读:
    java获得文件的最后修改时间
    【Tomcat】解决Tomcat catalina.out 不断成长导致档案过大的问题
    mysql报错Packet for query is too large (12238 > 1024). You can change this value
    【Tomcat】linux下实时查看tomcat运行日志
    linux下面MySQL变量修改及生效
    【Vim命令大全】史上最全的Vim命令
    (总结)Linux的chattr与lsattr命令详解
    MySql将查询结果插入到另外一张表
    dos中定义变量与获取常见的引用变量以及四则运算、备份文件(set用法)
    批处理BAT替换与截取字符串的用法t1=%a:~3%是什么意思
  • 原文地址:https://www.cnblogs.com/52fhy/p/11146047.html
Copyright © 2011-2022 走看看