4.4-中间件之gRPC

一、初识gRPC

gRPC是一个通用RPC框架,通过.proto文件里面规定好的消息格式与函数格式,用protoc工具自动生成代码,得到

  • .pb.h(包含消息类型(如请求、响应消息), 服务端和客户端带上#include "IM.Login.pb.h" 才能使用这些消息类型和接口)
  • .pb.cc(.pb.h中声明的具体实现,客户端构造请求消息、服务端解析请求和构造响应,都依赖于 .pb.cc 的实现)

比如:IM.Login.proto 文件内容如下

bash 复制代码
syntax = "proto3";
package IM.Login;

// 定义服务
service ImLogin {
   rpc Regist(IMRegistReq) returns (IMRegistRes) {} 
   rpc Login(IMLoginReq) returns (IMLoginRes) {}
}

// 注册账号
message IMRegistReq{
    string user_name = 1; // 用户名
    string password = 2; // 密码
}
// 注册返回
message  IMRegistRes{
    string user_name = 1; // 用户名
    uint32 user_id = 2; 
    uint32 result_code = 3; // 返回0的时候注册注册
}

message IMLoginReq{
    string user_name = 1; // 用户名
    string password = 2; // 密码
}
message  IMLoginRes{
    uint32 user_id = 1; 
    uint32 result_code = 2; // 返回0的时候注册注册
}

由于里面定义了服务,运行protoc --go_out=. --go-grpc_out=. user_service.proto后:

除了生成.pb.cc .pb.h之外 还会生成.grpc.pb.cc和.grpc.pb.h,里面包含 service(服务端基类)和 stub(客户端代理对象)的声明和实现

1、service:规定了服务器提供哪些远程方法(函数)可供调用。服务器开发者需要继承 这个自动生成的类,并重写​​ 里面定义的虚函数方法。这些方法里包含了真正的业务逻辑。

Go 复制代码
// 这是 protoc 自动生成的 Service 基类
class Service : public ::grpc::Service {
 public:
  Service();
  virtual ~Service();
  virtual ::grpc::Status Regist(::grpc::ServerContext* context, const ::IM::Login::IMRegistReq* request, ::IM::Login::IMRegistRes* response);
  virtual ::grpc::Status Login(::grpc::ServerContext* context, const ::IM::Login::IMLoginReq* request, ::IM::Login::IMLoginRes* response);
};

2、stub:客户端只需要持有一个 stub(代理对象),通过它调用远程方法

Go 复制代码
// 这是 protoc 自动生成的 Stub 类
class Stub final : public StubInterface {
 public:
  Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options = ::grpc::StubOptions());
  ::grpc::Status Regist(::grpc::ClientContext* context, const ::IM::Login::IMRegistReq& request, ::IM::Login::IMRegistRes* response) override;
  std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::IM::Login::IMRegistRes>> AsyncRegist(::grpc::ClientContext* context, const ::IM::Login::IMRegistReq& request, ::grpc::CompletionQueue* cq);
  std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::IM::Login::IMRegistRes>> PrepareAsyncRegist(::grpc::ClientContext* context, const ::IM::Login::IMRegistReq& request, ::grpc::CompletionQueue* cq);
  ::grpc::Status Login(::grpc::ClientContext* context, const ::IM::Login::IMLoginReq& request, ::IM::Login::IMLoginRes* response) override;
  std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::IM::Login::IMLoginRes>> AsyncLogin(::grpc::ClientContext* context, const ::IM::Login::IMLoginReq& request, ::grpc::CompletionQueue* cq);
  std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::IM::Login::IMLoginRes>> PrepareAsyncLogin(::grpc::ClientContext* context, const ::IM::Login::IMLoginReq& request, ::grpc::CompletionQueue* cq);
  // ... 还有 async 相关接口 ...
};

gRPC再基于HTTP2协议传输,将调用请求发送到服务端。

为什么采用HTTP2.0?

因为HTTP2.0支持pipeline发送请求,并且不限制服务端的处理响应顺序,而HTTP1.0只能一次发送一个请求不支持pipeline,HTTP1.1支持pipeline但是响应必须按照请求的顺序。

二、gRPC的四种模式 "流"

一元RPC模式:客户端发送一个请求,服务端回复一个响应

服务器端流RPC模式:客户端发送一个请求,服务端可以回复多个响应

客户端流RPC模式:客户端可以发送多个请求,服务端回复一个响应

双向流RPC模式:客户端可以发送多个请求,服务端可以回复多个响应

cpp 复制代码
rpc ListFeatures(Rectangle)returns (stream Feature){}    服务端返回流  即返回多个响应


客户端读取响应的方式:
std::unique ptr<clientReader<Feature> reader(    //Feature是返回消息类型
stub_->ListFeatures(&context,rect));

持续读取
while(reader->Read(&feature)){
    std::cout<< "Found feature called "<< feature.name()<<" at "
    <<feature.location().latitude()/kcoordFactor_ <<","
    <<feature.location().longitude()/ kcoordFactor_<< std::endl
}
Status status =reader->Finish();

三、gRPC的同步和异步

3.1在客户端调用方面

直观的区分就是客户端发送请求后,是持续等待服务端返回,还是先去干别的活等待返回了再去处理响应。

同步:

cpp 复制代码
// 1. 创建 Channel 和存根
auto channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
GreeterClient greeter(channel);

// 2. 准备请求
std::string user("world");
HelloReply reply;

// 3. 发起同步调用!这一行会阻塞线程,直到收到回复或出错。
Status status = greeter.SayHello(user, &reply);

// 4. 线程被唤醒,检查状态并使用回复
if (status.ok()) {
  std::cout << "Greeter received: " << reply.message() << std::endl;
} else {
  std::cout << "RPC failed: " << status.error_message() << std::endl;
}

异步:其核心是利用 ​CompletionQueue(完成队列,CQ)​​ 来解耦请求的发送和响应的接收。

cpp 复制代码
// 1. 创建 Channel、存根 和 最重要的 CompletionQueue
auto channel = ...;
GreeterClient greeter(channel);
CompletionQueue cq;

// 2. 准备请求和用于存储响应的对象
HelloRequest request;
request.set_name("world");
HelloReply reply;
Status status;

// 3. 创建上下文,并发起异步调用!此调用立即返回,不会阻塞。
ClientContext context;
// 注:这里使用 异步 方法,并传入了 cq 和一个唯一的 tag(指unique_ptr指针,标记这次请求)
std::unique_ptr<ClientAsyncResponseReader<HelloReply>> rpc( 
    stub_->AsyncSayHello(&context, request, &cq));

// 4. 告诉 gRPC 当 Finish 操作完成(即收到响应)时,将 tag 为 (void*)1 的事件放入 CQ
rpc->Finish(&reply, &status, (void*)1);

// 5. 主线程可以继续做其他事情...
// 同时,另一个工作线程(或主线程在某个时刻)会处理 CQ:

void* received_tag;
bool ok = false;
// 6. 等待事件完成。这里会阻塞,直到有响应到达。
cq.Next(&received_tag, &ok);

// 7. 通过 tag 识别是哪个请求完成了
if (received_tag == (void*)1) {
  // 8. 检查操作状态并处理响应
  if (ok && status.ok()) {
    // 处理 reply
  }
}

unique_ptr:它性能最好,语义最清晰,但是一次只能被一个对象持有。

场景:

gRPC 异步调用这样所有权转移路径非常明确,且需要标记服务端的响应,适合用unique_ptr。比如请求的是(1,2,3),响应的可能是(3,2,1),没有标记会无法正常接收。

shared_ptr:看似很智能,其实消耗较大,在需要用指针的情况下优先考虑unique_ptr。对于多个持有者,不确定谁先释放的情况,可以考虑shared_ptr。

场景:

在网络编程中,一个连接会话会被多个线程持有,接收线程会接收数据,工作线程会处理连接上的业务逻辑,心跳线程检查连接是否存活。此时各持有者的生命周期不确定,且不能由任意一个持有者放弃就销毁连接,此时shared_ptr的计数可以发挥作用。

3.2在服务端处理方面

不论同步还是异步处理,服务端都会采用多个epoll实例来均衡监听fd的压力。

在服务端的epoll监听到fd创建连接后,客户端发起调用请求如Register(),方法不会阻塞而是直接投递到 完成队列中,并且注册一个事件,服务端主线程不断从完成队列中取出事件(tag),执行对应的proceed函数。整体是一个事件驱动的异步模型,异步不是靠多线程堆积出来的,而是请求遇到等待的时候,工作线程去执行别的请求先,等待完成了,再加入回队列,工作线程会再取出执行。

注:具体的状态切换是在grpc框架的io底层帮助实现,服务端只需要:

cpp 复制代码
service_->RequestRegist(&ctx_, &request_, &responder_, cq_, cq_, this);

在这里注册了一个"监听器",告诉 gRPC:"有新请求时请通知我"。
之后的所有底层操作(如等待新请求、网络I/O、等待客户端响应、响应完成后投递事件到 CompletionQueue)都由 gRPC 框架自动管理。

不需要手动处理这些底层状态,gRPC 会在合适的时机把事件(tag)投递到队列,你只需要在 Proceed() 里处理业务逻辑即可。

四、手写rpc流程

4.1初始流程

  1. 编写proto文件
  2. 根据proto文件生成对应的pb.cc pb.h grpc.pb.cc grpc.pb.h文件
  3. server程序继承grpc.pb.cc中的Service类
  4. client程序继承grpc.pb.cc中的stub类
  5. 编译server 和 client程序

4.2增加 service 和 rpc 流程

(1)在 .proto 文件中增加 service 和 rpc 方法

例如,添加一个新服务和新方法:

cpp 复制代码
service MyService {
  rpc MyMethod (MyRequest) returns (MyResponse);
}

或者在已有 service 里增加新 rpc:

cpp 复制代码
service ImLogin {
  rpc Login (IMLoginReq) returns (IMLoginRes);
  rpc NewRpc (NewReq) returns (NewRes); // 新增的rpc
}

(2)重新生成代码

使用 protoc 工具(带 gRPC 插件)重新生成对的 .pb.h、.pb.cc、.grpc.pb.h、.grpc.pb.cc 文件。

(3)在服务端实现新 service 或新 rpc 方法

新 service:继承新生成的 Service 基类,实现所有 rpc 方法。

新 rpc:在已有 Service 派生类中实现新方法。

(4)在客户端调用新 service 或新 rpc

新 service:创建对应的 Stub,调用新方法。

新 rpc:用 Stub 调用新方法。

(5)重新编译并部署服务端和客户端

五、客户端和服务端代码编写实例

同步服务端

cpp 复制代码
#include <iostream>
#include <string>

// grpc头文件
#include <grpcpp/ext/proto_server_reflection_plugin.h>
#include <grpcpp/grpcpp.h>
#include <grpcpp/health_check_service_interface.h>

// 包含我们自己proto文件生成的.h
#include "IM.Login.pb.h"
#include "IM.Login.grpc.pb.h"

// 命名空间
// grcp
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
// 自己proto文件的命名空间
using IM::Login::ImLogin;
using IM::Login::IMRegistReq;
using IM::Login::IMRegistRes;
using IM::Login::IMLoginReq;
using IM::Login::IMLoginRes;


class IMLoginServiceImpl : public ImLogin::Service {
    // 注册
    virtual  Status Regist(ServerContext* context, const IMRegistReq* request, IMRegistRes* response) override {
        std::cout << "Regist user_name: " << request->user_name() << std::endl;

        response->set_user_name(request->user_name());
        response->set_user_id(10);
        response->set_result_code(0);

        return Status::OK;
    }
 
    // 登录
    virtual  Status Login(ServerContext* context, const IMLoginReq* request, IMLoginRes* response) override {
        std::cout << "Login user_name: " << request->user_name() << std::endl;
        response->set_user_id(10);
        response->set_result_code(0);
        return Status::OK;
    }
  
};


void RunServer() 
{
    std::string server_addr("0.0.0.0:50051");

    // 创建一个服务类
    IMLoginServiceImpl service;

    //由gRPC C++ 库提供的类 用于配置和创建一个 gRPC 服务器
    ServerBuilder builder;

    //配置服务器参数
    builder.AddListeningPort(server_addr, grpc::InsecureServerCredentials());
    builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_TIME_MS, 5000);
    builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 10000);
    builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, 1);

    //把我们自己的服务注册到服务器上
    builder.RegisterService(&service);
   
    //创建/启动
    std::unique_ptr<Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << server_addr << std::endl;
    // 进入服务循环
    server->Wait();
}

// 怎么编译?
// 手动编译
// 通过cmake的方式
int main(int argc, const char** argv)
{
    RunServer();
    return 0;
}

客户端

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
// /usr/local/include/grpcpp/grpcpp.h
#include <grpcpp/grpcpp.h>

// 包含我们自己proto文件生成的.h
#include "IM.Login.pb.h"
#include "IM.Login.grpc.pb.h"

// 命名空间
// grcp
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
// 自己proto文件的命名空间
using IM::Login::ImLogin;
using IM::Login::IMRegistReq;
using IM::Login::IMRegistRes;
using IM::Login::IMLoginReq;
using IM::Login::IMLoginRes;

//客户端封装类,用来组织和管理 gRPC 客户端的调用逻辑。   无需继承
class ImLoginClient
{
public:
    ImLoginClient(std::shared_ptr<Channel> channel)
        :stub_(ImLogin::NewStub(channel)) {

    }
    void Regist(const std::string &user_name, const std::string &password) {
        IMRegistReq request;
        request.set_user_name(user_name);
        request.set_password(password);
        
        IMRegistRes response;
        ClientContext context;
        
        //通过 stub_ 调用远程的 Regist 方法,向服务端发起注册请求
        Status status = stub_->Regist(&context, request, &response);
        if(status.ok()) {
            std::cout <<  "user_name:" << response.user_name() << ", user_id:" << response.user_id() << std::endl;
        } else {
            std::cout <<  "user_name:" << response.user_name() << "Regist failed: " << response.result_code()<< std::endl;
        }
    }

     void Login(const std::string &user_name, const std::string &password) {
        IMLoginReq request;
        request.set_user_name(user_name);
        request.set_password(password);
        
        IMLoginRes response;
        ClientContext context;
        
        Status status = stub_->Login(&context, request, &response);
        if(status.ok()) {
            std::cout <<  "user_id:" << response.user_id() << " login ok" << std::endl;
        } else {
            std::cout <<  "user_name:" << request.user_name() << "Login failed: " << response.result_code()<< std::endl;
        }
    }

private:
    //IM.Login.grpc.pb.cc 生成的Stub类
    std::unique_ptr<ImLogin::Stub> stub_;   
};


int main() 
{
    // 服务器的地址
    std::string server_addr = "localhost:50051";
    ImLoginClient im_login_client(
        grpc::CreateChannel(server_addr, grpc::InsecureChannelCredentials())
    );
    std::string user_name = "darren";
    std::string password = "123456";

    im_login_client.Regist(user_name, password);
    im_login_client.Login(user_name, password);

    return 0;
}

异步服务端

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <thread>

#include "IM.Login.grpc.pb.h"
#include "IM.Login.pb.h"

#include <grpc/support/log.h>
#include <grpcpp/grpcpp.h>

using grpc::Server;
using grpc::ServerAsyncResponseWriter;
using grpc::ServerBuilder;
using grpc::ServerCompletionQueue;
using grpc::ServerContext;
using grpc::Status;

// 自己proto文件的命名空间
using IM::Login::ImLogin;
using IM::Login::IMLoginReq;
using IM::Login::IMLoginRes;
using IM::Login::IMRegistReq;
using IM::Login::IMRegistRes;

//不是继承 Service,而是持有 AsyncService,用于异步注册和管理所有 rpc
class ServerImpl final {
 public:
    ~ServerImpl() {
        server_->Shutdown();
        cq_->Shutdown();
    }

    void Run() {  // 启动
        std::string server_address("0.0.0.0:50051");

        ServerBuilder builder;
        builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
        builder.RegisterService(&service_);
        cq_ = builder.AddCompletionQueue();
        server_ = builder.BuildAndStart();
        std::cout << "Server listening on " << server_address << std::endl;

        HandleRpcs();
    }

 private:
 /*
 calldata子类 
 提供虚函数 Proceed(),作为注册和登录的基类,让他们在里面实现具体的事件处理逻辑,就像同步里面重写的登录和注册接口,这里只是再他们之上再加封装一层calldata

 封装的方面:
 1、统一管理请求的生命周期和状态(CREATE、PROCESS、FINISH),便于异步下请求的状态区分。
 2、封装一次 RPC 请求的所有上下文信息

 调用:
调用proceed()的过程就是从cq_队列中取出事件,然后通过tag(this指针)找到对应的calldata对象,调用其Proceed()方法处理事件
 */
    class CallData {
    public:
        CallData(ImLogin::AsyncService* service, ServerCompletionQueue* cq)
            : service_(service), cq_(cq), status_(CREATE) {
            Proceed();
        }
        virtual ~CallData(){}
        virtual void Proceed() {
            // std::cout << "CallData Prceed" << std::endl//;
            return;
        }
        ImLogin::AsyncService* service_;
        ServerCompletionQueue* cq_;
        ServerContext ctx_;
        enum CallStatus { CREATE, PROCESS, FINISH };
        CallStatus status_; 
    };

    class RegistCallData : public CallData {
    public:
        RegistCallData(ImLogin::AsyncService* service, ServerCompletionQueue* cq) 
            :CallData(service, cq), responder_(&ctx_) {
            Proceed();
        }
        ~RegistCallData() {}
        void Proceed() override {
            // std::cout << "RegistCallData Prceed" << std::endl//;
            std::cout << "this: " << this
                    << " RegistCallData Proceed(), status: " << status_
                    << std::endl;   // darren 增加
            if (status_ == CREATE) {  // 0
                // Make this instance progress to the PROCESS state.
                status_ = PROCESS;

//异步的关键  grpc底层会帮助状态的切换   然后在请求阻塞解除后再加入队列
                service_->RequestRegist(&ctx_, &request_, &responder_, cq_, cq_, this);

            } else if (status_ == PROCESS) {  // 1
                new RegistCallData(service_, cq_);  // 1. 创建处理逻辑

                reply_.set_user_name(request_.user_name());
                reply_.set_user_id(10);
                reply_.set_result_code(0);

                status_ = FINISH;
                responder_.Finish(reply_, Status::OK, this);
            } else {
                GPR_ASSERT(status_ == FINISH);
                delete this;
            }
        }
    private:
        IMRegistReq request_;
        IMRegistRes reply_;
        ServerAsyncResponseWriter<IMRegistRes> responder_;
    };

    class LoginCallData : public CallData {
    public:
        LoginCallData(ImLogin::AsyncService* service, ServerCompletionQueue* cq) 
            :CallData(service, cq), responder_(&ctx_) {
            Proceed();
        }
        ~LoginCallData() {}
        void Proceed() override {
            // std::cout << "LoginCallData Prceed" << std::endl//;
            std::cout << "this: " << this
                    << " LoginCallData Proceed(), status: " << status_
                    << std::endl;   // darren 增加
            if (status_ == CREATE) {  // 0
                std::cout << "this: " << this << " LoginCallData Proceed(), status: "
                            << "CREATE" << std::endl;
                status_ = PROCESS;
//异步的关键  grpc底层会帮助状态的切换   然后在请求阻塞解除后再加入队列
                service_->RequestLogin(&ctx_, &request_, &responder_, cq_, cq_, this);

            } else if (status_ == PROCESS) {  // 1
                std::cout << "this: " << this << " LoginCallData Proceed(), status: "
                            << "PROCESS" << std::endl;
                new LoginCallData(service_, cq_);  // 1. 创建处理逻辑

                reply_.set_user_id(10);
                reply_.set_result_code(0);

                status_ = FINISH;
                responder_.Finish(reply_, Status::OK, this);
            } else {
                std::cout << "this: " << this << " LoginCallData Proceed(), status: "
                            << "FINISH" << std::endl;
                GPR_ASSERT(status_ == FINISH);
                delete this;
            }
        }
    private:
        IMLoginReq request_;
        IMLoginRes reply_;
        ServerAsyncResponseWriter<IMLoginRes> responder_;
    };
    //事件主循环
    void HandleRpcs() {  // 可以运行在多线程  这样就可以扩大并发能力  但单线程也已经是异步了

        //为注册和登录两个rpc方法创建监听对象  类似epoll的注册事件 这样才能监听到客户端的请求 放入队列
        new RegistCallData(&service_, cq_.get()); 
        new LoginCallData(&service_, cq_.get());
        void* tag;                              
        bool ok;
        while (true) {  //主线程不断从 CompletionQueue 取出事件(指针),并调用对应对象的 Proceed() 方法处理,实现事件驱动。
            GPR_ASSERT(cq_->Next(&tag, &ok));
            GPR_ASSERT(ok);
            static_cast<CallData*>(tag)->Proceed();
        }
    }

    std::unique_ptr<ServerCompletionQueue> cq_;     //所有异步事件(如新请求、响应完成)都通过 CompletionQueue 分发。
    
    ImLogin::AsyncService service_;//在builder中注册服务  也在calldata中注册请求
    
    std::unique_ptr<Server> server_;//负责整个 gRPC 服务器的启动和管理
};

int main(int argc, char** argv) {
  ServerImpl server;
  server.Run();

  return 0;
}
相关推荐
十五年专注C++开发4 小时前
C++类型转换通用接口设计实现
开发语言·c++·跨平台·类设计
胡萝卜3.04 小时前
掌握string类:从基础到实战
c++·学习·string·string的使用
江公望4 小时前
通过QQmlExtensionPlugin进行Qt QML插件开发
c++·qt·qml
Syntech_Wuz5 小时前
从 C 到 C++:容器适配器 std::stack 与 std::queue 详解
数据结构·c++·容器··队列
艾莉丝努力练剑6 小时前
【C++STL :stack && queue (一) 】STL:stack与queue全解析|深入使用(附高频算法题详解)
linux·开发语言·数据结构·c++·算法
胡萝卜3.06 小时前
深入理解string底层:手写高效字符串类
开发语言·c++·学习·学习笔记·string类·string模拟实现
kyle~6 小时前
计算机系统---CPU的进程与线程处理
linux·服务器·c语言·c++·操作系统·计算机系统
只是懒得想了6 小时前
用C++实现一个高效可扩展的行为树(Behavior Tree)框架
java·开发语言·c++·design-patterns
bkspiderx6 小时前
C++设计模式之行为型模式:模板方法模式(Template Method)
c++·设计模式·模板方法模式