对于同步API而言,程序的吞吐量并不高。因为在每次发送一个gRPC请求时,会阻塞整个线程,必须等待服务端的ack回到客户端才能继续运行或者发送下一个请求,因此异步API是提升程序吞吐量的必要手段。

gRPC异步操作依赖于完成队列CompletionQueue

官网教程:https://grpc.io/docs/languages/cpp/async/

参考博客1:https://www.luozhiyun.com/archives/671

参考博客2:https://blog.miigon.net/posts/cn-so-difference-between-sync-and-async-grpc/

整体思路概述:

  • 将一个完成队列CompletionQueue绑定到RPC调用
  • 在客户端与服务器两端执行写入或读取之类的操作,同时带有唯一的void*标签
  • 调用CompletionQueue::Next以等待操作完成。如果出现标签,则表示相应的操作完成

异步客户端:

要点

  • 就像同步客户端一样,我们需要创建一个存根stub用于进行gRPC方法的调用,但是此时我们需要调用的是异步方法,需要为其绑定一个完成队列
  1. //客户端的类实例在初始化时就需要创建Channel和Stub了,具体看官方实例代码中的greeter_async_client.cc
  2. CompletionQueue cq;//创建完成队列
  3. std::unique_ptr<ClientAsyncResponseReader<HelloReply> > rpc(
  4. stub_->AsyncSayHello(&context, request, &cq));//将完成队列绑定到存根,进而创建出客户端异步响应读取器
  • rpc->StartCall()初始化RPC请求
  • rpc->Finish()有三个参数,分别是gRPC响应、最终状态、唯一标签。一旦RPC请求完成,响应的结果、最终状态会被封装到前两个传入的参数中,同时会附带唯一标签。唯一标签可以使用RPC请求的地址,这样一来,当最终的响应到来时,我们可以拿着该地址进行相应处理。
  • 异步处理响应,拿着唯一标签可以获取对应RPC请求的地址,进而进行相关处理。
  • 为了使得请求的发出与响应的处理相互之间不阻塞,需要把响应的处理独立出一个线程

代码示例

  1. class GreeterClient {
  2. public:
  3. explicit GreeterClient(std::shared_ptr<Channel> channel)
  4. : stub_(Greeter::NewStub(channel)) {}
  5. // 客户端SayHello
  6. void SayHello(const std::string& user) {
  7. // RPC请求数据封装
  8. HelloRequest request;
  9. request.set_name(user);
  10. // 异步客户端请求,存储请求响应的状态和数据的结构体等,在下方进行的定义
  11. AsyncClientCall* call = new AsyncClientCall;
  12. // 初始化response_reader
  13. // stub_->PrepareAsyncSayHello()创建一个RPC对象,但是不会立即启动RPC调用
  14. call->response_reader =
  15. stub_->PrepareAsyncSayHello(&call->context, request, &cq_);
  16. // StartCall()方法发起真正的RPC请求
  17. call->response_reader->StartCall();
  18. // Finish()方法前两个参数用于指定响应数据的存储位置,第三个参数指定了该次RPC异步请求的地址
  19. call->response_reader->Finish(&call->reply, &call->status, (void*)call);
  20. }
  21. // 不断循环监听完成队列,对响应进行处理
  22. void AsyncCompleteRpc() {
  23. void* got_tag;
  24. bool ok = false;
  25. // 在队列为空时阻塞,队列中有响应结果时读取到got_tag和ok两个参数
  26. // 前者是结果对应的RPC请求的地址,后者是响应的状态
  27. while (cq_.Next(&got_tag, &ok)) {
  28. // 类型转换,获取到的实际上是此响应结果对应的RPC请求的地址,在这个地址下保存了实际的响应结果数据
  29. AsyncClientCall* call = static_cast<AsyncClientCall*>(got_tag);
  30. // 验证请求是否真的完成了
  31. GPR_ASSERT(ok);
  32. if (call->status.ok())
  33. std::cout << "Greeter received: " << call->reply.message() << std::endl;
  34. else
  35. std::cout << "RPC failed" << std::endl;
  36. // 完成了响应的处理后,清除该RPC请求
  37. delete call;
  38. }
  39. }
  40. private:
  41. // 异步客户端通话,存储一次RPC通话的信息,里面包含响应的状态和数据的结构
  42. struct AsyncClientCall {
  43. // 服务器返回的响应数据
  44. HelloReply reply;
  45. // 客户端的上下文信息,可以被用于向服务器传达额外信息或调整某些RPC行为
  46. ClientContext context;
  47. // RPC响应的状态
  48. Status status;
  49. // 客户端异步响应读取器
  50. std::unique_ptr<ClientAsyncResponseReader<HelloReply>> response_reader;
  51. };
  52. // 存根,在我们的视角里就是服务器端暴露的服务接口
  53. std::unique_ptr<Greeter::Stub> stub_;
  54. // 完成队列,一个用于gRPC异步处理的生产者消费者队列
  55. CompletionQueue cq_;
  56. };
  57. int main(int argc, char** argv) {
  58. // 实例化一个客户端,需要一个信道,第二个参数表明该通道未经过身份验证
  59. GreeterClient greeter(grpc::CreateChannel(
  60. "localhost:50051", grpc::InsecureChannelCredentials()));
  61. // 独立的异步响应处理线程
  62. // 由于该方法是客户端类内的非静态方法,所以需要传入客户端类的实例表明归属
  63. std::thread thread_ = std::thread(&GreeterClient::AsyncCompleteRpc, &greeter);
  64. //发送异步的请求SayHello()
  65. for (int i = 0; i < 100; i++) {
  66. std::string user("world " + std::to_string(i));
  67. greeter.SayHello(user); // The actual RPC call!
  68. }
  69. std::cout << "Press control-c to quit" << std::endl << std::endl;
  70. thread_.join(); //永远会阻塞,因为异步响应处理线程永远不会停止,必须ctrl+c才能退出
  71. return 0;
  72. }

异步服务器

要点

  • 客户端在发起请求时附带了标签(此次RPC请求会话的地址),因此服务器端也需要将该标签妥善处理再返回
  • 官方API中是准备一个CallData对象作为容器,gRPC通过ServerCompletionQueue将各种事件发送到CallData对象中,然后让这儿对象根据自身的状态进行处理。处理完成后还需要再手动创建一个CallData对象,这个对象是为下一个Client请求准备的,整个过程就像流水线一样
    • CREATE:CallData对象被创建处理之前处于CREATE状态
    • PROCESS:请求到达后,转换为PROCESS状态
    • FINISH:响应完成后,转换为FINISH状态
  • 整体服务器端流程
    • 启动服务时,预分配 一个 CallData 实例供未来客户端请求使用。
    • 该 CallData 对象构造时,service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_, this) 将被调用,通知gRPC开始准备接收恰好是一个SayHello请求。
    • 这时候我们还不知道请求会由谁发出,何时到达,我们只是告诉 gRPC 说我们已经准备好接收了,让 gRPC 在真的接收到时通知我们。
    • 供给 RequestSayHello的参数告诉了gRPC将上下文信息、请求体以及回复器放在哪里、使用哪个完成队列来通知、以及通知的时候,用于鉴别请求的 tag(在这个例子中,this 被作为 tag 使用)。

      HandleRpcs() 运行到 cq->Next() 并阻塞。等待下一个事件发生
    • 客户端发送一个 SayHello 请求到服务器,gRPC 开始接收并解码该请求(IO 操作)
    • 一段时间后….
    • gRPC接收请求完成了。它将请求体放入CallData对象的request_成员中(通过我们之前提供的指针),然后创建一个事件(使用指向CallData 对象的指针作为 tag),并 将该事件放到完成队列 cq_ 中.
    • HandleRpcs() 中的循环接收到了该事件(之前阻塞住的 cq->Next() 调用此时也返回),并调用 CallData::Proceed() 来处理请求。
    • CallData 的 status_ 属性此时是 PROCESS,它做了如下事情:
      • 创建一个新的 CallData 对象,这样在这个请求后的新请求才能被新对象处理。
      • 生成当前请求的回复,告诉 gRPC 我们处理完成了,将该回复发送回客户端
      • gRPC 开始回复的传输 (IO 操作)
      • HandleRpcs() 中的循环迭代一次,再次阻塞在 cq->Next(),等待新事件的发生。
    • 一段时间后….
    • gRPC 完成了回复的传输,再次通过在完成队列里放入一个以 CallData 指针为 tag 的事件的方式通知我们。
    • cq->Next() 接收到该事件并返回,CallData::Proceed() 将 CallData 对象释放(使用 delete this;)。HandleRpcs() 循环并重新阻塞在 cq->Next() 上,等待新事件的发生。

      整个过程看似和同步 API 很相似,只是多了对完成队列的控制。然而,通过这种方式,每一个 一段时间后.... (通常是在等待 IO 操作的完成或等待一个请求出现) cq->Next() 不仅可以接收到当前处理的请求的完成事件,还可以接收到其他请求的事件。

      所以假设第一个请求正在等待它的回复数据传输完成时,一个新的请求到达了,cq->Next() 可以获得新请求产生的事件,并开始并行处理新请求,而不用等待第一个请求的传输完成

代码示例

  1. //服务器实现类
  2. class ServerImpl final {
  3. public:
  4. ~ServerImpl() {
  5. server_->Shutdown();
  6. // 关闭服务器后也要关闭完成队列
  7. cq_->Shutdown();
  8. }
  9. // There is no shutdown handling in this code.
  10. void Run() {
  11. // 服务器地址和端口
  12. std::string server_address("0.0.0.0:50051");
  13. // 服务器构建器
  14. ServerBuilder builder;
  15. // 服务器IP与端口指定,第二个参数表示该通道未经过身份验证
  16. builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  17. // 注册服务
  18. builder.RegisterService(&service_);
  19. // 为当前服务器创建完成队列
  20. cq_ = builder.AddCompletionQueue();
  21. // 构建并启动服务器
  22. server_ = builder.BuildAndStart();
  23. std::cout << "Server listening on " << server_address << std::endl;
  24. // 运行服务器的主流程
  25. HandleRpcs();
  26. }
  27. private:
  28. // 处理一个请求所需要保存的状态、逻辑、数据被封装成了CallData类
  29. class CallData {
  30. public:
  31. // 传入service实例和服务器端的完成队列,创建后status_是CREATE状态
  32. CallData(Greeter::AsyncService* service, ServerCompletionQueue* cq)
  33. : service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {
  34. // 立即调用服务逻辑
  35. Proceed();
  36. }
  37. void Proceed() {
  38. if (status_ == CREATE) {
  39. // 转换为PROCESS状态
  40. status_ = PROCESS;
  41. // 作为初始化的一部分,请求系统开始处理SayHello请求。
  42. // 此处的this指代此CallData实例
  43. service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_,
  44. this);
  45. } else if (status_ == PROCESS) {
  46. // 在执行当前CallData的任务时,创建一个新的CallData实例去为新的请求服务
  47. // 当前CallData会在FINISH阶段自行销毁
  48. new CallData(service_, cq_);
  49. // 实际的逻辑处理
  50. std::string prefix("Hello ");
  51. reply_.set_message(prefix + request_.name());
  52. // 在完成后将状态置为FINISH。使用当前CallData的地址作为该事件的标签
  53. status_ = FINISH;
  54. responder_.Finish(reply_, Status::OK, this);
  55. } else {
  56. GPR_ASSERT(status_ == FINISH);
  57. // 销毁当前CallData
  58. delete this;
  59. }
  60. }
  61. private:
  62. // 异步服务
  63. Greeter::AsyncService* service_;
  64. //服务器端的完成队列,是一个生产者-消费者队列
  65. ServerCompletionQueue* cq_;
  66. // 服务器端上下文信息,可以被用于向客户端传达额外信息、数据或调整某些RPC行为
  67. ServerContext ctx_;
  68. // 客户端发来的请求
  69. HelloRequest request_;
  70. // 服务端的响应
  71. HelloReply reply_;
  72. // 发送服务端响应的工具
  73. ServerAsyncResponseWriter<HelloReply> responder_;
  74. // 状态机定义
  75. enum CallStatus { CREATE, PROCESS, FINISH };
  76. CallStatus status_; // 当前的状态
  77. };
  78. // 如果有需求的话,服务器的处理可以是多线程的
  79. void HandleRpcs() {
  80. // 创建一个新的CallData,将完成队列中的数据封装进去
  81. new CallData(&service_, cq_.get());
  82. void* tag; // 请求特有的标签,实际上是请求的RPC会话对象在客户端的地址信息
  83. bool ok;
  84. while (true) {
  85. // 阻塞等待读取完成队列中的事件,每个事件使用一个标签进行标识,该标签是CallData实例的地址
  86. // 完成队列的Next()方法应该每次都检查返回值,来确保事件和完成队列的状态正常
  87. GPR_ASSERT(cq_->Next(&tag, &ok));
  88. GPR_ASSERT(ok);
  89. // 从标签转换为CallData*类型,进而访问CallData中的方法
  90. static_cast<CallData*>(tag)->Proceed();
  91. }
  92. }
  93. // 当前服务器的完成队列
  94. std::unique_ptr<ServerCompletionQueue> cq_;
  95. // 当前服务器的异步服务
  96. Greeter::AsyncService service_;
  97. // 服务器实例
  98. std::unique_ptr<Server> server_;
  99. };
  100. int main(int argc, char** argv) {
  101. ServerImpl server;
  102. server.Run();
  103. return 0;
  104. }

【gRPC】C++异步服务端客户端API实例及代码解析的更多相关文章

  1. 【gRPC】C++异步服务端优化版,多服务接口样例

    官方的C++异步服务端API样例可读性并不好,理解起来非常的费劲,各种状态机也并不明了,整个运行过程也容易读不懂,因此此处参考网上的博客进行了重写,以求顺利读懂. C++异步服务端实例,详细注释版 g ...

  2. 基于JAX-WS的Web Service服务端/客户端 ;JAX-WS + Spring 开发webservice

    一.基于JAX-WS的Web Service服务端/客户端 下面描述的是在main函数中使用JAX-WS的Web Service的方法,不是在web工程里访问,在web工程里访问,参加第二节. JAX ...

  3. TCP Socket服务端客户端(二)

    本文服务端客户端封装代码转自https://blog.csdn.net/zhujunxxxxx/article/details/44258719,并作了简单的修改. 1)服务端 此类主要处理服务端相关 ...

  4. 手写内网穿透服务端客户端(NAT穿透)原理及实现

    Hello,I'm Shendi. 这天心血来潮,决定做一个内网穿透的软件. 用过花生壳等软件的就知道内网穿透是个啥,干嘛用的了. 我们如果有服务器(比如tomcat),实际上我们在电脑上开启了服务器 ...

  5. react服务端/客户端,同构代码心得

    FKP-REST是一套全栈javascript框架   react服务端/客户端,同构代码心得 作者:webkixi react服务端/客户端,同构代码心得 服务端,客户端同构一套代码,大前端的梦想, ...

  6. JAVA WEBSERVICE服务端&客户端的配置及调用(基于JDK)

    前言:我之前是从事C#开发的,因公司项目目前转战JAVA&ANDROID开发,由于对JAVA的各种不了解,遇到的也是重重困难.目前在做WEBSERVICE提供数据支持,看了网上相关大片的资料也 ...

  7. NTP时间同步 服务端 客户端 自动化安装配置

    NTP时间同步 服务端 客户端 自动化安装配置 原创内容 http://www.cnblogs.com/elvi/p/7657994.html #!/bin/sh #运行环境 centos6.cent ...

  8. chrony时间同步 服务端 客户端 安装配置

    chrony时间同步 服务端 客户端 安装配置 原创内容http://www.cnblogs.com/elvi/p/7658021.html #!/bin/sh #运行环境 centos7 #chro ...

  9. eclipse使用CXF3.1.*创建webservice服务端客户端以及客户端手机APP(二)

    eclipse使用CXF3.1.*创建webservice服务端客户端以及客户端手机APP(二) 接上篇博客,本篇博客主要包含两个内容: 4.使用Android studio创建webservice客 ...

随机推荐

  1. gslb(global server load balance)技术的一点理解

    gslb(global server load balance)技术的一点理解 前言 对于比较大的互联网公司来说,用户可能遍及海内外,此时,为了提升用户体验,公司一般会在离用户较近的地方建立机房,来服 ...

  2. centos7 ./configure --prefix error checking for C compiler

    解决方法: 输入以下命令 yum -y install gcc gcc-c++ autoconf automake make

  3. 使用supervisor设置应用开机自启

    安装supervisor: sudo apt install supervisor -y 创建配置文件: sudo vim /etc/supervisor/conf.d/frpc.conf frpc. ...

  4. NewApiDay03_File类

    File类创建一个新文件 File类的每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径) 使用File可以做到: 1:访问其表示的文件或目录的属性信息,例如:名字,大小 ...

  5. cookie和seesion的区别和联系

    今天来聊聊cookie和session的区别和联系.首先先确定一个各自的定义吧: cookies: 网站用于鉴别用户身份和追踪用户登录状态. 存在于浏览器端的一小段文本数据 session: 中文称之 ...

  6. 虚拟机上安装Linux系统

    1,打开VMware,文件--新建虚拟机 2,选择自定义 3,选择VMware版本,下一步 4,选择稍后安装操作系统,下一步 5,选择Linux,版本我这里用的是centos7 6, 设置虚拟名称,设 ...

  7. YII事件EVENT示例

    模型中/** * 在初始化时进行事件绑定 */ public function init() { $this->on(self::EVENT_HELLO,[$this,'sendMail']); ...

  8. 用户认证(Authentication)进化之路:由Basic Auth到Oauth2再到jwt

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_98 用户认证是一个在web开发中亘古不变的话题,因为无论是什么系统,什么架构,什么平台,安全性是一个永远也绕不开的问题 在HTTP ...

  9. Win10环境下使用Flask配合Celery异步推送实时/定时消息(Socket.io)/2020年最新攻略

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_163 首先得明确一点,和Django一样,在2020年Flask 1.1.1以后的版本都不需要所谓的三方库支持,即Flask-Ce ...

  10. Odoo14 给模块/应用加小图标

    # apps中的图标是固定路劲: static/description/icon.png # 主页图标是通过你的主menuitem的web_icon来设置的: <menuitem id=&quo ...