從零開始基於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裡是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/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: [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?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,欄位選項)支援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/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.h