Thrift多種語言Rpc呼叫實戰
每個公司,隨著業務持續不斷地增長,作為單體專案本身,都會變得越發臃腫,不論是部署,開發,排查問題都變得越來越蛋疼。這個時候,我們會想到的方法就是講一些業務服務逐步先服務化,在之後是微服務架構,甚至到最終是服務網格的模式。
當然本期的主題就是在拆分為多個服務時,我們應該怎麼處理我們的相互呼叫,我們最先考慮的就是http呼叫,但是對於這樣的呼叫存在的問題就是我們在拆分服務的時候,希望不用過多修改程式碼,不用關心呼叫裡需要處理的細節,這就引出了 RPC ,全稱遠端過程呼叫,雖然是兩個服務之間的呼叫,但是在程式碼層面上就是一個普通的函式呼叫,裡面的呼叫細節已經封裝好了,不用你關心,聽著就好爽,至於RPC相關的知識,我推薦的是極客時間的<<趣談網路協議>>相關章節,講的很好也很透徹,這是連結: ofollow,noindex">RPC綜述 。
既然選擇了RPC,我們就會考慮一些比較成熟的框架(當然不是自己搞了。。。。因為實踐就是最好的方案)。網上其實有許多不錯的方案,包括dubbo,dubbox, grpc, thrift等。不過本次的文章主要講的是多種語言之間的呼叫,所以我們的重點在於grpc和Thrift,grpc主要使用的是pb,而Thrift則較為靈活,支援多種壓縮,在傳輸協議上grpc是http2.0,而Thrift則是同時支援http和tcp。當然兩者更詳細的對比大家可以看下這篇文章 grpc 對比 thrift ,在我編寫實踐中,發現thrift編寫簡單直觀,雖然在程式碼生成上略多一些,但我還是選擇了thrift,當然grpc也是一個非常不錯的框架,另外實名反對上面那篇文章講的thrift的文件偏少,其實主要是中文的文件偏少,但是其實因為卻是簡單方便,不需要過多文件。
實戰準備
1.安裝最基本的thrift工具,因為我本身使用的是mac,所以安裝起來還是挺方便的
brew install thrift
其他平臺的安裝方式可以檢視 各平臺安裝thrift
通過在命令列呼叫 thrift -version 確定安裝成功
17:43:46 › thrift -version Thrift version 0.11.0
2.本次實戰講解用的是php和go,php作為我們的client端,go就作為我們的server端
thrift 的構成
Thrift 採用IDL(Interface Definition Language)來定義通用的服務介面,然後通過Thrift提供的編譯器,可以將服務介面編譯成不同語言編寫的程式碼,通過這個方式來實現跨語言的功能。
thrift大致採用的結構就如下圖所示:

thrift.jpg
Transport就是網路傳輸部分,通過抽象化傳輸部分,應用可以根據自己的需要來選擇自己傳輸方式,甚至可以使用自己寫的傳輸方式。
Protocol簡單來說就是我們的編解碼方式,選擇一種特定的方式來轉化成我們的傳輸資料,也可以自定義方式。
Processor顧名思義,從我們的輸入protocol 獲取資料,然後傳遞到我們例項化的handler上進行處理,然後在把處理的結果返回到輸出protocol上。
Server則是我們伺服器執行的方式,因為Thrift採用的是C/S架構,這樣我們的server可以選擇方式來處理遠端客戶端,比如單執行緒,事件驅動。
想要對thrift有更深入的瞭解,或者想對thrift檔案編寫和使用有更深瞭解的同學可以看這遍大牛寫的文章,講地很透徹。 thrift使用指南
程式碼實現
我們的IDL檔案作為例項,比較簡單,client端和server端都只有唯一檔案,但是namespace 應該以client語言為準 (我實踐的結果。。。)
//example.thrift namespace php example struct Data { 1:string text } service format_data { Data do_format(1:Data data) }
go server的實現
1.在根目錄裡建立example.thrift 檔案
2.執行命令
thrift -r --gen go example.thrift
-
根目錄會多出一個gen-go 的目錄,裡面是thrift的例項化程式碼,都是自動生成的。
go server
- 引入thrift-go 的庫
go get git.apache.org/thrift.git/lib/go/thrift
如果因為不可抗拒原因無法下載,可以使用原始碼安裝(當然我還是建議go get獲取,比較方便,可以在網上查下如何go get 能成功的辦法,在這裡就不做講解了)
通過原始碼安裝: *在 $GOPATH 的 src 目錄下建立多層級目錄:git.apache.org/thrift.git/lib/go *從 github 上下載 [thrift 0.10.0](https://github.com/apache/thrift/archive/0.10.0.zip) 的原始碼 ,解壓後進入 thrift-0.10.0/lib/go 目錄下,將 thrift 目錄 copy 到剛建立的 $GOPATH/src/git.apache.org/thrift.git/lib/go 目錄下 *在任意目錄下執行 $ go install git.apache.org/thrift.git/lib/go/thrift 就完成了 golang 的 thrift 庫的安裝
5.接下來,我們編寫函式的例項化程式碼,(為了方便,我把所有程式碼寫在main.go 裡)
type FormatDataImpl struct {} func (fdi *FormatDataImpl) DoFormat(ctx context.Context,data *example.Data) (r *example.Data, err error) { var rData example.Data rData.Text = strings.ToUpper(data.Text) fmt.Println(data, rData) return &rData, nil }
上面的程式碼其實比較簡單,我們定義了一個型別,並且有一個方法 DoFormat ,該方法例項化了我們定義的方法,由於我使用的go版本是1.10,所以方法的引數會增加 ctx context.Context ,如果低版本應該和你定義的是保持一致的。
6.接下來定義我們的server
handler := &FormatDataImpl{} processor := example.NewFormatDataProcessor(handler) serverTransport, err := thrift.NewTServerSocket(HOST + ":" + PORT) if err != nil { log.Fatalln("Error:", err) } transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory()) protocolFactory := thrift.NewTBinaryProtocolFactoryDefault() server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory) fmt.Println("Running at:", HOST + ":" + PORT) server.Serve()
之前我講了thrift的大致結構,所以我們的server也是這樣生成的,我相信你肯定看懂了,嘿嘿。
這是一個採用阻塞式socker,以frame為單位進行傳輸,採用二進位制格式的簡單的單執行緒服務模型。
想對各層次具有哪些格式和型別,可以查閱這篇文章 Thrift 架構總結
7.所以我們完整的main.go 就如下
package main import ( "context" "fmt" "git.apache.org/thrift.git/lib/go/thrift" "log" "strings" "thrift/gen-go/example" ) type FormatDataImpl struct {} func (fdi *FormatDataImpl) DoFormat(ctx context.Context,data *example.Data) (r *example.Data, err error) { var rData example.Data rData.Text = strings.ToUpper(data.Text) fmt.Println(data, rData) return &rData, nil } const ( HOST = "localhost" PORT = "8080" ) func main(){ handler := &FormatDataImpl{} processor := example.NewFormatDataProcessor(handler) serverTransport, err := thrift.NewTServerSocket(HOST + ":" + PORT) if err != nil { log.Fatalln("Error:", err) } transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory()) protocolFactory := thrift.NewTBinaryProtocolFactoryDefault() server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory) fmt.Println("Running at:", HOST + ":" + PORT) server.Serve() }
-
編譯執行,在go1.10可能會出現如下報錯
error
這是因為生成的程式碼有問題,沒給Flush函式傳入context造成的,比較蛋疼,我們可以先手動給它傳入了
oprot.Flush(ctx)
9.接下來我們正常的編譯執行,如下圖

go ok
10.ok 現在我們的server端OK了
php client的實現 (基於php7)
1.準備工作,由於官方的包沒有做成composer包,所以我們測試的話可以按照官方的方法直接copy程式碼放入我們的專案,但我的建議,如果在正式環境中,最好把程式碼打成一個composer包,方便管理使用。
thrift的php官方程式碼倉庫在 github ,我們在根目錄建立lib,然後在lib下建立Thrift這個目錄,然後把連結點開,把lib下面的所有程式碼都拷貝到Thrift這個目錄下面,結果大概如下圖。

tree
2.接下來就是生成我們的php程式碼,依然用的還是那個thrift檔案
比較簡單的生成方式是
thrift -r --gen php thrift/example.thrift
這樣生成的方式會將所有類都糅合進一個檔案,所以我們也可以這樣
thrift -r --gen php:psr4,validate,json thrift/example.thrift
- psr4 程式碼程式碼生成會遵循psr4標準,每個類一個檔案
- validate 會生成校驗程式碼,對引數進行校驗
- json 生成的程式碼會例項化 JsonSerializable 介面
當然這樣生成的程式碼是client端的,程式碼量其實不是那麼多,如果要生成server端程式碼,還需要加上 server 引數
thrift --gen php:server,psr4,validate,json
3.接下來是我們如何引入thrift-php 的類,我們因為是直接拷貝的程式碼,所以需要手動引入,
require_once __DIR__.'/lib/Thrift/ClassLoader/ThriftClassLoader.php'; use Thrift\ClassLoader\ThriftClassLoader; $GEN_DIR = realpath(dirname(__FILE__)).'/gen-php'; $loader = new ThriftClassLoader(); $loader->registerNamespace('Thrift',__DIR__.'/lib'); $loader->registerNamespace('example',$GEN_DIR); $loader->register();
需要注意的一點,如果沒有加上psr4,我們生成的程式碼都在一個類,需要引入我們的 example 的名稱空間需要換成
$loader->registerDefinition('example',$GEN_DIR);
4.接下來是我們的呼叫
$socket = new TSocket('localhost',8080); $transport = new TFramedTransport($socket); $protocol = new TBinaryProtocol($transport); $client = new format_dataClient($protocol); $transport->open(); $data = new Data(); $data->text = 'World!'; $res = $client->do_format($data); var_dump($res); $transport->close();
為什麼這樣寫我相信大家應該都懂了,:smile:
5.我們完整的client.php 程式碼如下
<?php error_reporting(E_ALL); require_once __DIR__.'/lib/Thrift/ClassLoader/ThriftClassLoader.php'; use Thrift\ClassLoader\ThriftClassLoader; $GEN_DIR = realpath(dirname(__FILE__)).'/gen-php'; $loader = new ThriftClassLoader(); $loader->registerNamespace('Thrift',__DIR__.'/lib'); $loader->registerNamespace('example',$GEN_DIR); $loader->register(); use Thrift\Protocol\TBinaryProtocol; use Thrift\Transport\TFramedTransport; use Thrift\Transport\TSocket; use example\Data; use example\format_dataClient; try { $socket = new TSocket('localhost',8080); $transport = new TFramedTransport($socket); $protocol = new TBinaryProtocol($transport); $client = new format_dataClient($protocol); $transport->open(); $data = new Data(); $data->text = 'World!'; $res = $client->do_format($data); var_dump($res); $transport->close(); } catch (\Exception $e) { print 'TException:'.$e->getMessage().PHP_EOL; }
- 接下來執行我們的client,結果就如下
13:02:54 › php Client.php object(example\Data)#9 (1) { ["text"]=> string(6) "WORLD!" }
7.然後看下我們server端的輸出
API server listening at: 127.0.0.1:60656 Running at: localhost:8080 Data({Text:World!}) {WORLD!}
- 這就是我們一次成功的rpc呼叫(當然是基於我們簡單又靈活的thrift了),希望通過這樣一個簡單的過程,大家對rpc有了更深的瞭解,能夠應用到日常生活中去。