1. 程式人生 > >C++ thrift詳細教程 及和Protobuf對比

C++ thrift詳細教程 及和Protobuf對比

最近同時用了thrift和protobuf,進行通訊傳輸。之前已寫過關於protobuf的一篇博文,具體請點選:http://blog.csdn.net/zsk4232000/article/details/50300201 ,現在就總結一下thrift的編寫,並與protobuf進行一些比較。沒有進行深入的研究,只描述一下我用thrift的流程與方法。

thrift本身封裝了一些通訊的庫,支援完整的client/server RPC框架,所以用thrift編寫網路通訊會非常方便,而且支援const、typedef等,所以比較流行用thrift進行網路通訊協議的編寫。
1. .thrift檔案格式
company.thrift 程式碼:

namespace cpp com.company.project
enum HOBBY
{
    RUNNING = 0,
    BADMINTION = 1,
    PINGPONG = 3,
}
struct Person
{
    1:required string name;
    2:required i32  age ;
    3:required string home;
    4:optional HOBBY hobby = HOBBY.RUNNING;
}

enum FIELD
{
    IT = 0,
    MECHINE = 1,
    SECURITY = 2
, } struct Company { 1:required list<Person> staff; 2:required FIELD field; 3:required i32 staff_num; 4:optional i32 rank = 1; } service TCompanyService { Person GetStaffInfo(1:i32 index) void PrintAllStaffName(1:list<Person> staffs) }

注意點:

  • 命令空間的格式:
namespace cpp com
.company.project //a namespace java com.company.project //b

a : 為C++語言格式的命令空間,轉化成namespace com { namespace company{ namespace project {
b : 為java語言格式的命令空間,轉化成package com.example.project

  • 不可以將巢狀寫法,即不能將結構體或者列舉體寫在其他結構體內,如下為錯誤寫法:
struct Person
{
    1:required string name;
    2:required i32  age ;
    3:required string home;

    enum HOBBY
    {
        RUNNING = 0,
        BADMINTION = 1,
        PINGPONG = 3,
    }

    4:optional HOBBY hobby = HOBBY.RUNNING;
}
  • 提供了三種容器:
   List< t1 >:一系列t1型別的元素組成的有序表,元素可以重複
   Set< t1 >:一系列t1型別的元素組成的無序表,元素唯一
   Map < t1 , t2 >:key/value對(key的型別是t1且key唯一,value型別是t2)
  • 可以省略修飾符required 和optional ,但最好還是寫上,如果是optional ,最好有預設值。
  • 如果想引用其他thrift檔案中的型別,可用include進行包含(不是#include),如include “tweet.thrift”
  • 和protobuf區別:
    1. thrift不支援無符號整型 , protobuf支援;
    2. thrift支援容器,protobuf不支援,只能用repeated表示;
    3. thrift支援const、typedef,protobuf不支援;
    4. thrift支援服務介面的宣告,並可以繼承,protobuf不支援;
    5. 書寫格式不一樣:
      //thrift
      1:required string name;
      2:optional HOBBY hobby = HOBBY.RUNNING;

      //protobuf
      required string name = 1;
      optional HOBBY hobby = 2[default = RUNNING];

2 . thrift編譯

thrift --gen cpp company.thrift    //結果程式碼存放在gen-cpp目錄下
thrift --gen java company.thrift   //結果程式碼存放在gen-java目錄下

在C++中,生成7個檔案:

company_constants.h  company_constants.cpp :存放常量
company_types.h      company_types.cpp     :存放定義的型別
TCompanyService.h    TCompanyService.cpp   :客戶端服務介面的宣告及定義
TCompanyService_server.skeleton.cpp        :服務端介面實現,具體的需要自己修改和新增,只給出一個模板。

我們需要關注的是:

TCompanyService.h    TCompanyService.cpp    TCompanyService_server.skeleton.cpp

其中前兩者用於客戶端,後者用於服務端,且名字可以修改,它只是一個模板.

3. 序列化/反序列化

  • Transport類:負責資料傳輸
    可用選項有:

    1. TFileTransport:檔案(日誌)傳輸類,允許client將檔案傳給server,允許server將收到的資料寫到檔案中。
    2. THttpTransport:採用Http傳輸協議進行資料傳輸
    3. TSocket:採用TCP Socket進行資料傳輸
    4. TZlibTransport:壓縮後對資料進行傳輸,或者將收到的資料解壓

    下面幾個類主要是對上面幾個類地裝飾(採用了裝飾模式),以提高傳輸效率。

    1. TBufferedTransport:對某個Transport物件操作的資料進行buffer,即從buffer中讀取資料進行傳輸,或者將資料直接寫入buffer
    2. TFramedTransport:同TBufferedTransport類似,也會對相關資料進行buffer,同時,它支援定長資料傳送和接收。
    3. TMemoryBuffer:從一個緩衝區中讀寫資料
  • Protocol類:負責資料編碼
    可用選項有:
    TBinaryProtocol:二進位制編碼
    TJSONProtocol:JSON編碼
    TCompactProtocol:密集二進位制編碼
    TDebugProtocol:以使用者易讀的方式組織資料
  • Server類
    可用選項有:
    TSimpleServer:簡單的單執行緒伺服器,主要用於測試
    TThreadPoolServer:使用標準阻塞式IO的多執行緒伺服器
    TNonblockingServer:使用非阻塞式IO的多執行緒伺服器,TFramedTransport必須使用該型別的server

我沒有具體研究過,用的都是最常用的。
以下程式碼為其中的一種:

//序列化
template<typename T>
void object2String(const T &object, string &buf)
{
    boost::shared_ptr<TMemoryBuffer> membuffer(new TMemoryBuffer());
    boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(membuffer));
    object.write(protocol.get());
    buf.clear();
    buf = membuffer->getBufferAsString();
}

//反序列化
template<typename T>
void string2object(const string &buf , T &object)
{
    uint8_t *p = (uint8_t *)(buf.data());
    uint32_t size = buf.size();
    boost::shared_ptr<TMemoryBuffer> membuffer(new TMemoryBuffer(p,size));
    boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(membuffer));
    object.read(protocol.get());
}

4. 客戶端編寫
1. 需要包含標頭檔案TCompanyService.h,它本身已經包含了標頭檔案company_types.h,所以我們可以用在.thrift定義的類。
2. 在TCompanyService.h 和 TCompanyService.cpp中,已經幫我們生成了一個應用於客戶端的類:TCompanyServiceClient,由它實現我們宣告的介面。
3. 開啟TCompanyService.cpp中,可以看到介面實現的函式體:

void TCompanyServiceClient::GetStaffInfo(Person& _return, const int32_t index)
{
  send_GetStaffInfo(index);
  recv_GetStaffInfo(_return);
}

void TCompanyServiceClient::PrintAllStaffName(const std::vector<Person> & staffs)
{
  send_PrintAllStaffName(staffs);
  recv_PrintAllStaffName();
}

函式體已經幫我們實現了傳送和接受,非常非常方便。
需要注意的是,我們在.thrift宣告的介面的返回值在此作為引數進行返回。

客戶端程式碼:

#include "gen-cpp/TCompanyService.h"
#include <boost/make_shared.hpp>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/transport/TSocket.h>
#include <thrift/protocol/TBinaryProtocol.h>

using namespace std;
using namespace boost;
using namespace com::company::project;   //對應於.thrift中的namespace com.company.project
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;

void CSocketClient::Start()
{
    //進行網路通訊初始化
    auto mySock = boost::make_shared<TSocket>("10.17.128.19",7200);
    boost::shared_ptr<TTransport> myTransport(new TBufferedTransport(mySock));
    boost::shared_ptr<TProtocol> myProtoc(new TBinaryProtocol(myTransport));
    TCompanyServiceClient client(myProtoc);   //建立客戶端例項
    myTransport->open();

    while(true)
    {
        char *p = (char*)malloc(100);
        if(!cin.getline(p,100))
        {
            cout<<"please enter some words!"<<endl;
            continue;
        }
        Person person;

        //服務端需要返回全體員工中編號為2的員工;注:person在此為返回資訊,不需要賦值
        client.GetStaffInfo(person,2);

        cout<<person.name<<endl;

    }
    myTransport->close();
}

5. 服務端編寫
其實編譯器已經生成了一個服務端的總體網路通訊框架,即檔案TCompanyService_server.skeleton.cpp,我們可以把它拷貝到服務端專案中,改成你想要的名字。因為此檔案已經實現了main函式,所以需要把原來的main函式刪了。
TCompanyService_server.skeleton.cpp 程式碼:

class TCompanyServiceHandler : virtual public TCompanyServiceIf {
 public:
  TCompanyServiceHandler() {
    // Your initialization goes here
  }

  void GetStaffInfo(Person& _return, const int32_t index) {
    // Your implementation goes here
    printf("GetStaffInfo\n");
  }

  void PrintAllStaffName(const std::vector<Person> & staffs) {
    // Your implementation goes here
    printf("PrintAllStaffName\n");
  }
};

int main(int argc, char **argv) {
  int port = 7200;
  shared_ptr<TCompanyServiceHandler> handler(new TCompanyServiceHandler());
  shared_ptr<TProcessor> processor(new TCompanyServiceProcessor(handler));
  shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
  shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

  TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
  server.serve();
  return 0;
}

我們只需要做的就是實現服務端介面GetStaffInfo()和PrintAllStaffName(),當然main()函式給出的實現只是其中一種,我們可以根據需要修改,如將簡單的單執行緒伺服器TSimpleServer換為標準阻塞式IO的多執行緒伺服器TThreadPoolServer。在此我們假設已知一Company型別例項mycompany,且有1000個員工staff,則我們可以實現介面

void GetStaffInfo(Person& _return, const int32_t index) {
    if(index > 1000)
    {
        cout<<"can not find the staff"<<endl;
        return;
    }
    _return = mycompany.staff.at(index);
}

main()函式會一直阻塞在server.serve()上,進行接收和傳送。比如我們在客戶端呼叫的介面為GetStaffInfo(),server()函式接收到後進行判斷是哪個介面在傳送,知道是介面GetStaffInfo()後,就呼叫伺服器上的介面實現GetStaffInfo()進行資料處理,之後再進行傳送成果物。這些所有的一切都已經幫我們實現了,我們不用再去操心通訊的過程,只需好好的寫服務端的資料處理。