Thrift RPC 使用指南實戰(附golang&PHP代碼)

Thrift RPC 框架指南

認識Thrift框架

thrift是一個軟件框架,用來進行可擴展且跨語言的服務的開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務。

  • thrift最初由facebook開發,07年四月開放源碼,08年5月進入apache孵化器。
  • thrift允許定義一個簡單的定義文件中的數據類型和服務接口,以作爲輸入文件,編譯器生成代碼用來方便地生成RPC客戶端和服務器通信的無縫跨編程語言。
  • 類似Thrift的工具,還有Avro、protocol buffer,但相對於Thrift來講,都沒有Thrift支持全面和使用廣泛。

Thrift自下到上可以分爲4層

Server(single-threaded, event-driven etc)
服務器進程調度
Processor(compiler generated)
RPC接口處理函數分發,IDL定義接口的實現將掛接到這裏面
Protocol (JSON, compact etc)
協議
Transport(raw TCP, HTTP etc)
網絡傳輸

Thrift實際上是實現了C/S模式,通過代碼生成工具將接口定義文件生成服務器端和客戶端代碼(可以爲不同語言),從而實現服務端和客戶端跨語言的支持。用戶在Thirft描述文件中聲明自己的服務,這些服務經過編譯後會生成相應語言的代碼文件,然後用戶實現服務(客戶端調用服務,服務器端提服務)便可以了。其中protocol(協議層, 定義數據傳輸格式,可以爲二進制或者XML等)和transport(傳輸層,定義數據傳輸方式,可以爲TCP/IP傳輸,內存共享或者文件共享等)被用作運行時庫。

Thrift支持的傳輸及服務模型

支持的傳輸格式:

參數 描述
TBinaryProtocol 二進制格式
TCompactProtocol 壓縮格式
TJSONProtocol JSON格式
TSimpleJSONProtocol 提供JSON只寫協議, 生成的文件很容易通過腳本語言解析。
TDebugProtocol 使用易懂的可讀的文本格式,以便於debug

支持的數據傳輸方式:

參數 描述
TSocket 阻塞式socker
TFramedTransport 以frame爲單位進行傳輸,非阻塞式服務中使用。
TFileTransport 以文件形式進行傳輸。
TMemoryTransport 將內存用於I/O. java實現時內部實際使用了簡單的ByteArrayOutputStream。
TZlibTransport 使用zlib進行壓縮, 與其他傳輸方式聯合使用。當前無java實現。

支持的服務模型:

參數 描述
TSimpleServer 簡單的單線程服務模型,常用於測試
TThreadPoolServer 多線程服務模型,使用標準的阻塞式IO。
TNonblockingServer 多線程服務模型,使用非阻塞式IO(需使用TFramedTransport數據傳輸方式)

Thrift 下載及安裝

如何獲取Thrift

  1. 官網:http://thrift.apache.org/
  2. golang的Thrift包:
go get git.apache.org/thrift.git/lib/go/thrift

如何安裝Thrift

mac下安裝Thrift,參考上一篇介紹
其他平臺安裝自行挖掘,呵呵。
安裝後通過

liuxinmingMacBook-Rro#:thrift -version
Thrift version 0.9.2 #看到這一行表示安裝成功

Golang、PHP通過Thrift調用

先發個官方各種語言DEMO地址 https://git1-us-west.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial;h=d69498f9f249afaefd9e6257b338515c0ea06390;hb=HEAD

Thrift的協議庫IDL文件

語法參考

參考資料

http://www.cnblogs.com/tianhuilove/archive/2011/09/05/2167669.html

http://my.oschina.net/helight/blog/195015

基本類型


  • bool: 布爾值 (true or false), one byte
  • byte: 有符號字節
  • i16: 16位有符號整型
  • i32: 32位有符號整型
  • i64: 64位有符號整型
  • double: 64位浮點型
  • string: Encoding agnostic text or binary string

基本類型中基本都是有符號數,因爲有些語言沒有無符號數,所以Thrift不支持無符號整型。

特殊類型


  • binary: Blob (byte array) a sequence of unencoded bytes

這是string類型的一種變形,主要是爲java使用

struct結構體

thrift中struct是定義爲一種對象,和面嚮對象語言的class差不多.,但是struct有以下一些約束:
struct不能繼承,但是可以嵌套,不能嵌套自己。
1. 其成員都是有明確類型
2. 成員是被正整數編號過的,其中的編號使不能重複的,這個是爲了在傳輸過程中編碼使用。
3. 成員分割符可以是逗號(,)或是分號(;),而且可以混用,但是爲了清晰期間,建議在定義中只使用一種,比如C++學習者可以就使用分號(;)。
4. 字段會有optional和required之分和protobuf一樣,但是如果不指定則爲無類型–可以不填充該值,但是在序列化傳輸的時候也會序列化進去,
optional是不填充則部序列化。
required是必須填充也必須序列化。
5. 每個字段可以設置默認值
6. 同一文件可以定義多個struct,也可以定義在不同的文件,進行include引入。

struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}

容器(Containers)

Thrift3種可用容器類型:


  • list(t): 元素類型爲t的有序表,容許元素重複。
  • set(t):元素類型爲t的無序表,不容許元素重複。對應c++中的set,java中的HashSet,python中的set,php中沒有set,則轉換爲list類型。
  • map(t,t): 鍵類型爲t,值類型爲t的kv對,鍵不容許重複。對用c++中的map, Java的HashMap, PHP 對應 array, Python/Ruby 的dictionary。

容器中元素類型可以是除了service外的任何合法Thrift類型(包括結構體和異常)。爲了最大的兼容性,map的key最好是thrift的基本類型,有些語言不支持複雜類型的key,JSON協議只支持那些基本類型的key。
容器都是同構容器,不失異構容器。

實現Thrift TDL文件

batu.thrift文件:

/**
 * BatuThrift TDL
 * @author liuxinming
 * @time 2015.5.13
 */

namespace go batu.demo
namespace php batu.demo

/**
 * 結構體定義
 */
struct Article{
 1: i32 id, 
 2: string title,
 3: string content,
 4: string author,
}

const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}

service batuThrift {        
        list<string> CallBack(1:i64 callTime, 2:string name, 3:map<string, string> paramMap),
        void put(1: Article newArticle),
}

編譯IDL文件,生成相關代碼

thrift -r --gen go batu.thrift
thrift -r --gen php batu.thrift  
thrift -r --gen php:server batu.thrift #生成PHP服務端接口代碼有所不一樣

Golang Service 實現

  1. 先按照golang的Thrift包

    go get git.apache.org/thrift.git/lib/go/thrift

  2. 將Thrift生成的開發庫複製到GOPATH中

    cp -r /Users/liuxinming/wwwroot/testphp/gen-go/batu $GOPATH/src

  3. 開發Go server端代碼(後面的代碼,目錄我們放在$GOPATH/src/thrift 中運行和演示)
    test.go文件:

package main

import (
    "batu/demo" #注意導入Thrift生成的接口包
    "fmt"
    "git.apache.org/thrift.git/lib/go/thrift"
    "os"
    "time"
)

const (
    NetworkAddr = "127.0.0.1:9090" #監聽地址&端口
)

type batuThrift struct {
}

func (this *batuThrift) CallBack(callTime int64, name string, paramMap map[string]string) (r []string, err error) {
    fmt.Println("-->from client Call:", time.Unix(callTime, 0).Format("2006-01-02 15:04:05"), name, paramMap)
    r = append(r, "key:"+paramMap["a"]+"    value:"+paramMap["b"])
    return
}

func (this *batuThrift) Put(s *demo.Article) (err error) {
    fmt.Printf("Article--->id: %d\tTitle:%s\tContent:%t\tAuthor:%d\n", s.Id, s.Title, s.Content, s.Author)
    return nil
}

func main() {
    transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
    protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
    //protocolFactory := thrift.NewTCompactProtocolFactory()

    serverTransport, err := thrift.NewTServerSocket(NetworkAddr)
    if err != nil {
        fmt.Println("Error!", err)
        os.Exit(1)
    }

    handler := &batuThrift{}
    processor := demo.NewBatuThriftProcessor(handler)

    server := thrift.NewTSimpleServer4(processor, serverTransport, transportFactory, protocolFactory)
    fmt.Println("thrift server in", NetworkAddr)
    server.Serve()
}

4.運行go服務端(監聽9090端口)

liuxinmingdeMacBook-Pro:thrift liuxinming$ go run test.go
thrift server in 127.0.0.1:9090

至此Go的Thrift服務端OK.

Golang Client 實現

goClient.go文件:

package main

import (
    "batu/demo"
    "fmt"
    "git.apache.org/thrift.git/lib/go/thrift"
    "net"
    "os"
    "strconv"
    "time"
)

const (
    HOST = "127.0.0.1"
    PORT = "9090"
)

func main() {
    startTime := currentTimeMillis()

    transportFactory := thrift.NewTFramedTransportFactory(thrift.NewTTransportFactory())
    protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()

    transport, err := thrift.NewTSocket(net.JoinHostPort(HOST, PORT))
    if err != nil {
        fmt.Fprintln(os.Stderr, "error resolving address:", err)
        os.Exit(1)
    }

    useTransport := transportFactory.GetTransport(transport)
    client := demo.NewBatuThriftClientFactory(useTransport, protocolFactory)
    if err := transport.Open(); err != nil {
        fmt.Fprintln(os.Stderr, "Error opening socket to "+HOST+":"+PORT, " ", err)
        os.Exit(1)
    }
    defer transport.Close()

    for i := 0; i < 10; i++ {
        paramMap := make(map[string]string)
        paramMap["a"] = "batu.demo"
        paramMap["b"] = "test" + strconv.Itoa(i+1)
        r1, _ := client.CallBack(time.Now().Unix(), "go client", paramMap)
        fmt.Println("GOClient Call->", r1)
    }

    model := demo.Article{1, "Go第一篇文章", "我在這裏", "liuxinming"}
    client.Put(&model)
    endTime := currentTimeMillis()
    fmt.Printf("本次調用用時:%d-%d=%d毫秒\n", endTime, startTime, (endTime - startTime))

}

func currentTimeMillis() int64 {
    return time.Now().UnixNano() / 1000000
}

goClient運行後結果:

liuxinmingdeMacBook-Pro:thrift liuxinming$ go run goClient.go
GOClient Call-> [key:batu.demo value:test1]
GOClient Call-> [key:batu.demo value:test2]
GOClient Call-> [key:batu.demo value:test3]
GOClient Call-> [key:batu.demo value:test4]
GOClient Call-> [key:batu.demo value:test5]
GOClient Call-> [key:batu.demo value:test6]
GOClient Call-> [key:batu.demo value:test7]
GOClient Call-> [key:batu.demo value:test8]
GOClient Call-> [key:batu.demo value:test9]
GOClient Call-> [key:batu.demo value:test10]
本次調用用時:1431583140857-1431583140855=2毫秒

PHP Client 實現

  1. 首先去下載Thrift,git庫地址爲:https://github.com/apache/thrift
  2. 新建項目目錄testphp,然後把thrift/lib/php/lib複製到testphp目錄下面
  3. 複製生成的gen-php到testphp目錄下面
  4. 客戶端代碼
<?php
/**
 * Thrift RPC - PHPClient
 * @author liuxinming
 * @time 2015.5.13
 */
namespace  batu\testDemo;
header("Content-type: text/html; charset=utf-8");
$startTime = getMillisecond();//記錄開始時間

$ROOT_DIR = realpath(dirname(__FILE__).'/');
$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\TSocketPool;
use Thrift\Transport\TFramedTransport;
use Thrift\Transport\TBufferedTransport;

$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift',$ROOT_DIR);
$loader->registerDefinition('batu\demo', $GEN_DIR);
$loader->register();

$thriftHost = '127.0.0.1'; //UserServer接口服務器IP
$thriftPort = 9090;            //UserServer端口

$socket = new TSocket($thriftHost,$thriftPort);  
$socket->setSendTimeout(10000);#Sets the send timeout.
$socket->setRecvTimeout(20000);#Sets the receive timeout.
//$transport = new TBufferedTransport($socket); #傳輸方式:這個要和服務器使用的一致 [go提供後端服務,迭代10000次2.6 ~ 3s完成]
$transport = new TFramedTransport($socket); #傳輸方式:這個要和服務器使用的一致[go提供後端服務,迭代10000次1.9 ~ 2.1s完成,比TBuffer快了點]
$protocol = new TBinaryProtocol($transport);  #傳輸格式:二進制格式
$client = new \batu\demo\batuThriftClient($protocol);# 構造客戶端

$transport->open();  
$socket->setDebug(TRUE);

for($i=1;$i<11;$i++){
    $item = array();
    $item["a"] = "batu.demo";
    $item["b"] = "test"+$i;
    $result = $client->CallBack(time(),"php client",$item); # 對服務器發起rpc調用
    echo "PHPClient Call->".implode('',$result)."<br>";
}

$s = new \batu\demo\Article();
$s->id = 1;
$s->title = '插入一篇測試文章';
$s->content = '我就是這篇文章內容';
$s->author = 'liuxinming';
$client->put($s);

$s->id = 2;
$s->title = '插入二篇測試文章';
$s->content = '我就是這篇文章內容';
$s->author = 'liuxinming';
$client->put($s);

$endTime = getMillisecond();

echo "本次調用用時: :".$endTime."-".$startTime."=".($endTime-$startTime)."毫秒<br>";

function getMillisecond() {
    list($t1, $t2) = explode(' ', microtime());
    return (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000);
}

$transport->close();

PHP運行後結果:

PHPClient Call->key:batu.demo value:1
PHPClient Call->key:batu.demo value:2
PHPClient Call->key:batu.demo value:3
PHPClient Call->key:batu.demo value:4
PHPClient Call->key:batu.demo value:5
PHPClient Call->key:batu.demo value:6
PHPClient Call->key:batu.demo value:7
PHPClient Call->key:batu.demo value:8
PHPClient Call->key:batu.demo value:9
PHPClient Call->key:batu.demo value:10
本次調用用時: :1431582183296-1431582183290=6毫秒

Go服務端看到打印數據:

–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:1]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:2]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:3]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:4]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:5]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:6]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:7]
–>from client Call: 2015-05-13 22:43:03 php client map[b:8 a:batu.demo]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:9]
–>from client Call: 2015-05-13 22:43:03 php client map[a:batu.demo b:10]
Article—>id: 1 Title:插入一篇測試文章 Content:我就是這篇文章內容 Author:liuxinming
Article—>id: 2 Title:插入二篇測試文章 Content:我就是這篇文章內容 Author:liuxinming

完結,至此一個Golang的Thrift服務端 和 PHP的Thrift客戶端完成!

發佈了111 篇原創文章 · 獲贊 14 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章