C# SSL gRPC客户端正常 C++端报UNAVAILABLE Empty update错误
gRPC C++客户端连接返回"Empty update"错误排查与修复
问题背景
运行环境为搭载VS2022 17.1.2、.NET 6的Windows 10 Pro 21H2,在将简易C# gRPC客户端移植为C版本时,两端代码逻辑一致,但C客户端始终无法连接服务端。
服务端采用Let's Encrypt签发的SSL证书(通过LettuceEncrypt工具生成),客户端统一使用默认SslCredentials凭证。
正常运行的C#客户端实现
通道与客户端初始化代码:
// Channel and client creation var channel = new Channel("my.domain.org:12345", new SslCredentials()); var client = new MyGrpc.MyGrpcClient(channel);
接口调用示例:
// Sample call LoginUserReply reply = client.LoginUser(new LoginUserRequest() { Username = username });
异常的C++客户端实现
参考gRPC官方示例改写的C++版本代码,通道与存根初始化:
// Channel and client creation auto channelCreds = SslCredentials(SslCredentialsOptions()); auto channel = CreateChannel("my.domain.org:12345", channelCreds); auto stub = MyGrpc::NewStub(channel);
接口调用示例:
// Sample call ClientContext context; LoginUserRequest request; request.set_username(username); LoginUserReply reply; Status status = m_stub->LoginUser(&context, request, &reply);
运行上述C++代码时,接口调用始终失败,status返回错误:UNAVAILABLE (14) "Empty update"。
已完成排查结果
- 使用
InsecureChannelCredentials()创建通道时,调用返回错误UNAVAILABLE (14) "failed to connect to all addresses" - 测试场景下将服务端切换为明文模式运行时,C++客户端调用可正常返回OK (0)
- 配置
GRPC_TRACE=all与GRPC_VERBOSITY=DEBUG环境变量后,未观测到客户端输出额外调试日志 - 将C#服务端gRPC日志等级设置为Debug后,未观测到正常/非加密客户端连接时会出现的"Reading message"日志条目
故障根因
该错误由Windows平台下C++ gRPC库的默认TLS配置与C# Grpc.Core库逻辑不一致导致,核心差异有两点:
- 根证书信任逻辑差异:C#版
SslCredentials默认会自动读取Windows系统受信任根证书存储,可正常校验Let's Encrypt的证书链;但C++ gRPC传入空SslCredentialsOptions时,仅会加载内置的Mozilla根证书列表,部分旧版本内置根证书未同步最新的Let's Encrypt ISRG根证书,会导致证书校验失败。 - ALPN协商逻辑差异:C#客户端默认会在TLS握手中携带
h2的ALPN标识,告知服务端后续走HTTP/2协议;但部分版本C++ gRPC在空SSL配置下存在ALPN标识未正确携带的问题,搭配LettuceEncrypt配置的ASP.NET Core gRPC服务端时,服务端识别不到HTTP/2协议请求会直接断开TCP连接,客户端收不到合法的HTTP/2响应帧,最终抛出"Empty update"错误。
注:此前配置gRPC调试日志未生效,是因为Windows平台下gRPC的日志环境变量需要在进程启动前加载,在VS中调试时需要直接在项目属性的调试配置页添加环境变量,全局配置后不重启VS不会生效。
修复方案
- 显式加载Windows系统根证书,替换空的SSL凭证配置,确保证书链校验通过:
#include <wincrypt.h> #pragma comment(lib, "crypt32.lib") // 读取Windows系统受信任根证书存储,转换为PEM格式 std::string LoadWindowsSystemRootCerts() { HCERTSTORE cert_store = CertOpenSystemStoreA(0, "ROOT"); if (!cert_store) return {}; std::string pem_certs; PCCERT_CONTEXT cert_ctx = nullptr; while ((cert_ctx = CertEnumCertificatesInStore(cert_store, cert_ctx)) != nullptr) { DWORD pem_length = 0; CryptBinaryToStringA(cert_ctx->pbCertEncoded, cert_ctx->cbCertEncoded, CRYPT_STRING_BASE64HEADER, nullptr, &pem_length); std::vector<char> pem_buffer(pem_length); CryptBinaryToStringA(cert_ctx->pbCertEncoded, cert_ctx->cbCertEncoded, CRYPT_STRING_BASE64HEADER, pem_buffer.data(), &pem_length); pem_certs.append(pem_buffer.data(), pem_length); } CertCloseStore(cert_store, 0); return pem_certs; } // 初始化SSL凭证时传入加载到的根证书 SslCredentialsOptions ssl_opts; ssl_opts.pem_root_certs = LoadWindowsSystemRootCerts(); auto channel_creds = SslCredentials(ssl_opts); - 通过通道参数显式指定HTTP/2相关配置,同时配置SSL目标域名和证书域名匹配,避免ALPN协商失败:
grpc::ChannelArguments channel_args; // 禁用无数据时的ping限制,适配服务端默认配置 channel_args.SetInt(GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA, 0); // 显式指定SSL校验的目标域名,和证书签发域名保持一致 channel_args.SetSslTargetNameOverride("my.domain.org"); // 使用自定义参数创建通道 auto channel = CreateCustomChannel("my.domain.org:12345", channel_creds, channel_args); - 若当前使用的gRPC C++版本低于v1.46,先升级到v1.46及以上稳定版本,旧版本在Windows平台存在已知的TLS握手兼容bug。
内容的提问来源于stack exchange,提问作者Ray
相关产品推荐
相关产品推荐

