gRPC C++客户端grpc::Status.error_message无法返回具体连接失败原因
问题
实现同时支持TCP和TLS的gRPC C++客户端,对接Go编写的服务器模拟器时,以下四种连接场景均返回通用错误failed to connect to all addresses,无法获取具体失败原因:
- A. 服务器未启动时尝试连接
- B. 客户端用TLS、服务器用TCP,双方正常运行
- C. 客户端用TCP、服务器用TLS,双方正常运行
- D. 客户端与服务器均用TLS,但证书不匹配,双方正常运行
项目需要向客户明确具体失败原因,仅通用错误信息无法满足需求。虽然ssl_transport_security有相关日志,但无法被程序直接调用,期望通过grpc::Status或其他方式获取可用于告知客户的具体错误信息。
现有代码片段:
//Channel creation bool isTls = true; grpc::SslCredentialsOptions ssl_opts; ssl_opts.pem_root_certs = ReadFile("ca.crt"); ssl_opts.pem_cert_chain = ReadFile("client.crt"); ssl_opts.pem_private_key = ReadFile("client.key"); if (ssl_opts.pem_root_certs.empty() || ssl_opts.pem_cert_chain.empty() || ssl_opts.pem_private_key.empty()) { isTls = false; } auto channelArgs = grpc::ChannelArguments(); channelArgs.SetInt(GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS, 2000); channelArgs.SetInt(GRPC_ARG_MIN_RECONNECT_BACKOFF_MS, 2000); channelArgs.SetInt(GRPC_ARG_MAX_RECONNECT_BACKOFF_MS, 2000); if (isTls) { m_channel = grpc::CreateCustomChannel(url, grpc::SslCredentials(ssl_opts), channelArgs); } else { m_channel = grpc::CreateCustomChannel(url, grpc::InsecureChannelCredentials(), channelArgs); } //on_response callback implementation for rpc "rpc Send(stream gnmi.SubscribeResponse) returns (google.protobuf.Empty);" void on_response(const grpc::Status &status, const google::protobuf::Empty *response) { cout << "on_response code: " << status.error_code() << ", message: " << status.error_message() << "\n"; }
当前输出:
on_response code: 14 , message: failed to connect to all addresses
预期输出:
返回精准错误信息,如「连接被拒绝」「协议不匹配(客户端TLS/服务器TCP)」「证书验证失败」等中文描述。
解决方案
1. 解析error_details获取结构化错误信息
gRPC的grpc::Status提供error_details()方法,可提取包含具体错误原因的结构化数据。需要引入Google RPC的标准错误定义proto文件并编译,从中解析TLS、连接类错误细节。
代码片段:
首先确保已编译google/rpc/status.proto和google/rpc/error_details.proto(可从gRPC依赖的protobuf库中获取),然后在回调中添加解析逻辑:
#include <google/rpc/status.pb.h> #include <google/rpc/error_details.pb.h> void on_response(const grpc::Status &status, const google::protobuf::Empty *response) { cout << "on_response code: " << status.error_code() << ", message: " << status.error_message() << "\n"; // 解析error_details到Google标准Status结构 google::rpc::Status rpc_status; if (status.error_details().UnpackTo(&rpc_status)) { for (const auto& detail : rpc_status.details()) { // 提取调试信息(部分TLS错误会包含在这里) google::rpc::DebugInfo debug_info; if (detail.UnpackTo(&debug_info)) { cout << "具体错误原因: " << debug_info.detail() << "\n"; } // 提取参数/协议类错误 google::rpc::BadRequest bad_request; if (detail.UnpackTo(&bad_request)) { for (const auto& violation : bad_request.field_violations()) { cout << "参数/协议错误: " << violation.description() << "\n"; } } } } }
2. 自定义客户端拦截器捕获底层传输错误
通过gRPC客户端拦截器,可以在连接建立、请求发送的关键节点捕获更底层的错误(如TCP连接被拒绝、TLS握手失败),并将这些信息传递到业务回调中。
代码片段:
首先定义拦截器和拦截器工厂:
#include <grpcpp/impl/codegen/client_interceptor.h> // 自定义上下文,用于传递错误信息 struct CustomCallContext { std::string detailed_error; }; class ErrorCaptureInterceptor : public grpc::experimental::Interceptor { public: void Intercept(grpc::experimental::InterceptorBatchMethods* methods) override { // 捕获初始元数据发送前的状态,此时可获取连接错误 if (methods->QueryInterceptionHookPoint(grpc::experimental::InterceptionHookPoints::PRE_SEND_INITIAL_METADATA)) { auto call = methods->GetCall(); grpc::Status peer_status; if (call->GetPeerStatus(&peer_status) && !peer_status.ok()) { // 将错误信息存入自定义上下文 auto* ctx = static_cast<CustomCallContext*>(call->GetUserData()); if (ctx) { // 根据错误码和消息特征转换为用户友好描述 if (peer_status.error_code() == grpc::StatusCode::UNAVAILABLE) { const std::string& msg = peer_status.error_message(); if (msg.find("SSL") != std::string::npos) { if (msg.find("certificate") != std::string::npos) { ctx->detailed_error = "证书验证失败:服务器证书不匹配或未受信任"; } else { ctx->detailed_error = "TLS握手失败:协议不匹配或SSL配置错误"; } } else if (msg.find("refused") != std::string::npos) { ctx->detailed_error = "连接被拒绝:目标服务器未启动或端口未开放"; } else { ctx->detailed_error = "连接不可用:" + msg; } } else { ctx->detailed_error = peer_status.error_message(); } } } } methods->Proceed(); } }; class ErrorCaptureInterceptorFactory : public grpc::experimental::ClientInterceptorFactoryInterface { public: grpc::experimental::Interceptor* CreateClientInterceptor(grpc::experimental::ClientRpcInfo*) override { return new ErrorCaptureInterceptor(); } };
创建通道时添加拦截器:
// 组装拦截器工厂列表 grpc::experimental::ClientInterceptorFactoryList interceptor_factories; interceptor_factories.push_back(std::make_unique<ErrorCaptureInterceptorFactory>()); // 创建带拦截器的通道 if (isTls) { m_channel = grpc::CreateCustomChannel(url, grpc::SslCredentials(ssl_opts), channelArgs, interceptor_factories); } else { m_channel = grpc::CreateCustomChannel(url, grpc::InsecureChannelCredentials(), channelArgs, interceptor_factories); }
在调用RPC时绑定自定义上下文,并在回调中获取错误:
// 发起RPC时绑定上下文 CustomCallContext ctx; auto stub = gnmi::gNMI::NewStub(m_channel); auto rpc = stub->Send(&context, &response, [&ctx](const grpc::Status& status, const google::protobuf::Empty* resp) { if (!ctx.detailed_error.empty()) { cout << "具体失败原因: " << ctx.detailed_error << "\n"; } else { cout << "on_response code: " << status.error_code() << ", message: " << status.error_message() << "\n"; } }); rpc->SetUserData(&ctx);
3. 调整gRPC日志级别辅助排查(可选)
如果需要更底层的调试信息,可以调整gRPC日志级别,将SSL相关日志输出到文件,程序可读取日志文件提取关键错误:
// 初始化时设置日志输出和级别 grpc::logging::SetLogger(grpc::logging::FileLogger("grpc_client_errors.log")); grpc::logging::SetLogLevel(GRPC_LOG_SEVERITY_DEBUG);
内容的提问来源于stack exchange,提问作者Martin Zamkotsian

