從零開始基於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], ":\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) {
  }
}

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM