如何不发送消息校验gRPC双向流端点有效性(适配HTTPS)
问题背景
需要在不发送测试消息的前提下校验gRPC双向流端点的有效性,目前已实现非加密连接场景的校验逻辑,但将该逻辑复用到HTTPS加密连接时会抛出异常,移除校验逻辑后HTTPS连接可正常通信。
现有客户端实现代码:
var saCerts = CertificateValidation.CreateTrustedRootCollection( certificateAthorities ); var options = new GrpcChannelOptions(); if( url.StartsWith( "https" ) ) { var handler = new HttpClientHandler(); handler.ClientCertificates.Add( new X509Certificate2( /*certificates*/ ) ); handler.ServerCertificateCustomValidationCallback = CertificateValidation.CreateCustomRootValidator( saCerts ); options.HttpHandler = handler; } channel = GrpcChannel.ForAddress( url, options ); //This is how I found to test an unsecure connection channel.ConnectAsync(); var task = channel.WaitForStateChangedAsync( ConnectivityState.Connecting ); task.Wait(); if( channel.State == ConnectivityState.TransientFailure ) throw new Exception( $"Could not connect to URL: {url}." ); //End connection test var client = new MyMessage.MyMessageClient( channel ); call= client.Stream( null, null, cancellationSource.Token );
接入HTTPS端点时抛出InvalidOperationException,异常信息如下:
Channel is configured with an HTTP transport doesn't support client-side load balancing or connectivity state tracking. The underlying HTTP transport must be a SocketsHttpHandler with no SocketsHttpHandler.ConnectCallback configured. The HTTP transport must be configured on the channel using GrpcChannelOptions.HttpHandler.
待解决疑问:
- 目前使用的非加密连接端点校验方式是否合理?如果不合理,最优的校验方案是什么?
- 如何让该校验逻辑适配HTTPS场景?如果必须使用SocketsHttpHandler作为HTTP处理器,要如何完成现有逻辑中的客户端证书、根证书校验等相关配置?
问题解答
1. 现有非加密校验逻辑的合理性说明
现有校验逻辑不合理,存在两个明显缺陷:
- 调用
channel.ConnectAsync()后未等待任务执行完成,连接发起时序不可控,可能出现状态判断时连接流程还未正式启动的问题 - 仅监听一次从
Connecting状态的变更、只判断TransientFailure状态,覆盖场景不全:连接被拒绝、证书无效、握手失败等场景可能流转到Shutdown等其他异常状态,会出现漏判。
无消息场景下的最优校验方案:直接依赖gRPC通道原生的ConnectAsync()方法,该方法只会完成传输层连接建立、TLS握手(HTTPS场景)、HTTP/2协议协商流程,不会发送任何业务gRPC请求,完全符合不发测试消息的要求。只需要给该方法配置合理超时,等待方法执行完成即可,不需要自己手动监听通道状态变更:如果连接、握手、协议协商全流程成功,方法会正常返回;如果任意环节失败,方法会抛出对应异常,直接捕获判断即可。
2. HTTPS场景适配方案
抛出异常的核心原因是:gRPC .NET客户端的连接状态跟踪、负载均衡能力依赖SocketsHttpHandler的原生实现,你当前使用的HttpClientHandler在多数运行时版本下是对SocketsHttpHandler的高层包装,不满足gRPC通道的底层能力要求,替换为直接实例化SocketsHttpHandler即可。你之前移除校验逻辑后HTTPS能正常通信,是因为没有调用ConnectAsync、WaitForStateChangedAsync这类依赖通道状态跟踪能力的API,普通RPC调用不需要用到这些底层能力,所以用HttpClientHandler也能正常工作。
SocketsHttpHandler提供了和HttpClientHandler完全一致的证书配置能力,不需要改写原有证书校验逻辑,对应配置映射如下:
- 客户端证书配置:直接给
SocketsHttpHandler.ClientCertificates集合添加客户端证书即可,用法和原逻辑完全一致 - 自定义根证书校验:直接给
SocketsHttpHandler.ServerCertificateCustomValidationCallback属性赋值自定义校验委托即可,你原有CreateCustomRootValidator生成的委托可以直接复用,不需要修改。
修正后的完整实现代码如下:
var saCerts = CertificateValidation.CreateTrustedRootCollection(certificateAthorities); var options = new GrpcChannelOptions(); if (url.StartsWith("https")) { // 替换为SocketsHttpHandler满足gRPC通道状态跟踪要求 var handler = new SocketsHttpHandler(); // 配置客户端证书,和原逻辑一致 handler.ClientCertificates.Add(new X509Certificate2(/*客户端证书参数*/)); // 配置自定义服务端根证书校验,和原逻辑一致 handler.ServerCertificateCustomValidationCallback = CertificateValidation.CreateCustomRootValidator(saCerts); options.HttpHandler = handler; } channel = GrpcChannel.ForAddress(url, options); // 正确的无消息连接校验:等待ConnectAsync完成,配置10秒超时(可根据实际场景调整) try { var connectToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; // Wait适配同步调用场景,异步上下文可直接await ConnectAsync channel.ConnectAsync(connectToken).Wait(connectToken); } catch (AggregateException ex) when (ex.InnerException is RpcException or HttpRequestException or OperationCanceledException) { throw new Exception($"Could not connect to URL: {url}.", ex.InnerException); } // 校验通过后再初始化客户端、发起流调用 var client = new MyMessage.MyMessageClient(channel); call = client.Stream(null, null, cancellationSource.Token);
该实现完全不需要发送任何业务消息,同时兼容HTTP非加密和HTTPS加密场景,不会再抛出传输层不支持的异常。
内容的提问来源于stack exchange,提问作者NullMan

