天天看點

Thrift 介紹

1、前言

Thrift是一個跨語言的服務部署架構,最初由Facebook于2007年開發,2008年進入Apache開源項目。Thrift通過一個中間語言(IDL, 接口定義語言)來定義RPC的接口和資料類型,然後通過一個編譯器生成不同語言的代碼(目前支援C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),并由生成的代碼負責RPC協定層和傳輸層的實作。

本文組織結構如下:1)引言 2)架構3)支援的資料傳輸格式、資料傳輸方式和服務模型 4)Thrift安裝 5)利用Thift部署服務

關于thrift使用方法的介紹,參考我這篇文章:Thrift使用指南 。

關于thrift client和server的編寫方法,可參考我這篇文章:使用Thrift RPC編寫程式 。

2、架構

Thrift 介紹

Thrift實際上是實作了C/S模式,通過代碼生成工具将接口定義檔案生成伺服器端和用戶端代碼(可以為不同語言),進而實作服務端和用戶端跨語言的支援。使用者在Thirft描述檔案中聲明自己的服務,這些服務經過編譯後會生成相應語言的代碼檔案,然後使用者實作服務(用戶端調用服務,伺服器端提服務)便可以了。其中protocol(協定層, 定義資料傳輸格式,可以為二進制或者XML等)和transport(傳輸層,定義資料傳輸方式,可以為TCP/IP傳輸,記憶體共享或者檔案共享等)被用作運作時庫。上圖的詳細解釋參考引用【1】。

3、 支援的資料傳輸格式、資料傳輸方式和服務模型

(1)支援的傳輸格式

TBinaryProtocol – 二進制格式.

TCompactProtocol – 壓縮格式

TJSONProtocol – JSON格式

TSimpleJSONProtocol –提供JSON隻寫協定, 生成的檔案很容易通過腳本語言解析。

TDebugProtocol – 使用易懂的可讀的文本格式,以便于debug

(2) 支援的資料傳輸方式

TSocket -阻塞式socker

TFramedTransport – 以frame為機關進行傳輸,非阻塞式服務中使用。

TFileTransport – 以檔案形式進行傳輸。

TMemoryTransport – 将記憶體用于I/O. java實作時内部實際使用了簡單的ByteArrayOutputStream。

TZlibTransport – 使用zlib進行壓縮, 與其他傳輸方式聯合使用。目前無java實作。

(3)支援的服務模型

TSimpleServer – 簡單的單線程服務模型,常用于測試

TThreadPoolServer – 多線程服務模型,使用标準的阻塞式IO。

TNonblockingServer – 多線程服務模型,使用非阻塞式IO(需使用TFramedTransport資料傳輸方式)

4、 Thrift安裝

下載下傳:http://incubator.apache.org/thrift/download/

安裝要求:

Unix/linux 系統,windows+cygwin

C++語言:g++、boost

java 語言:JDK、Apache Ant

其他語言:Python、PHP、Perl, etc…

編譯安裝:./configure –》make –》make install

1、 利用Thrift部署服務

主要流程:編寫服務說明,儲存到.thrift檔案–》根據需要, 編譯.thrift檔案,生成相應的語言源代碼–》根據實際需要, 編寫client端和server端代碼。

(1).thrift檔案編寫

一般将服務放到一個.thrift檔案中,服務的編寫文法與C語言文法基本一緻,在.thrift檔案中有主要有以下幾個内容:變量聲明、資料聲明(struct)和服務接口聲明(service, 可以繼承其他接口)。

下面分析Thrift的tutorial中帶的例子tutorial.thrift

包含頭檔案:

59行:include “shared.thrift”

指定目智語言:

65行:namespace cpp tutorial

定義變量:

80行:const i32 INT32CONSTANT = 9853

定義結構體:

103行:struct Work {

1: i32 num1 = 0,

2: i32 num2,

3: Operation op,

4: optional string comment,

}

定義服務:

service Calculator extends shared.SharedService {

void ping(),

i32 add(1:i32 num1, 2:i32 num2),

i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),

oneway void zip()

}

     要生成C++代碼:./thrift --gen cpp tutorial.thrift,結果代碼存放在gen-cpp目錄下      
     要生成java代碼:./thrift --gen java tutorial.thrift,結果代碼存放在gen-java目錄下
      …..       

(2) client端和server端代碼編寫

client端和sever端代碼要調用編譯.thrift生成的中間檔案。

下面分析cpp檔案下面的CppClient.cpp和CppServer.cpp代碼

Thrift 介紹

在client端,使用者自定義CalculatorClient類型的對象(使用者在.thrift檔案中聲明的服務名稱是Calculator, 則生成的中間代碼中的主類為CalculatorClient), 該對象中封裝了各種服務,可以直接調用(如client.ping()), 然後thrift會通過封裝的rpc調用server端同名的函數。

在server端,需要實作在.thrift檔案中聲明的服務中的所有功能,以便處理client發過來的請求。

一、ubuntu下thrift的安裝

1.下載下傳源代碼

http://thrift.apache.org/download/

下載下傳最新版本thrift-0.8.0.tar.gz

2.安裝boost庫

sudo apt-get install libboost-dev libboost-dbg libboost-doc bcp libboost-*

3.安裝其他相關工具包

sudo apt-get install libboost-dev libboost-test-dev libboost-program-options-dev libevent-dev automake libtool flex bison pkg-config g++ libssl-dev ant

如果需要支援java,需要安裝jdk,配置java環境變量。

4.解壓檔案,進入目錄thrift-0.8.0安裝

./configure --with-cpp --with-boost --without-python --without-csharp --with-java --without-erlang --without-perl --with-php --without-php_extension --without-ruby --without-haskell  --without-go

make

sudo make install

要支援java,需要編譯生成jar包,到lib/java目錄下,執行ant指令。将在lib/java/build目錄下生成libthrift-0.8.0.jar和libthrift-0.8.0-javadoc.jar。編譯過程中,可能出錯,需要檢查lib/java/build/tools/maven-ant-tasks-2.1.3.jar是否正确下載下傳。

5.測試

直接輸入thrift指令,看是否有用法提示

二、thrift自帶的測試樣例

進入tutorial檔案夾,shared.thrift和tutorial.thrift是接口定義檔案。

thrift -r --gen java tutorial.thrift

thirft -r --gen cpp tutorial.thrift

執行這兩條指令可以生成gen-java和gen-cpp兩個檔案夾,這些是thrift編譯器自動生成的代碼。

然後到java目錄下,執行ant指令,編譯成功後,在兩個不同的視窗下執行以下指令:

./JavaServer

./JavaClient simple

進入cpp目錄下,執行make指令,如果編譯出錯,第一個錯誤是

/usr/local/include/thrift/protocol/TBinaryProtocol.tcc:147:35: error: there are no arguments to ‘htons’ that depend on a template parameter, so a declaration of ‘htons’ must be available

則修改Makefile,加上編譯選項-DHAVE_NETINET_IN_H

server: CppServer.cpp

        g++ -DHAVE_NETINET_IN_H -o CppServer -I${THRIFT_DIR} -I${BOOST_DIR}  -I../gen-cpp -L${LIB_DIR} -lthrift CppServer.cpp ${GEN_SRC}

client: CppClient.cpp

        g++ -DHAVE_NETINET_IN_H -o CppClient -I${THRIFT_DIR} -I${BOOST_DIR}  -I../gen-cpp -L${LIB_DIR} -lthrift CppClient.cpp ${GEN_SRC}

編譯通過生,将生成CppClient和CppServer兩個可執行程式。同樣,在兩個不同的視窗執行以下指令:

./CppServer

./CppClient

而且java和c++的Client和Server都可以交叉運作。比如運作JavaServer和CppClient也能得到同樣的結果。以此達到了多語言的互相調用。

三、Hello World

仿照tutorial,寫一個簡單的hello world。在tutorial平級目錄下,建立目錄hello,這裡隻是為了測試需要。

服務端用java,用戶端用java和c++。

1. 編寫tt.thrift

namespace java demo

namespace cpp demo

service Hello{

  string helloString(1:string para)

}

2. 編譯生成代碼

thrift -r --gen java tt.thrift

thrift -r --gen cpp tt.thrift

生成gen-java和gen-cpp兩個檔案夾

gen-cpp下有如下檔案

Hello.cpp  Hello.h  Hello_server.skeleton.cpp  tt_constants.cpp  tt_constants.h  tt_types.cpp  tt_types.h

其中Hello.h,Hello.cpp中定義了遠端調用的接口,實作了底層通信。可以在Hello.h中找到用戶端遠端調用需要用到的類HelloClient,調用方法:

void helloString(std::string& _return, const std::string& para);

這個跟thrift檔案中申明的方法有點不一定,傳回參數是通過引用傳回來的。

Hello_server.skeleton.cpp将實作Hello.h的服務端接口,如果要用c++作為服務端,還需要将這個檔案拷出去,重命名,實作類HelloHandler的方法helloString,遠端調用方法的業務邏輯也就寫在helloString中。可能還需要改main函數中的端口資訊。

gen-java/demo下隻有Hello.java一個檔案,它定義了服務端和用戶端的接口,實作了底層的通信。

3. 編寫java服務端和用戶端

仿照tutorial,在hello目錄下建立java目錄,将tutorial/java/下的一些檔案和目錄拷到hello/java下

build.xml JavaClient  JavaServer  src

删除src下所有檔案,在src下編寫代碼。

1) HelloImpl.java 遠端過程調用的業務邏輯

import demo.*;

import org.apache.thrift.TException;

class HelloImpl implements Hello.Iface {

 public HelloImpl() {}

 public String helloString(String para) throws org.apache.thrift.TException {

        //各種業務邏輯

         return "hello " + para;

 }

}

2) Server.java  服務端程式

import demo.*;

import java.io.IOException;

import org.apache.thrift.protocol.TBinaryProtocol;

import org.apache.thrift.protocol.TBinaryProtocol.Factory;

import org.apache.thrift.server.TServer;

import org.apache.thrift.server.TThreadPoolServer.Args;

import org.apache.thrift.server.TThreadPoolServer;

import org.apache.thrift.transport.TServerSocket;

import org.apache.thrift.transport.TTransportException;

public class Server {

private void start() {

try {

    TServerSocket serverTransport = new TServerSocket(7911);

    Hello.Processor processor = new Hello.Processor(new HelloImpl());

    Factory protFactory = new TBinaryProtocol.Factory(true, true);

    Args args = new Args(serverTransport);

    args.processor(processor);

    args.protocolFactory(protFactory);

    TServer server = new TThreadPoolServer(args);

    //TServer server = new TThreadPoolServer(processor, serverTransport, protFactory);

    System.out.println("Starting server on port 7911 ...");

    server.serve();

   } catch (TTransportException e) {

    e.printStackTrace();

   } catch (Exception e) {

    e.printStackTrace();

}

}

public static void main(String args[]) {

    Server srv = new Server();

    srv.start();

}

}

3) Client.java 用戶端程式

import demo.*;

import java.io.IOException;

import org.apache.thrift.*;

import org.apache.thrift.protocol.*;

import org.apache.thrift.transport.*;

public class Client {

      public static void main(String [] args) {

           try {

                    TTransport transport = new TSocket("localhost", 7911);

                    TProtocol protocol = new TBinaryProtocol(transport);

                    Hello.Client client = new Hello.Client(protocol);

                    transport.open();

                    System.out.println("Client calls hello");

                    System.out.println(client.helloString("world"));

                    transport.close();

               } catch (TException x) {

                    x.printStackTrace();

               }

        }

}

4) 修改 build.xml

<project name="hello" default="hello" basedir=".">

  <description>Thrift Hello</description>

  <property name="src" location="src" />

  <property name="gen" location="../gen-java" />

  <property name="build" location="build" />

  <path id="libs.classpath">

    <fileset dir="http://www.cnblogs.com/lib/java/build">

      <include name="*.jar" />

      <exclude name="-test.jar" />

    </fileset>

    <fileset dir="http://www.cnblogs.com/lib/java/build/lib">

      <include name="*.jar" />

    </fileset>

  </path>

  <path id="build.classpath">

    <path refid="libs.classpath" />

    <pathelement path="${gen}" />

  </path>

  <target name="init">

    <tstamp />

    <mkdir dir="${build}"/>

  </target>

  <target name="compile" depends="init">

    <javac srcdir="${gen}" destdir="${build}" classpathref="libs.classpath" target="_blank" rel="external nofollow" />

    <javac srcdir="${src}" destdir="${build}" classpathref="build.classpath" target="_blank" rel="external nofollow" />

  </target>

  <target name="hello" depends="compile">

    <jar jarfile="hello.jar" basedir="${build}"/>

  </target>

  <target name="clean">

    <delete dir="${build}" />

    <delete file="hello.jar" />

  </target>

</project>

5) 編譯

ant

将生成build檔案夾

Client.class  demo  hello  HelloImpl.class  hello.jar  Server.class

6) 修改執行腳本

JavaClient

java -cp http://www.cnblogs.com/lib/java/build/lib/*:http://www.cnblogs.com/lib/java/build/*:hello.jar Client

JavaServer

java -cp http://www.cnblogs.com/lib/java/build/lib/*:http://www.cnblogs.com/lib/java/build/*:hello.jar Server

4. 編寫c++用戶端

同樣仿照tutorial,将tutorial/cpp中的Makefile和CppClient.cpp拷到hello/cpp下。

1) 将CppClient.cpp重命名為Client.cpp,并修改

#include <stdio.h>

#include <unistd.h>

#include <sys/time.h>

#include <protocol/TBinaryProtocol.h>

#include <transport/TSocket.h>

#include <transport/TTransportUtils.h>

#include "../gen-cpp/Hello.h"

#include <string>

using namespace std;

using namespace apache::thrift;

using namespace apache::thrift::protocol;

using namespace apache::thrift::transport;

using namespace demo;

using namespace boost;

int main(int argc, char** argv) {

  shared_ptr<TTransport> socket(new TSocket("localhost", 7911));

  shared_ptr<TTransport> transport(new TBufferedTransport(socket));

  shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));

  HelloClient client(protocol);

  try {

    transport->open();

    string ret;

    client.helloString(ret, "world");

    printf("%s\n", ret.c_str());

    transport->close();

  } catch (TException &tx) {

    printf("ERROR: %s\n", tx.what());

  }

}

2). 修改Makefile

BOOST_DIR = /usr/local/boost/include/boost-1_33_1/

THRIFT_DIR = /usr/local/include/thrift

LIB_DIR = /usr/local/lib

GEN_SRC = ../gen-cpp/tt_types.cpp ../gen-cpp/Hello.cpp

default: client

client: Client.cpp

        g++ -DHAVE_NETINET_IN_H -o client -I${THRIFT_DIR} -I${BOOST_DIR}  -I../gen-cpp -L${LIB_DIR} -lthrift Client.cpp ${GEN_SRC}

clean:

        $(RM) -r client

3). 編譯

make

生成可執行檔案client

5. 運作程式

運作服務端程式,java目錄下:./JavaServer

運作用戶端程式,cpp目錄下:./client

這樣c++程式通過 client.helloString(ret, "client") 可以調用服務端的java接口String helloString(String para)。

進而實作了遠端多語言調用。

一個不會敲代碼的程式員

一 前言

  Thrift是facebook技術核心架構之一,不同開發語言開發的服務可以通過該架構實作通信。Thrift通過接口定義語言 (interface definition language,IDL) 來定義資料類型和服務,Thrift接口定義檔案由Thrift代碼編譯器生成thrift目智語言的代碼(目前支援C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),并由生成的代碼負責RPC協定層和傳輸層的實作。

  簡而言之,開發者隻需準備一份thrift腳本,通過thrift code generator(像gcc那樣輸入一個指令)就能生成所要求的開發語言代碼。不支援windows。

  Thrift側重點是建構跨語言的可伸縮的服務,特點就是支援的語言多,同時提供了完整的RPC service framework,可以很友善的直接建構服務,不需要做太多其他的工作。服務端可以根據需要編譯成simple | thread-pool | threaded | nonblocking等方式;

  本文檔參考:Thrift types, Thrift IDL, Thrift:The Missing Guide.

二 文法參考

 2.1 類型

  Thrift類型系統包括預定義基本類型,使用者自定義結構體,容器類型,異常和服務定義。

 2.1.1 基本類型

bool: 布爾值 (true or false), one byte

byte: 有符号位元組

i16: 16位有符号整型

i32: 32位有符号整型

i64: 64位有符号整型

double: 64位浮點型

string: Encoding agnostic text or binary string

Note that: Thrift不支援無符号整型,因為Thrift目智語言沒有無符号整型,無法轉換。

 2.1.2 容器(Containers)

  Thrift容器與流行程式設計語言的容器類型相對應,采用Java泛型風格。它有3種可用容器類型:

list<t1>: 元素類型為t1的有序表,容許元素重複。(有序表ordered list不知道如何了解?排序的?c++的vector不排序)

set<t1>:元素類型為t1的無序表,不容許元素重複。

map<t1,t2>: 鍵類型為t1,值類型為t2的kv對,鍵不容許重複。

  容器中元素類型可以是除了service外的任何合法Thrift類型(包括結構體和異常)。

 2.1.3 結構體和異常(Structs and Exceptions)

  Thrift結構體在概念上類似于(similar to)C語言結構體類型--将相關屬性封裝在一起的簡便方式。Thrift結構體将會被轉換成面向對象語言的類。

  異常在文法和功能上類似于(equivalent to)結構體,差别是異常使用關鍵字exception而不是struct聲明。但它在語義上不同于結構體:當定義一個RPC服務時,開發者可能需要聲明一個遠端方法抛出一個異常。

  結構體和異常的聲明将在下一節介紹。

 2.1.4 服務(Services)

  服務的定義方法在語義(semantically)上等同于面向對象語言中的接口。Thrift編譯器會産生執行這些接口的client和server stub。具體參見下一節。

 2.2 類型定義(Typedef)

  Thrift支援C/C++類型定義。

   typedef i32 MyInteger // a  typedef T ReT // b

  說明:a.  末尾沒有逗号。b.   struct也可以使用typedef。

 2.3 枚舉(Enums)

  很多語言都有枚舉,意義都一樣。比如,當定義一個消息類型時,它隻能是預定義的值清單中的一個,可以用枚舉實作。

enum TweetType {TWEET, // (1)   RETWEET = 2, // (2)DM = 0xa, // (3)   REPLY} // (4)struct Tweet { 1: required i32 userId; 2: required string userName; 3: required string text; 4: optional Location loc; 5: optional TweetType tweetType = TweetType.TWEET; // (5) 16: optional string language = "english" }

  說明:

  (1).  編譯器預設從0開始指派

  (2).  可以賦予某個常量某個整數

  (3).  允許常量是十六進制整數

  (4).  末尾沒有分号

  (5).  給常量賦預設值時,使用常量的全稱

  注意,不同于protocal buffer,thrift不支援枚舉類嵌套,枚舉常量必須是32位的正整數

 2.4 注釋(Comment)

  Thrift支援shell風格, C多行風格和Java/C++單行風格。

# This is a valid comment.  // C++/Java style single-line comments work just as well.

 2.5 名字空間(Namespace)

  Thrift中的命名空間類似于C++中的namespace和java中的package,它們提供了一種組織(隔離)代碼的簡便方式。名字空間也可以用于解決類型定義中的名字沖突。

  由于每種語言均有自己的命名空間定義方式(如python中有module), thrift允許開發者針對特定語言定義namespace:  

namespace cpp com.example.project // (1)namespace java com.example.project // (2)namespace php com.example.project 

  (1). 轉化成namespace com { namespace example { namespace project {

  (2).  轉換成package com.example.project

 2.6 Includes

  便于管理、重用和提高子產品性/組織性,我們常常分割Thrift定義在不同的檔案中。包含檔案搜尋方式與c++一樣。Thrift允許檔案包含其它thrift檔案,使用者需要使用thrift檔案名作為字首通路被包含的對象,如:

include "tweet.thrift" // (1)...struct TweetSearchResult { 1: tweet.Tweet tweet; // (2)}

  說明:

  (1).  thrift檔案名要用雙引号包含,末尾沒有逗号或者分号

  (2).  注意tweet字首

 2.7 常量(Constant)

  Thrift允許定義跨語言使用的常量,複雜的類型和結構體可使用JSON形式表示。

const i32 INT_CONST = 1234; // (1)

  說明:

  (1) 分号可有可無。支援16進制。

 2.8 結構體定義(Defining Struct)

  struct是Thrift IDL中的基本組成塊,由域組成,每個域有唯一整數辨別符,類型,名字和可選的預設參數組成。如定義一個類似于Twitter服務:

struct Tweet { 1: required i32 userId; // (1) 2: required string userName; // (2) 3: required string text; 4: optional Location loc; // (3) 16: optional string language = "english" // (4)} struct Location { // (5) 1: required double latitude; 2: required double longitude;}

 (1) 每個域有一個唯一的正整數辨別符;

 (2) 每個域可辨別為required或optional;

 (3) 結構體可以包含其它結構體

 (4) 域可有預設值,與required或optional無關。

 (5) Thrift檔案可以定義多個結構體,并在同一檔案中引用,也可加入檔案限定詞在其它Thrift檔案中引用。

  如上所見,消息定義中的每個域都有一個唯一數字标簽,這些數字标簽在傳輸時用來确定域,一旦使用消息類型,标簽不可改變。(随着項目的進展,可以要變更Thrift檔案,最好不要改變原有的數字标簽)

  規範的struct定義中的每個域均會使用required或者optional關鍵字進行辨別。如果required辨別的域沒有指派,Thrift将給予提示;如果optional辨別的域沒有指派,該域将不會被序列化傳輸;如果某個optional辨別域有預設值而使用者沒有重新指派,則該域的值一直為預設值;如果某個optional辨別域有預設值或者使用者已經重新指派,而不設定它的__isset為true,也不會被序列化傳輸。(不被序列化傳輸的後果是什麼?為空為零?還是預設值,下次試試)

  與services不同,結構體不支援繼承。

2.9 服務定義(Defining Services)

  在流行的序列化/反序列化架構(如protocal buffer)中,Thrift是少有的提供多語言間RPC服務的架構。這是Thrift的一大特色。

  Thrift編譯器會根據選擇的目智語言為server産生服務接口代碼,為client産生stubs。

service Twitter { // A method definition looks like C code. It has a return type, arguments, // and optionally a list of exceptions that it may throw. Note that argument // lists and exception list are specified using the exact same syntax as // field lists in structs. void ping(), // (1) bool postTweet(1:Tweet tweet); // (2)TweetSearchResult searchTweets(1:string query); // (3)  // The 'oneway' modifier indicates that the client only makes a request and // does not wait for any response at all. Oneway methods MUST be void.oneway void zip() // (4)}

 (1) 有點亂,接口支援以逗号和分号結束;

 (2) 參數可以是基本類型和結構體;(參數是cosnt的,轉換為c++語言是const&)

 (3) 傳回值同參數一樣;

 (4) 傳回值是void,注意oneway;

Note that:參數清單的定義與結構體一樣。服務支援繼承。