Thrift 是一種被普遍使用的 rpc 框架,能夠比較靈活的定義數據結構和函數輸入輸出參數,而且能夠跨語言調用。爲了保證服務接口的統一性和可維護性,咱們須要在最開始就制定一系列規範並嚴格遵照,下降後續維護成本。php
Thrift開發流程是:先定義IDL,使用thrift工具生成目標語言接口(interface
)代碼,而後進行開發。html
官網: http://thrift.apache.org/
github:https://github.com/apache/thrift/java
將Thrift IDL文件編譯成目標代碼須要安裝Thrift二進制工具。git
建議直接使用brew
安裝,節省時間:github
brew install thrift
安裝後查看版本:golang
$ thrift -version Thrift version 0.12.0
也能夠下載源碼安裝,參考:http://thrift.apache.org/docs/install/os_x。shell
源碼地址:http://www.apache.org/dyn/closer.cgi?path=/thrift/0.12.0/thrift-0.12.0.tar.gzapache
需下載源碼安裝,參考:http://thrift.apache.org/docs/install/centos。編程
需下載源碼安裝,先安裝依賴:http://thrift.apache.org/docs/install/debian,而後安裝thrift:http://thrift.apache.org/docs/BuildingFromSource。json
能夠直接下載二進制包。地址: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 ) }
說明:
一、namespace
用於標記各語言的命名空間或包名。每一個語言都須要單獨聲明。
二、struct
在PHP裏至關於class
,golang裏仍是struct
。
三、service
在PHP裏至關於interface
,golang裏是interface
。service
裏定義的方法必須由服務端實現。
四、typedef
和c語言裏的用法一致,用於從新定義類型的名稱。
五、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) 基本類型
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{ }
注意:
(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
的,由於同一時刻只有一個變量有值。
二、註釋
支持shell註釋風格、C/C++語言中的單行或多行註釋風格。
# 這是註釋 // 這是註釋 /* * 這是註釋 */
三、namespace
定義命名空間或者包名。格式示例:
namespace go Sample namespace php Sample
須要支持多個語言,則須要定義多行。命名空間或者包名是多層級,使用.
號隔開。例如Sample.Model
最終生成的代碼裏面PHP的命名空間是\Sample\Model
,golang則會生成目錄Sample/Model
,包名是Model
。
四、文件包含
thrift支持引入另外一個thrift文件:
include "User.thrift" include "TestDefine.thrift"
注意:
(1) include 引入的文件使用的使用,字段必須帶文件名前綴:
1:required User.User user
不能直接寫User user
,這樣會提示找不到User
定義。
(2)假設編譯的時候A裏引入了B,那麼編譯A的時候,B裏面定義的也會被編譯。
五、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; }
一、JetBrains PhpStorm 能夠在插件裏找到Thrift Support
安裝,重啓IDE後就支持Thrift
格式語法了。
二、VScode 在擴展裏搜索 Thrift
,安裝便可。
一、Apache Thrift - Index of tutorial/ http://thrift.apache.org/tutorial/ 二、Apache Thrift - Interface Description Language (IDL) http://thrift.apache.org/docs/idl 三、Thrift語法參考 - 流水殤 - 博客園 https://www.cnblogs.com/yuananyun/p/5186430.html 四、和 Thrift 的一場美麗邂逅 - cyfonly - 博客園 https://www.cnblogs.com/cyfonly/p/6059374.html