C++ RPC 框架:gRPC 与 Thrift 在分布式系统中的应用

各位观众,各位朋友,大家好!今天咱们来聊聊分布式系统里的“通信员”——RPC框架,重点说说两位重量级选手:gRPC和Thrift。这俩哥们儿,一个出身名门(Google出品),一个历史悠久(Facebook贡献),都是解决分布式系统服务间通信问题的利器。

想象一下,你开了个饭馆,后厨(服务A)负责做菜,前台(服务B)负责点单。客人点了菜,前台得告诉后厨做什么,做好后还得通知前台上菜。如果前台和后厨离得近,吆喝一声就行。但如果他们不在一栋楼里,甚至不在一个城市,那吆喝就不好使了,得用“对讲机”或者“电话”。RPC框架,就是分布式系统里的“对讲机”或者“电话”,让服务之间可以像调用本地函数一样调用远程服务。

什么是RPC?

RPC,全称Remote Procedure Call,远程过程调用。简单来说,就是让一个程序(客户端)调用另一个程序(服务端)的函数,就像调用本地函数一样。你不用关心底层网络通信的细节,RPC框架会帮你搞定。

为什么我们需要RPC框架?

  • 解耦: 服务之间通过接口通信,降低耦合度,方便独立开发和部署。
  • 可扩展性: 可以轻松地增加或减少服务节点,提高系统的吞吐量和可用性。
  • 异构性: 支持不同的编程语言和平台,可以构建异构的分布式系统。
  • 简化开发: 隐藏底层通信细节,让开发者专注于业务逻辑。

gRPC:Google的现代战舰

gRPC,是Google开源的一个高性能、通用的RPC框架,基于 Protocol Buffers (protobuf) 作为接口定义语言(IDL)和序列化协议。它采用 HTTP/2 作为底层传输协议,支持多种编程语言。

gRPC的优点:

  • 高性能: HTTP/2 协议带来多路复用、头部压缩等优化,protobuf 序列化效率高。
  • 跨语言: 支持多种编程语言,包括 C++, Java, Python, Go, Ruby, C#, Node.js, Android, Objective-C, PHP。
  • 代码生成: 通过 protobuf 定义服务接口,自动生成客户端和服务端代码,减少手动编写代码的工作量。
  • 流式传输: 支持单向流、双向流等多种流式传输模式,适用于实时性要求高的场景。
  • 身份验证: 内置身份验证机制,保障服务安全。

gRPC的缺点:

  • 学习曲线: 需要学习 protobuf 语法和 gRPC 的使用方式。
  • protobuf 依赖: 必须使用 protobuf 作为 IDL 和序列化协议,虽然protobuf 性能好,但可能不适用于所有场景。
  • HTTP/2 兼容性: 部分旧版本的 HTTP 客户端可能不支持 HTTP/2。

gRPC实战:一个简单的计算器服务

咱们来用 gRPC 实现一个简单的计算器服务,包括加法和乘法两个操作。

  1. 定义 protobuf 文件 (calculator.proto):
syntax = "proto3";

package calculator;

service Calculator {
  rpc Add (AddRequest) returns (AddResponse) {}
  rpc Multiply (MultiplyRequest) returns (MultiplyResponse) {}
}

message AddRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message AddResponse {
  int32 result = 1;
}

message MultiplyRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message MultiplyResponse {
  int32 result = 1;
}

这个文件定义了一个 Calculator 服务,包含 AddMultiply 两个 RPC 方法。每个方法都有对应的请求和响应消息。

  1. 生成 C++ 代码:

你需要安装 protobuf 编译器 (protoc) 和 gRPC C++ 插件。然后使用以下命令生成 C++ 代码:

protoc --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` calculator.proto

这会生成 calculator.pb.h, calculator.pb.cc, calculator.grpc.pb.hcalculator.grpc.pb.cc 四个文件。

  1. 实现服务端 (server.cc):
#include <iostream>
#include <memory>
#include <string>

#include <grpcpp/grpcpp.h>
#include "calculator.grpc.pb.h"

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using calculator::Calculator;
using calculator::AddRequest;
using calculator::AddResponse;
using calculator::MultiplyRequest;
using calculator::MultiplyResponse;

class CalculatorServiceImpl final : public Calculator::Service {
  Status Add(ServerContext* context, const AddRequest* request,
             AddResponse* reply) override {
    int32_t num1 = request->num1();
    int32_t num2 = request->num2();
    reply->set_result(num1 + num2);
    return Status::OK;
  }

  Status Multiply(ServerContext* context, const MultiplyRequest* request,
                MultiplyResponse* reply) override {
    int32_t num1 = request->num1();
    int32_t num2 = request->num2();
    reply->set_result(num1 * num2);
    return Status::OK;
  }
};

void RunServer() {
  std::string server_address("0.0.0.0:50051");
  CalculatorServiceImpl service;

  ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;
  server->Wait();
}

int main() {
  RunServer();
  return 0;
}

这个代码定义了一个 CalculatorServiceImpl 类,实现了 Calculator::Service 接口,提供了 AddMultiply 两个方法的具体实现。 RunServer 函数启动 gRPC 服务器,监听 50051 端口。

  1. 实现客户端 (client.cc):
#include <iostream>
#include <memory>
#include <string>

#include <grpcpp/grpcpp.h>
#include "calculator.grpc.pb.h"

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using calculator::Calculator;
using calculator::AddRequest;
using calculator::AddResponse;
using calculator::MultiplyRequest;
using calculator::MultiplyResponse;

class CalculatorClient {
 public:
  CalculatorClient(std::shared_ptr<Channel> channel)
      : stub_(Calculator::NewStub(channel)) {}

  int32_t Add(int32_t num1, int32_t num2) {
    AddRequest request;
    request.set_num1(num1);
    request.set_num2(num2);
    AddResponse reply;
    ClientContext context;

    Status status = stub_->Add(&context, request, &reply);

    if (status.ok()) {
      return reply.result();
    } else {
      std::cout << status.error_code() << ": " << status.error_message()
                << std::endl;
      return -1;
    }
  }

  int32_t Multiply(int32_t num1, int32_t num2) {
    MultiplyRequest request;
    request.set_num1(num1);
    request.set_num2(num2);
    MultiplyResponse reply;
    ClientContext context;

    Status status = stub_->Multiply(&context, request, &reply);

    if (status.ok()) {
      return reply.result();
    } else {
      std::cout << status.error_code() << ": " << status.error_message()
                << std::endl;
      return -1;
    }
  }

 private:
  std::unique_ptr<Calculator::Stub> stub_;
};

int main(int argc, char** argv) {
  CalculatorClient calculator(grpc::CreateChannel(
      "localhost:50051", grpc::InsecureChannelCredentials()));
  int32_t num1 = 10;
  int32_t num2 = 20;
  int32_t add_result = calculator.Add(num1, num2);
  std::cout << num1 << " + " << num2 << " = " << add_result << std::endl;

  int32_t multiply_result = calculator.Multiply(num1, num2);
  std::cout << num1 << " * " << num2 << " = " << multiply_result << std::endl;

  return 0;
}

这个代码定义了一个 CalculatorClient 类,使用 gRPC 连接到服务器,并调用 AddMultiply 方法。

  1. 编译和运行:

编译服务端和客户端代码,并先运行服务端,再运行客户端。客户端会输出加法和乘法的结果。

Thrift:身经百战的老兵

Thrift,是 Apache 基金会下的一个跨语言的 RPC 框架,最初由 Facebook 开发。它也使用 IDL 定义服务接口,支持多种序列化协议和传输协议。

Thrift的优点:

  • 跨语言: 支持多种编程语言,包括 C++, Java, Python, PHP, Ruby, Erlang, Go, Haskell, Perl, Objective-C, Delphi, C#, Node.js。
  • 多种序列化协议: 支持多种序列化协议,包括 Binary, Compact, JSON 等,可以根据场景选择合适的协议。
  • 多种传输协议: 支持多种传输协议,包括 TCP, HTTP, Memory 等,可以根据场景选择合适的协议。
  • 成熟稳定: 经过多年的发展,Thrift 已经非常成熟稳定,在很多大型系统中得到应用。

Thrift的缺点:

  • 性能: 相比 gRPC,Thrift 的性能可能稍逊一筹,尤其是在 CPU 密集型场景下。
  • 代码生成: 虽然 Thrift 也有代码生成功能,但生成的代码可能不如 gRPC 简洁易用。
  • 社区活跃度: 相比 gRPC,Thrift 的社区活跃度较低。

Thrift实战:同样是计算器服务

咱们再用 Thrift 实现同样的计算器服务。

  1. 定义 Thrift 文件 (calculator.thrift):
namespace cpp calculator

struct AddRequest {
  1: i32 num1;
  2: i32 num2;
}

struct AddResponse {
  1: i32 result;
}

struct MultiplyRequest {
  1: i32 num1;
  2: i32 num2;
}

struct MultiplyResponse {
  1: i32 result;
}

service Calculator {
  AddResponse Add(1: AddRequest request)
  MultiplyResponse Multiply(1: MultiplyRequest request)
}

这个文件定义了和 gRPC 版本类似的 Calculator 服务和相关的数据结构。

  1. 生成 C++ 代码:

你需要安装 Thrift 编译器 (thrift). 然后使用以下命令生成 C++ 代码:

thrift -r --gen cpp calculator.thrift

这会生成 gen-cpp 目录,包含 calculator_types.h, calculator_types.cpp, Calculator.hCalculator.cpp 等文件。

  1. 实现服务端 (server.cc):
#include <iostream>
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

#include "gen-cpp/Calculator.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 ::calculator;

class CalculatorHandler : virtual public CalculatorIf {
 public:
  CalculatorHandler() {
    // Your initialization goes here
  }

  void Add(AddResponse& _return, const AddRequest& request) {
    // Your implementation goes here
    _return.result = request.num1 + request.num2;
    printf("Add(%d, %d)n", request.num1, request.num2);
  }

  void Multiply(MultiplyResponse& _return, const MultiplyRequest& request) {
    // Your implementation goes here
    _return.result = request.num1 * request.num2;
    printf("Multiply(%d, %d)n", request.num1, request.num2);
  }

};

int main(int argc, char **argv) {
  int port = 9090;
  shared_ptr<CalculatorHandler> handler(new CalculatorHandler());
  shared_ptr<TProcessor> processor(new CalculatorProcessor(handler));
  shared_ptr<TServerSocket> serverTransport(new TServerSocket(port));
  shared_ptr<TBufferedTransportFactory> transportFactory(new TBufferedTransportFactory());
  shared_ptr<TBinaryProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

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

这个代码定义了一个 CalculatorHandler 类,实现了 CalculatorIf 接口,提供了 AddMultiply 两个方法的具体实现。 main 函数启动 Thrift 服务器,监听 9090 端口。

  1. 实现客户端 (client.cc):
#include <iostream>
#include <memory>

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

#include "gen-cpp/Calculator.h"

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

using boost::shared_ptr;

using namespace ::calculator;

int main(int argc, char **argv) {
  shared_ptr<TTransport>  socket(new TSocket("localhost", 9090));
  shared_ptr<TTransport> transport(new TBufferedTransport(socket));
  shared_ptr<TProtocol>  protocol(new TBinaryProtocol(transport));
  CalculatorClient client(protocol);
  try {
    transport->open();

    AddRequest add_request;
    add_request.num1 = 10;
    add_request.num2 = 20;
    AddResponse add_response;
    client.Add(add_response, add_request);
    std::cout << add_request.num1 << " + " << add_request.num2 << " = " << add_response.result << std::endl;

    MultiplyRequest multiply_request;
    multiply_request.num1 = 10;
    multiply_request.num2 = 20;
    MultiplyResponse multiply_response;
    client.Multiply(multiply_response, multiply_request);
    std::cout << multiply_request.num1 << " * " << multiply_request.num2 << " = " << multiply_response.result << std::endl;

    transport->close();
  } catch (TException &tx) {
    std::cout << "ERROR: " << tx.what() << std::endl;
  }
  return 0;
}

这个代码创建了一个 Thrift 客户端,连接到服务器,并调用 AddMultiply 方法。

  1. 编译和运行:

编译服务端和客户端代码,并先运行服务端,再运行客户端。客户端会输出加法和乘法的结果。

gRPC vs. Thrift:谁更胜一筹?

特性 gRPC Thrift
IDL Protocol Buffers (protobuf) Thrift IDL
序列化协议 protobuf Binary, Compact, JSON 等
传输协议 HTTP/2 TCP, HTTP, Memory 等
性能 较高,尤其是在 CPU 密集型场景下 相对较低,但在 IO 密集型场景下表现良好
跨语言支持 广泛 广泛
代码生成 简洁易用 相对复杂
流式传输 支持 不直接支持,但可以通过其他方式实现
社区活跃度 较高 较低
适用场景 高性能、实时性要求高的场景,微服务架构 异构系统集成,传统 RPC 应用,对性能要求不高的场景

如何选择?

选择 gRPC 还是 Thrift,取决于你的具体需求:

  • 如果你追求高性能、需要流式传输、构建微服务架构,并且不介意学习 protobuf,那么 gRPC 是一个不错的选择。
  • 如果你的系统需要集成多种编程语言、需要支持多种序列化协议和传输协议、对性能要求不高,并且已经熟悉 Thrift,那么 Thrift 也是一个可行的方案。

总结

gRPC 和 Thrift 都是优秀的 RPC 框架,各有优缺点。选择哪个,需要根据你的实际情况进行权衡。希望今天的讲解能帮助你更好地理解 gRPC 和 Thrift,并在实际项目中做出正确的选择。记住,没有银弹,只有最适合你的工具!

下次有机会,咱们再聊聊其他RPC框架,比如Dubbo,或者更深入地探讨gRPC和Thrift的内部实现。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注