Flag Counter
© 2017 Tao Peng. All rights reserved.

RPC框架Thrift (C++和GO语言例子)


2016年10月24日

之前一段时间, 看了一下RPC相关的内容, 主要看了一下Facebook的开源RPC框架Thrift, 现在终于能抽点时间来记录一下了, Orz…

1).什么是RPC: 远程过程调用
2).什么是thrift: Apache Thrift - 可伸缩的跨语言服务开发框架

1.安装thrift

本机环境: mac osx, 如果是Linux环境, 那么网上的安装方法也是很多很多的.
我使用brew安装thrift的brew install thrift, 安装的是最新的版本0.9.3, 需要注意, thrift依赖boost库和libevent, 如果是手动安装, 那么首先需要安装这两个库, 使用brew, 会自动帮你进行相关的依赖库安装.

<font color=#0099ff>注意: 使用brew安装, 貌似只有C++库, 我擦, 我也不知道是不是我安装姿势不对, 反正搞得很无语, 后面的go相关的库我是手动又装了一次.</font>

安装go语言库: go get git.apache.org/thrift.git/lib/go/thrift/..., 那么相应的库句安装到GOPATH路径下了.

2.开发流程


1). 基本流程

根据需求,编写thrift接口定义文件。
使用thrift binary为不同的语言生成代码。
根据需求,修改生成的代码(主要是Server端),编写实际的业务逻辑。
编译、部署。


2). thrift文件怎么写
thrift文件定义RPC过程中的通信数据结构、通信接口定义等。接口定义语法类似于C语言,包含了struct、enum、map、list等基础数据结构, 同时支持大部分基本数据类型,如32位整型“i32”等。 请参考: Thrift interface description language


3). 具体的thrift文件, 参考自:

namespace cpp TTG
namespace go  TTG

enum ResponseState {
    StateOk = 0,
    StateError = 1,
    StateEmpty = 2
}

struct Request {
    1: i32 studentID = 0
}

struct Response {
    1: i32 studentID = 0,
    2: string name,
    3: list<string> infos,
    4: ResponseState state
}

service TTGService {
    Response getStudentInfo(1: Request request);
}

上面主要包含数据结构和方法定义, 语法和C++还是比较相似的, 注意最后一个service定义的是一些方法, 然后thrift会根据定义的文件, 生成相应的语言的数据, 这里主要生成 C++和go语言的代码.


4). 生成代码

thrift --gen cpp TTG.thrift
thrift --gen go  TTG.thrift

上面的命令生成两个文件夹: gen-cpp 和 gen-go/ttg/, 两个文件夹分别包含的文件是:

1

2

默认在生成CPP中, 有一个server文件: TTGService_server.skeleton.cpp


5). 编译运行


CPP server 和 CPP client

首先我们需要编译server文件, 刚刚我们说了, 默认会生成一个server文件: TTGService_server.skeleton.cpp

#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;

using boost::shared_ptr;

using namespace  ::TTG;

class TTGServiceHandler : virtual public TTGServiceIf {
 public:
  TTGServiceHandler() {
    // Your initialization goes here
  }

  void getStudentInfo(Response& _return, const Request& request) {
    // Your implementation goes here
    printf("getStudentInfo\n");

    // 下面一段逻辑处理代码是我自己添加的
    //
    printf("request.studentID: %d", request.studentID);

    _return.studentID = request.studentID;
    _return.name = "hello";
    _return.infos.push_back("测试1");
    _return.infos.push_back("测试2");
    _return.state = ResponseState::StateOk;
  }

};

int main(int argc, char **argv) {
  // 默认端口是9090
  int port = 9090;
  // 下面定义一些协议
  shared_ptr<TTGServiceHandler> handler(new TTGServiceHandler());
  shared_ptr<TProcessor> processor(new TTGServiceProcessor(handler));
  shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
  shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

  // 创建一个server对象
  TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
  // server开始监听&serve
  server.serve();
  return 0;
}

那么server文件怎么build起来呢? 使用下面的编译命令:

// 注意:
// 1. 如果是brew安装的boost库和thrift, 那么brew会自动将库的头文件放进/usr/local/include中, 如果是手动安装的, 那么需要 "-I 头文件位置"引入
// 2. 注意引入thrift库是必须的, 不然会报错: -lthrift, 这个库在brew安装的时候会生成, 手动make install后也会生成, 当然不放心可以直接-L /usr/local/lib
// 3. 注意需要使用-std=c++11, 之前没有使用c++11, 出现类似的错误是: using namespace  ::bind找不到
g++ -o xxx_server -std=c++11 -I . -I /usr/local/include -L /usr/local/lib -lthrift TTGService_server.skeleton.cpp TTGService.cpp ttg_constants.cpp ttg_types.cpp


3

好的, 下面来写一个client.cpp文件:

#include "TTGService.h"

#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/protocol/TCompactProtocol.h>

using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;

using boost::shared_ptr;

int main(int argc, char **argv) {
        // 定义socket的host和端口, server端口是9090之前已经说过
        boost::shared_ptr<TSocket> socket(new TSocket("localhost", 9090));
        // 下面定义协议, 如果协议和server不一致, 会出现一些错误
        //
        // 错误: Thrift: Mon Oct 24 22:22:52 2016 TConnectedClient processing exception: Bad version identifier
        //
        boost::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
        boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));

        transport->open();
        // ID设置为1
        TTG::Request r;
        r.studentID = 1;
        TTG::Response resp;

        TTG::TTGServiceClient client(protocol);
        client.getStudentInfo(resp, r);

        transport->close();
        printf("ID=%d  name=%s  state=%d\n", resp.studentID, resp.name.c_str(), resp.state);
        return 0;
}

client.cpp文件的build和之前是类似的:

g++ -o xxx_client -std=c++11 -I . -I /usr/local/include -L /usr/local/lib -lthrift client.cpp TTGService.cpp ttg_constants.cpp ttg_types.cpp

OK, 如果正常的话, 那么现在目录下应该是这样的:

4

OK, 那么现在启动server和client,

./xxx_server
./xxx_client
// 在client的终端能看到输出: ID=1  name=hello  state=0

此处的图片就不贴了… OK, 下面看一下go的client怎么连cpp的server!



CPP server 和 GO client

server还是之前的server, 下面主要编写go的client文件, 那么我们进到gen-go文件夹下去, 我们编写一个client文件如下:

package main

import(
        "./ttg"
        "fmt"
        "git.apache.org/thrift.git/lib/go/thrift"
        "os"
        "net"
)

func main (){
        // 下面定义协议
        transportFactory := thrift.NewTTransportFactory()
        protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
        transport ,err := thrift.NewTSocket(net.JoinHostPort("127.0.0.1","9090"))

        if err != nil {
                fmt.Fprintln(os.Stderr, "error resolving address:", err)
                os.Exit(1)
        }

        useTransport := transportFactory.GetTransport(transport)
        // 创建一个client
        client := ttg.NewTTGServiceClientFactory(useTransport, protocolFactory)
        if err := transport.Open(); err != nil {
                fmt.Fprintln(os.Stderr, "Error opening socket to server", " ", err)
                os.Exit(1)
        }
        defer transport.Close()
        // 注意此时的ID999
        r := &ttg.Request{
                StudentID:999,
        }
        // 根据request获取response
        resp, _ := client.GetStudentInfo(r)

        fmt.Println(resp)
}

需要注意几点:

  1. 当前的文件夹ttg没有放在GOPATH下, 而是和client.go文件放在同一个目录下, 所以需要使用import “./ttg”, 如果放在GOPATH下, 直接”ttg”就OK
  2. 如果之前使用了go get git.apache.org/thrift.git/lib/go/thrift/...安装go, 那么GOPATH下应该有”git.apache.org/thrift.git/lib/go/thrift”, 如果没有,请执行上面命令.

同样, 执行go命令: go run client.go, 输出的结果是: “ Response({StudentID:999 Name:hello Infos:[测试1 测试2] State:StateOk}) “

3. 感受

总体来说thrift还是很方便实用的, 但是总觉得太重…而且代码耦合性强, 还关联了boost库等等…
相比来说, 还是更喜欢RESTful的做法, 简单明了~~~

4. 参考

Go Tutorial
RPC框架Thrift例子-PHP调用C++后端程序