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 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 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 membuffer(new TMemoryBuffer());
    boost::shared_ptr 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 membuffer(new TMemoryBuffer(p,size));
    boost::shared_ptr 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 & staffs)
{
  send_PrintAllStaffName(staffs);
  recv_PrintAllStaffName();
}

函数体已经帮我们实现了发送和接受,非常非常方便。
需要注意的是,我们在.thrift声明的接口的返回值在此作为参数进行返回。

客户端代码:

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

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("10.17.128.19",7200);
    boost::shared_ptr myTransport(new TBufferedTransport(mySock));
    boost::shared_ptr 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!"<continue;
        }
        Person person;

        //服务端需要返回全体员工中编号为2的员工;注:person在此为返回信息,不需要赋值
        client.GetStaffInfo(person,2);

        cout<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 & staffs) {
    // Your implementation goes here
    printf("PrintAllStaffName\n");
  }
};

int main(int argc, char **argv) {
  int port = 7200;
  shared_ptr handler(new TCompanyServiceHandler());
  shared_ptr processor(new TCompanyServiceProcessor(handler));
  shared_ptr serverTransport(new TServerSocket(port));
  shared_ptr transportFactory(new TBufferedTransportFactory());
  shared_ptr 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"<return;
    }
    _return = mycompany.staff.at(index);
}

main()函数会一直阻塞在server.serve()上,进行接收和发送。比如我们在客户端调用的接口为GetStaffInfo(),server()函数接收到后进行判断是哪个接口在发送,知道是接口GetStaffInfo()后,就调用服务器上的接口实现GetStaffInfo()进行数据处理,之后再进行发送成果物。这些所有的一切都已经帮我们实现了,我们不用再去操心通讯的过程,只需好好的写服务端的数据处理。

你可能感兴趣的:(通讯协议)