43

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

 5 years ago
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.
neoserver,ios ssh client

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里是 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/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里是 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...

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;
}

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

(本文完)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK