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區別:
- thrift不支援無符號整型 , protobuf支援;
- thrift支援容器,protobuf不支援,只能用repeated表示;
- thrift支援const、typedef,protobuf不支援;
- thrift支援服務介面的宣告,並可以繼承,protobuf不支援;
- 書寫格式不一樣:
//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類:負責資料傳輸
可用選項有:- TFileTransport:檔案(日誌)傳輸類,允許client將檔案傳給server,允許server將收到的資料寫到檔案中。
- THttpTransport:採用Http傳輸協議進行資料傳輸
- TSocket:採用TCP Socket進行資料傳輸
- TZlibTransport:壓縮後對資料進行傳輸,或者將收到的資料解壓
下面幾個類主要是對上面幾個類地裝飾(採用了裝飾模式),以提高傳輸效率。
- TBufferedTransport:對某個Transport物件操作的資料進行buffer,即從buffer中讀取資料進行傳輸,或者將資料直接寫入buffer
- TFramedTransport:同TBufferedTransport類似,也會對相關資料進行buffer,同時,它支援定長資料傳送和接收。
- 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()進行資料處理,之後再進行傳送成果物。這些所有的一切都已經幫我們實現了,我們不用再去操心通訊的過程,只需好好的寫服務端的資料處理。