让远程服务调用得像本地函数一样自然——用一份 .proto 契约,打通两个进程、两种语言
核心方法论:艺术与工程
「伟大的工程都有艺术的内核:比例、节奏、留白。接口设计亦然——一份好的 .proto 契约,读起来像一张设计图,两端程序按图施工,严丝合缝。这一章,我们就用 gRPC 这副画笔,把\"跨机器调用\"这件事画成一件作品。」
前九章我们都在\"单机\"世界里打转:编译、链接、日志、测试,全在一个进程里。但真实系统是分布式的:用户下单,订单服务要问库存服务\"还有没有货\"。程序一旦拆到不同进程、不同机器上,\"怎么互相调用\"就成了新问题。最原始的办法是自己用 socket 收发字节流——可你要自己设计报文格式、处理粘包半包、定义错误码,第 8 章手写 HTTP 解析的痛苦换个皮再来一遍。于是,RPC 登场了。
RPC(Remote Procedure Call,远程过程调用)——说人话:让一段程序像调用本地函数一样调用另一台机器上的函数,中间的打包、传输、解包全部藏起来。核心思想是\"调用透明\":客户端这边有一个和远程函数长得一模一样的替身,叫做 stub(桩)——说人话就是\"本地替身\":你调用它,它替你打包参数、发到网络、收回应、拆包,再把结果像返回值一样还给你。调用方全程感觉不到网络的存在——这也是它最难的地方:透明之下藏着序列化、传输、并发、失败重试一大堆细节。
对比两种方案,就知道为什么没人手写协议了:
// 方案 A:自己用 socket 实现远程调用(手写协议,痛苦)
// 要自己解决:报文格式、字节序、粘包、半包、超时、重试、错误码……
// 每个接口都要写一遍"打包 + 发送 + 等待 + 解包",全是样板代码。
// 方案 B:用 RPC 框架(比如 gRPC)
// 你只需要:用 .proto 定义接口(10.3 讲)+ 实现函数体(10.6 讲)
// 剩下的序列化、传输、解包、重试,框架全部包办。
// RPC 的四个幕后动作(以"客户端发一个请求"为例)
// ① 序列化:把参数按约定格式打包成紧凑的二进制字节流(stub 帮你做)
// ② 传输:通过 HTTP/2 连接把字节流发给远程服务器
// ③ 服务器反序列化:还原参数,调用真正的实现函数,结果再序列化回传
// ④ 客户端反序列化:把返回的字节流还原成"返回值",交还给调用方
// 调用方全程只看到:int sum = remote_add(1, 2); // 返回 3
// 中间发生了什么,由 RPC 框架(stub 与 server)替你包办
第 8 章的 REST 也是远程调用,和 RPC 什么关系?REST 是\"风格约定\",通常基于 HTTP 文本传输,URL 描述资源;RPC 是\"方法调用\",接口像函数签名,数据用二进制。公开 Web API 多用 REST(浏览器友好),内部高性能调用多用 RPC——gRPC 正是后者的代表。
gRPC 是 Google 在 2015 年开源的高性能 RPC 框架(Apache 2.0 协议)——说人话:它把\"定义接口 → 生成代码 → 跑起服务\"整条流水线都做好了,你专心写业务。为什么在众多 RPC 框架里选它?三个理由最有说服力:
第一,传输层用 HTTP/2。HTTP/2 是多路复用(multiplexing)的——说人话:一条连接上可以同时跑很多个请求,互不排队,还能双向同时传数据,比 HTTP/1.1 排队高效得多。第二,序列化用 protobuf(10.3 的主角),紧凑的二进制格式——说人话:同样的数据,打包出的字节比 JSON/XML 这类文本格式少得多,拆包也快得多。第三,跨语言:一份接口契约,官方就支持 C++、Java、Python、Go 等十几种语言生成代码,客户端用 Python、服务端用 C++ 照样互通——\"每种语言各写各的\"在大公司就是这么活的。
gRPC 支持四种 RPC 类型,从\"一问一答\"到\"两边同时说\"(接口来自官方 route_guide 示例):
| 类型 | proto 声明 | 说人话 |
|---|---|---|
| 一元 RPC | rpc GetFeature(Point) returns (Feature) | 一问一答,最像普通函数调用 |
| 服务端流式 | rpc ListFeatures(Rectangle) returns (stream Feature) | 问一次,服务端连发一串回来 |
| 客户端流式 | rpc RecordRoute(stream Point) returns (RouteSummary) | 客户端连发一串,最后收一个总结 |
| 双向流式 | rpc RouteChat(stream RouteNote) returns (stream RouteNote) | 两边同时说,互不等待 |
// 四种 RPC 类型在 .proto 里的声明方式(stream 关键字决定流式方向)
service RouteGuide {
rpc GetFeature(Point) returns (Feature) {}
rpc ListFeatures(Rectangle) returns (stream Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
// gRPC 客户端的两个核心概念(10.6 会真正用上):
// channel(通道):到服务器的逻辑连接,管地址、凭据、重连
std::shared_ptr<grpc::Channel> channel =
grpc::CreateChannel("localhost:50051",
grpc::InsecureChannelCredentials());
// stub(桩):绑在 channel 上的"远程对象",调它的方法就是发一次 RPC
std::unique_ptr<Greeter::Stub> stub = Greeter::NewStub(channel);
gRPC 的传输要求两端都支持 HTTP/2,浏览器默认不支持它的二进制帧,需要 grpc-web 之类的代理中转。但服务器与服务器、服务器与 App 之间的通信,正是它的拿手好戏。
protobuf(Protocol Buffers,协议缓冲区)——说人话:Google 开发的一种序列化格式,把姓名、年龄、列表这类结构化数据压成紧凑的二进制;配套用一种叫 proto 的\"接口描述语言\"(IDL,Interface Definition Language,说人话:专门描述数据结构和接口的说明书语言)来写 .proto 文件。gRPC 用 .proto 文件同时定义两样东西:消息(数据结构)和服务(接口)。这份文件就是契约(contract)——说人话:客户端和服务端共同遵守的\"接口合同\",先谈好合同,两端再各自开工。
.proto 文件开头第一行是 syntax = "proto3";,声明使用第 3 版语法——说人话:proto3 比老版本更简洁,所有字段都有默认值(字符串默认空串、整数默认 0),不用手写初始化。接着用 message 定义数据结构:每个字段由\"类型 + 名字 + 编号\"组成。那个数字编号是字段在二进制格式里的\"身份证号\"——打包时用它标识\"这段数据是哪个字段\",所以编号不能重复,发布后更不能随便改,否则老客户端就解不出来了。
数据定义好了,再用 service 定义服务:一个服务就是一组 RPC 方法的集合,每个方法声明\"传入什么消息、返回什么消息\"。下面就是官方 helloworld 示例的完整 .proto 文件,全章的地基:
// 官方示例 examples/protos/helloworld.proto
syntax = "proto3"; // 使用 proto3 语法
package helloworld; // 包名:生成代码的命名空间(C++ 里是 namespace)
// 服务定义:一组 RPC 方法的集合
service Greeter {
// 一元 RPC:传 HelloRequest,返回 HelloReply
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 请求消息:一个 string 字段 name,编号 1
message HelloRequest {
string name = 1;
}
// 响应消息:一个 string 字段 message,编号 1
message HelloReply {
string message = 1;
}
// 字段编号是二进制格式识别字段的依据,改编号 = 破坏协议
message Person {
string name = 1; // 打包时用"编号 1"标记这段数据是 name
int32 age = 2; // 用"编号 2"标记 age
// 编号一旦发布就不能改;但可以追加新字段、用新编号:
string email = 3; // 后来加的字段,老客户端解析时会安全跳过
}
proto3 里标量字段没有\"是否填了\"的概念——没赋值就是默认值(0、空串),解析出来分不清\"没填\"和\"恰好是默认值\"。真想表达\"字段缺失\",要给字段加 optional 关键字或用包装类型。
.proto 只是设计图,不能直接运行。protoc(Protocol Buffer Compiler)——说人话:把 .proto 文件翻译成各种语言代码的编译器。C++ 要用两条命令:--cpp_out 生成消息的读写代码,--grpc_out 生成服务端基类和客户端 stub 代码。后者依赖 grpc_cpp_plugin——说人话:protoc 的\"外挂插件\",由 gRPC 官方提供,专门把 service 翻译成 C++ 骨架,用 --plugin=protoc-gen-grpc=路径 指定它的位置。
运行 protoc 后,当前目录会多出四个文件,分工如下:
# 官方编译命令(C++):先 --grpc_out 再 --cpp_out
protoc -I ../../protos --grpc_out=. \
--plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../../protos/helloworld.proto
protoc -I ../../protos --cpp_out=. ../../protos/helloworld.proto
# 生成的四个文件及其内容
helloworld.pb.h # 消息类声明:HelloRequest / HelloReply,set_name()、message() 等
helloworld.pb.cc # 消息类实现:二进制序列化 / 反序列化
helloworld.grpc.pb.h # 服务端基类 Greeter::Service + 客户端 Greeter::Stub 的声明
helloworld.grpc.pb.cc # 服务端 / 客户端骨架实现
# 四个文件都是自动生成的,顶部写着"DO NOT EDIT"——不要手改,下次生成就覆盖
官方示例的 Makefile 怎么知道插件在哪?靠 which 自动探测——这行看似不起眼的规则,正是 10.5 那个著名报错 which: no grpc_cpp_plugin 的来源:
# 官方 examples/cpp/helloworld/Makefile 关键规则(节选)
GRPC_CPP_PLUGIN ?= $(shell which grpc_cpp_plugin) # 找不到插件时,which 报错
PROTOC ?= $(shell which protoc)
helloworld.grpc.pb.cc: ../../protos/helloworld.proto
$(PROTOC) -I ../../protos --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN) $<
helloworld.pb.cc: ../../protos/helloworld.proto
$(PROTOC) -I ../../protos --cpp_out=. $<
想看生成代码长什么样?编译后打开 helloworld.grpc.pb.h:class Greeter 下嵌着 class Stub(客户端)和 class Service(服务端基类),还有 NewStub(...)——10.6 的代码全是从这里长出来的。生成代码是最好的\"API 字典\"。
gRPC 的依赖很多(protobuf、OpenSSL、zlib 等),当年(素材笔记,2020 年)在 CentOS 7 上必须从源码编译。下面的命令全部来自真实操作记录,可以照抄。第一步装编译工具和依赖包——注意 CMake 也要新版:yum 自带的版本太低,素材是去 cmake.org 下载源码安装的(./configure → make && make install),必要时用 ln -s 把新 cmake 指到 /usr/bin/cmake。
# 素材笔记(CentOS 7):安装编译依赖
yum install -y pkgconfig autoconf automake libtool make gcc-c++ unzip
yum install -y gflags-devel gtest-devel clang libcxx-devel
yum install -y openssl openssl-devel
yum install -y libunwind libunwind-devel
yum install -y epel-release
yum install -y golang # gRPC 工具链需要
# 克隆 gRPC 源码并初始化子模块(protobuf 就在 third_party 里)
git clone https://github.com/grpc/grpc.git
cd grpc
git submodule update --init
# 先编译 protobuf(素材原样):gRPC 依赖它生成消息代码
cd third_party/protobuf
git submodule update --init --recursive
./autogen.sh
./configure --prefix=/usr/local/helios/protobuf
make
make check
make install
# 软链 protoc 到系统路径并验证
ln -s /usr/local/helios/protobuf/bin/protoc /usr/local/bin/protoc
protoc --version
# 再编译 gRPC 本体(素材原样,CMake 方式)
cd grpc
mkdir -p cmake/build
cd cmake/build # ../../ 指向 grpc 根目录
cmake -DBUILD_SHARED_LIBS=on -DCMAKE_INSTALL_PREFIX=/usr/local/helios/grpc \
-DCMAKE_BUILD_TYPE=DEBUG -Wno-dev ../../
make
make install
装完跑官方示例 examples/cpp/helloworld,素材笔记里一连串环境报错,根因只有一个:东西装进了自定义前缀 /usr/local/helios/,而各工具默认的搜索路径不知道这件事。四个问题逐一解决:插件找不到(PATH)、头文件找不到(CPLUS_INCLUDE_PATH,第 4 章讲过)、pkg-config 找不到包(PKG_CONFIG_PATH)、运行时报动态库缺失(LD_LIBRARY_PATH)。
# 坑 1:make 时 protoc 插件找不到
# 报错:which: no grpc_cpp_plugin in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin)
# 插件在自定义路径,不在 PATH 里——软链到系统路径即可
ln -s /usr/local/helios/grpc/bin/grpc_cpp_plugin /usr/local/bin/grpc_cpp_plugin
# 坑 2:编译器找不到 protobuf 头文件
# 报错:helloworld.pb.h:10:40: fatal error: google/protobuf/port_def.inc: No such file or directory
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/helios/grpc/include
# 坑 3:pkg-config 找不到 protobuf / grpc 的 .pc 描述文件
# 报错:Package protobuf was not found in the pkg-config search path.
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/helios/protobuf/lib/pkgconfig/:/usr/local/helios/grpc/lib/pkgconfig/
# 坑 4:运行时动态库找不到
# 报错:error while loading shared libraries: libgrpc_plugin_support.so.1: cannot open shared object file
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/helios/grpc/lib/:/usr/local/helios/grpc/lib64/:/usr/local/helios/protobuf/lib
注意:素材还记录了一个 2020 年老版本 gRPC 的诡异问题:编译好的 helloworld 服务器运行后\"没有响应\"(官方 issue #21280),根因是初始化时 tracer 重复注册了同一个 TraceFlag,解法是给源码打两个补丁(src/core/lib/surface/init.cc 里注销 grpc_tracer_init(),src/core/lib/debug/trace.cc 的 TraceFlagList::Add 加去重检查)。这是历史版本 bug,新版本早已修复——现在遇到\"服务器卡死无响应\",先检查版本与日志,别急着改源码。
今天的官方 quickstart 已经不用这么折腾:git clone --recurse-submodules -b v1.81.0 --depth 1 https://github.com/grpc/grpc 一步带子模块,再 cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_CXX_STANDARD=17 -DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR ../.. 连 protobuf 一起装好。但素材的四连坑到今天依然天天发生——第 4 章的环境变量功夫,这里正好实战验收。
地基打好了,来看真家伙。10.3 的 greeter.proto 经 10.4 生成代码后,服务端要做的只有一件事:继承生成的 Greeter::Service 基类,覆写 SayHello。生成的基类里每个方法默认返回\"未实现\"状态(UNIMPLEMENTED),覆写才有真实业务逻辑。启动服务器的四个零件:ServerBuilder(说人话:服务器的\"装配车间\")→ AddListeningPort(绑定地址端口,第二参数是凭据,这里用不加密的)→ RegisterService(把实现注册进车间)→ BuildAndStart(建成并启动)。最后 Wait() 阻塞等待请求到来。
// greeter_server.cc(简化自官方示例:实现 Greeter::Service 并启动服务器)
#include <grpcpp/grpcpp.h>
#include <iostream>
#include <memory>
#include <string>
#include "helloworld.grpc.pb.h"
using namespace helloworld;
// 服务端实现:继承 Greeter::Service 覆写 SayHello
class GreeterServiceImpl final : public Greeter::Service {
grpc::Status SayHello(grpc::ServerContext* context,
const HelloRequest* request,
HelloReply* reply) override {
reply->set_message("Hello " + request->name()); // 把结果写进 reply
return grpc::Status::OK; // 返回 OK 表示成功
}
};
int main() {
std::string server_address("0.0.0.0:50051");
GreeterServiceImpl service; // 业务实现
grpc::ServerBuilder builder; // 装配车间
builder.AddListeningPort(server_address, // 绑定地址与端口
grpc::InsecureServerCredentials());
builder.RegisterService(&service); // 注册实现
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
server->Wait(); // 阻塞,等待请求
return 0;
}
客户端对称地来四步:CreateChannel 建通道 → Greeter::NewStub 造桩 → 填 HelloRequest → stub->SayHello(&context, request, &reply) 发 RPC。其中 ClientContext——说人话:一次调用的\"档案袋\",装着超时时间、元数据、取消信号,出问题时它是排查的第一现场。返回值 grpc::Status 是调用成败的\"体检报告\",status.ok() 为真才是成功。官方示例用类把调用包装起来(GreeterClient),这里直接内联,逻辑完全一样:
// greeter_client.cc(简化自官方示例:stub 发起一次 RPC)
#include <grpcpp/grpcpp.h>
#include <iostream>
#include "helloworld.grpc.pb.h"
using namespace helloworld;
int main() {
auto channel = grpc::CreateChannel("localhost:50051",
grpc::InsecureChannelCredentials());
auto stub = Greeter::NewStub(channel); // 造桩
HelloRequest request;
request.set_name("world"); // 填参数
HelloReply reply;
grpc::ClientContext context; // 一次调用的"档案袋"
grpc::Status status = stub->SayHello(&context, request, &reply);
if (status.ok()) {
std::cout << "Greeter received: " << reply.message() << std::endl;
} else {
std::cout << "RPC failed: " << status.error_message() << std::endl;
}
return 0;
}
# 素材笔记:examples/cpp/helloworld 下直接 make(Makefile 先跑 protoc)
cd examples/cpp/helloworld
make
# 终端 1:启动服务器
./greeter_server
# Server listening on 0.0.0.0:50051
# 终端 2:启动客户端
./greeter_client
# Greeter received: Hello world
grpc::InsecureChannelCredentials() 是明文不加密的通道,只适合学习和内网调试。生产环境要换成 TLS:服务器 grpc::SslServerCredentials(...),客户端 grpc::SslCredentials(...)——那又是另一章的故事了。
10.6 的写法是同步调用——说人话:打电话,拨出去就占线等着,对方不接就一直等。代码简单,但高并发时大量线程都在\"干等\",浪费资源。另一种是异步调用——说人话:发短信,发完该干嘛干嘛,收到回执再处理,一个线程就能同时伺候成千上万个请求。gRPC 的异步 API 核心是 CompletionQueue(完成队列)——说人话:一个\"结果回执信箱\",完成的调用以\"标记(tag)\"投递进来,你不断取件处理。信箱这头一个线程循环取件即可。
服务端异步要继承生成的另一个基类 Greeter::AsyncService,并用 builder.AddCompletionQueue() 建信箱。每个请求由一个 CallData 对象管理,跑一个三态状态机:CREATE(请求槽位就位)→ PROCESS(处理业务、发响应)→ FINISH(响应发完,销毁自己,再挂一个新槽位)。骨架来自官方异步示例(完整可运行版在官方仓库 greeter_async_server.cc):
// 服务端异步骨架(官方 greeter_async_server.cc 压缩版)
class CallData {
public:
CallData(Greeter::AsyncService* service,
grpc::ServerCompletionQueue* cq)
: service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {
Proceed(); // 构造时就"挂号"一个请求槽位
}
void Proceed() {
if (status_ == CREATE) { // 新请求到达 → 去处理
status_ = PROCESS;
service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_, this);
} else if (status_ == PROCESS) { // 处理完 → 发响应
reply_.set_message("Hello " + request_.name());
status_ = FINISH;
responder_.Finish(reply_, grpc::Status::OK, this);
} else { // FINISH:发完 → 销毁,挂新槽位
delete this;
new CallData(service_, cq_);
}
}
// 成员:ctx_、request_、reply_、responder_、status_(三态)
};
void HandleRpcs() { // 信箱循环:取件 → 让对应的 CallData 干活
new CallData(&service_, cq_.get());
void* tag; bool ok;
while (true) {
GPR_ASSERT(cq_->Next(&tag, &ok)); // 阻塞等"回执"
GPR_ASSERT(ok);
static_cast<CallData*>(tag)->Proceed();
}
}
客户端异步的思路一样:不再\"阻塞等结果\",而是把请求挂号到队列上、挂一个回执标记,之后随时来取。新版生成器的方法叫 PrepareAsyncSayHello(老版本叫 AsyncSayHello),返回的 ClientAsyncResponseReader 就是这张\"挂号单\":
// 客户端异步骨架(官方 greeter_async_client.cc 压缩版)
grpc::CompletionQueue cq;
std::unique_ptr<grpc::ClientAsyncResponseReader<HelloReply>> rpc(
stub->PrepareAsyncSayHello(&context, request, &cq)); // 挂号
rpc->StartCall(); // 开跑
rpc->Finish(&reply, &status, (void*)1); // 挂回执:完成时投递 tag=1
void* got_tag; bool ok = false;
cq.Next(&got_tag, &ok); // 阻塞等回执(可放到别处等)
if (ok && got_tag == (void*)1 && status.ok()) {
std::cout << "Greeter received: " << reply.message() << std::endl;
}
到这里,gRPC 的完整链路已经画完:.proto 定契约 → protoc 生成代码 → 服务端覆写 Service 实现业务 → 客户端用 stub 发起调用 → 同步还是异步,取决于你的并发模型。这一章我们走通了\"一问一答\"的一元 RPC,四种 RPC 类型、TLS、负载均衡都留给了未来。第 11 章我们换个风格,认识通用库 POCO——一个把网络、JSON、线程全部包圆的\"瑞士军刀\"。
异步代码最难的其实不是 API,而是\"状态机思维\":每个请求的生命周期被拆成若干个回调(tag 回执),你必须自己记住\"这个请求走到哪一步了\"。建议先把 10.6 的同步版跑熟,再对照官方 greeter_async_server.cc / greeter_async_client.cc 逐行啃异步版——两版对比,状态机脉络一下就清晰了。
用一句话说人话分别解释:RPC、stub、protobuf。再回答:一个 .proto 文件里,syntax、message、service 各定义了什么东西?
回想 10.1 的\"本地替身\"比喻和 10.3 的\"契约\"比喻。三个关键词分别对应:调用方式、客户端视角的代理、数据格式。
RPC:让程序像调用本地函数一样调用远程服务的机制,网络细节被框架藏起来;stub:客户端这边的\"远程函数替身\",你调它,它负责打包请求、发送、接收、解包;protobuf:Google 的二进制序列化格式,把结构化数据压成紧凑字节流。.proto 里 syntax 声明用哪版语法(proto3),message 定义数据结构(字段与编号),service 定义接口(一组 RPC 方法,声明请求与响应类型)。
官方 quickstart 的经典练习:在 greeter.proto 里给 Greeter 服务加一个 SayHelloAgain 方法(请求响应类型与 SayHello 相同),重新生成代码,在服务端实现它,客户端调用它,最终输出 Greeter received: Hello again world。
三步走:① 改 .proto 加 rpc SayHelloAgain (HelloRequest) returns (HelloReply) {};② 重新跑 10.4 的两条 protoc 命令(或直接 make,Makefile 检测到 .proto 变了会重跑);③ 服务端类里加同名 override,客户端里加同名调用。
.proto 里加:rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}。服务端 GreeterServiceImpl 加:Status SayHelloAgain(ServerContext* context, const HelloRequest* request, HelloReply* reply) override { reply->set_message("Hello again " + request->name()); return Status::OK; }。客户端再调一次 SayHelloAgain 并打印。价值:改接口 = 改 .proto + 重新生成 + 两端各补一段代码——这就是\"契约驱动开发\"的威力。
不依赖官方示例,自己写一个 Calculator 服务:.proto 定义 CalcRequest(int32 a = 1; int32 b = 2;)与 CalcReply(int32 result = 1;),服务 Calculator 提供 Add 方法。走完整流程:写 .proto → protoc 生成 → 写服务端(监听 50052 端口)→ 写客户端 → 验证 3 + 4 返回 7。
把 10.3 的 greeter.proto 和 10.6 的两份 .cc 当模板改:包名、类名、方法名全部换成自己的;protoc 输入换成 calculator.proto;端口用 50052 别撞车。
calculator.proto:syntax = "proto3"; package calc; service Calculator { rpc Add (CalcRequest) returns (CalcReply) {} } message CalcRequest { int32 a = 1; int32 b = 2; } message CalcReply { int32 result = 1; }。生成命令:protoc -I . --cpp_out=. calculator.proto 与 protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` calculator.proto。服务端实现 Status Add(ServerContext* ctx, const CalcRequest* req, CalcReply* reply) override { reply->set_result(req->a() + req->b()); return Status::OK; },监听 0.0.0.0:50052。客户端 stub->Add(&context, req, &reply),打印 reply.result() 应为 7。
基于 10.7 的异步客户端骨架,一次发起 3 个 SayHello 请求(分别用 tag 1、2、3 挂号),然后统一从 CompletionQueue 里收 3 个回执,验证每个回执的 tag 和结果一一对应。这模拟了\"一个线程管理大量并发请求\"的真实场景。
3 个请求各建一个 ClientContext 和 HelloRequest(名字不同),各自 PrepareAsyncSayHello + StartCall + Finish,然后循环 3 次 cq.Next(&tag, &ok),用 tag 区分是哪个请求的回应。
核心是每个请求独立挂号:rpc1->Finish(&reply1, &status1, (void*)1)、rpc2->Finish(&reply2, &status2, (void*)2)、rpc3->Finish(&reply3, &status3, (void*)3),然后 for (int i = 0; i < 3; i++) { cq.Next(&tag, &ok); if (ok) 按 tag 打印对应 reply; }——回执到达顺序可能与发起顺序不同,这正是异步的本质:不保证完成次序,用 tag 标识归属。