在分布式系统开发中,gRPC作为高性能、跨语言的RPC框架,其同步通信模式是最基础也最常用的交互方式。不同于异步模式需要处理复杂的回调逻辑,同步通信以直观的阻塞调用方式,让开发者能够以类似本地函数调用的思维处理远程服务交互。
本次我们聚焦gRPC四种核心通信模式的同步实现,通过完整的C++示例代码,深入剖析:
技术栈提示:本文基于gRPC 1.46+版本和Protocol Buffers 3语法,需要读者已掌握C++11基础知识和proto文件编写能力。
协议定义是gRPC开发的起点,我们的example.proto文件虽然简洁,但包含了关键设计考量:
protobuf复制syntax = "proto3";
package example;
service ExampleService {
rpc UnaryCall (Request) returns (Response);
rpc ServerStream (Request) returns (stream Response);
rpc ClientStream (stream Request) returns (Response);
rpc BidiStream (stream Request) returns (stream Response);
}
message Request { string data = 1; }
message Response { string data = 1; }
example)避免命名冲突data = 1),这是Protocol Buffers的二进制编码依据stream关键字区分流式接口,其位置决定流方向:
returns (stream Response):服务端流(stream Request) returns:客户端流stream使用protoc编译器生成代码时,推荐以下命令参数:
bash复制protoc -I=. --cpp_out=./build --grpc_out=./build \
--plugin=protoc-gen-grpc=`which grpc_cpp_plugin` \
example.proto
这会生成:
example.pb.h/cc:消息序列化代码example.grpc.pb.h/cc:gRPC服务桩代码文件目录规范:建议将生成的代码统一放在
build目录,与手写代码隔离。大型项目可使用CMake的add_custom_command集成编译流程。
服务端核心是实现ExampleService::Service的派生类,需注意:
cpp复制class ExampleServiceImpl final : public ExampleService::Service {
Status UnaryCall(ServerContext* context, const Request* req,
Response* res) override {
// 实现细节...
}
// 其他方法...
};
OK表示成功,其他状态码表示错误cpp复制Status UnaryCall(ServerContext* context, const Request* req, Response* res) {
res->set_data("Server: " + req->data()); // 简单回声处理
return Status::OK; // 必须返回Status对象
}
cpp复制Status ServerStream(ServerContext* context, const Request* req,
ServerWriter<Response>* writer) {
for (int i = 0; i < 3; ++i) {
Response res;
res.set_data("Stream " + std::to_string(i));
if (!writer->Write(res)) { // 检查写入状态
// 客户端可能提前关闭连接
break;
}
std::this_thread::sleep_for(1s); // 模拟处理延迟
}
return Status::OK;
}
流控制技巧:服务端流适合推送实时数据(如股票行情),但要注意:
- 避免过快发送导致客户端缓冲区溢出
- 通过
Write()返回值检测连接状态
cpp复制Status ClientStream(ServerContext* context, ServerReader<Request>* reader,
Response* res) {
Request req;
std::vector<std::string> received;
while (reader->Read(&req)) {
received.push_back(req.data());
}
res->set_data("Received " + std::to_string(received.size()) + " items");
return Status::OK;
}
cpp复制Status BidiStream(ServerContext* context,
ServerReaderWriter<Response, Request>* stream) {
Request req;
while (stream->Read(&req)) {
Response res;
res.set_data("Echo: " + req.data());
if (!stream->Write(res)) {
break; // 写入失败处理
}
}
return Status::OK;
}
cpp复制void RunServer() {
std::string addr("0.0.0.0:50051");
ExampleServiceImpl service;
ServerBuilder builder;
// 配置监听端口(支持多个端口)
builder.AddListeningPort(addr, grpc::InsecureServerCredentials());
// 注册服务(可注册多个服务)
builder.RegisterService(&service);
// 高级配置示例:
builder.SetMaxReceiveMessageSize(100 * 1024 * 1024); // 100MB
builder.SetMaxSendMessageSize(100 * 1024 * 1024);
std::unique_ptr<Server> server(builder.BuildAndStart());
server->Wait(); // 阻塞直到Shutdown()调用
}
生产环境建议:使用SSL/TLS证书替换
InsecureServerCredentials(),配置线程池大小(SetSyncServerOption)和连接超时等参数。
cpp复制class ExampleClient {
public:
ExampleClient(std::shared_ptr<Channel> channel)
: stub_(ExampleService::NewStub(channel)) {}
private:
std::unique_ptr<ExampleService::Stub> stub_;
};
cpp复制auto channel = grpc::CreateChannel("localhost:50051",
grpc::InsecureChannelCredentials());
ExampleClient client1(channel), client2(channel); // 共享连接
cpp复制void CallUnary(const std::string& msg) {
Request req;
req.set_data(msg);
Response res;
ClientContext ctx;
// 设置超时(单位:微秒)
ctx.set_deadline(std::chrono::system_clock::now() + 5s);
Status status = stub_->UnaryCall(&ctx, req, &res);
if (!status.ok()) {
std::cerr << "RPC failed: " << status.error_message() << std::endl;
return;
}
std::cout << res.data() << std::endl;
}
cpp复制void CallServerStream(const std::string& msg) {
Request req;
req.set_data(msg);
ClientContext ctx;
auto reader = stub_->ServerStream(&ctx, req);
Response res;
while (reader->Read(&res)) {
std::cout << "Received: " << res.data() << std::endl;
}
Status status = reader->Finish();
if (!status.ok()) { /* 错误处理 */ }
}
cpp复制void CallClientStream(const std::vector<std::string>& msgs) {
ClientContext ctx;
Response res;
auto writer = stub_->ClientStream(&ctx, &res);
for (const auto& msg : msgs) {
Request req;
req.set_data(msg);
if (!writer->Write(req)) {
break; // 写入失败
}
}
writer->WritesDone(); // 必须调用以结束流
Status status = writer->Finish();
if (status.ok()) {
std::cout << "Server response: " << res.data() << std::endl;
}
}
cpp复制void CallBidiStream(const std::vector<std::string>& msgs) {
ClientContext ctx;
auto stream = stub_->BidiStream(&ctx);
// 写线程
std::thread writer([&] {
for (const auto& msg : msgs) {
Request req;
req.set_data(msg);
if (!stream->Write(req)) {
break;
}
}
stream->WritesDone();
});
// 读线程
Response res;
while (stream->Read(&res)) {
std::cout << "Echo: " << res.data() << std::endl;
}
writer.join();
Status status = stream->Finish();
}
性能提示:双向流建议使用多线程处理读写,避免阻塞导致的吞吐量下降。示例中使用
std::thread简单演示,生产环境建议使用线程池。
gRPC同步API的阻塞特性体现在:
stub_->Method()调用阻塞直到收到响应或超时reader->Read()/writer->Write()阻塞直到IO完成context->IsCancelled()cpp复制builder.SetSyncServerOption(
ServerBuilder::SyncServerOption::MAX_POLLERS, 20);
plaintext复制Client Server
|-- Request -->|
|<-- Response--|
plaintext复制Client Server
|-- Request -->|
|<-- Response--|
|<-- Response--|
|<-- ... ---|
plaintext复制Client Server
|-- Request -->|
|-- Request -->|
|-- ... -->|
|<-- Response--|
plaintext复制Client Server
|-- Request -->|
|<-- Response--|
|-- Request -->|
|<-- Response--|
|-- ... -->|
|<-- ... --|
gRPC使用Status对象传递错误信息,常见状态码:
| 状态码 | 含义 | 典型场景 |
|---|---|---|
| OK | 成功 | 正常完成 |
| CANCELLED | 操作取消 | 客户端主动取消 |
| DEADLINE_EXCEEDED | 超时 | 未在截止时间前完成 |
| RESOURCE_EXHAUSTED | 资源耗尽 | 并发限制或内存不足 |
| UNIMPLEMENTED | 未实现 | 服务端未覆盖的方法 |
cpp复制Status status = stub_->UnaryCall(&ctx, req, &res);
if (!status.ok()) {
if (status.error_code() == StatusCode::DEADLINE_EXCEEDED) {
std::cerr << "Timeout occurred" << std::endl;
} else {
std::cerr << "Error: " << status.error_details() << std::endl;
}
}
cpp复制grpc::ChannelArguments args;
// 设置连接超时(毫秒)
args.SetInt(GRPC_ARG_CLIENT_CONNECTION_TIMEOUT_MS, 5000);
// 启用Keepalive
args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 60000);
args.SetInt(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 20000);
auto channel = grpc::CreateCustomChannel(
"localhost:50051",
grpc::InsecureChannelCredentials(),
args);
cpp复制// 服务端启用压缩
builder.SetDefaultCompressionAlgorithm(GRPC_COMPRESS_GZIP);
// 客户端请求压缩
ctx.set_compression_algorithm(GRPC_COMPRESS_GZIP);
虽然本文聚焦同步API,但在高并发场景可考虑:
CompletionQueue实现半同步模式症状:Status{code=UNAVAILABLE, details="..."}
排查步骤:
netstat -tulnp | grep 50051telnet localhost 50051症状:高延迟或低吞吐
优化方向:
GRPC_ARG_TCP_READ_CHUNK_SIZE)检测方法:
ClientContext和流对象Finish()调用都检查返回值经过多个项目的实战检验,总结以下经验:
超时设置:所有RPC必须设置合理的deadline
cpp复制ctx.set_deadline(std::chrono::system_clock::now() + 100ms);
负载均衡:多实例部署时使用gRPC内置LB:
cpp复制grpc::ChannelArguments args;
args.SetLoadBalancingPolicyName("round_robin");
健康检查:实现Health.Check服务端点
监控指标:集成Prometheus客户端收集:
日志规范:为每个RPC记录:
cpp复制ctx.AddMetadata("request-id", GenerateUUID());
在最近的一个物联网平台项目中,我们采用同步服务端处理设备指令,实测单机可稳定支撑8000+ QPS。关键配置是:
同步API虽然简单,但通过精细调优仍能满足大多数性能场景。对于超高性能需求,可考虑基于本文代码逐步迁移到异步模式。