ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

.NET 全双工 RPC 实战:基于 gRPC 双向流实现双向通信

.NET 全双工 RPC 实战:基于 gRPC 双向流实现双向通信 传统 RPC 我一直觉得是个“别扭”的东西。客户端发个请求、服务端回个响应像打电话一样一问一答大部分场景下够用。但一旦涉及“服务端主动找客户端说话”或者“双方要持续不停地互抛数据”这套模型就卡住了。轮询资源浪费严重消息队列引入额外组件WebSocket又得自己设计协议和消息语义。这也是为什么我最近折腾 .NET 全双工 RPC 时感觉整个通信方式“换了条腿走路”。这篇就手把手拆一下全双工 RPC 到底双在哪里、跟传统 RPC 什么关系、在 .NET 里怎么落地、踩过哪些坑。适合正在选型实时通信方案、被服务端推送困扰或者想把客户端和服务端从“主从关系”改成“对等关系”的 .NET 开发者不管你是刚接触 RPC 还是已经在用 gRPC、SignalR这篇应该都有点参考价值。1. 内容整体设计与思路拆解1.1 传统 RPC 的“单向”到底单在哪先说清楚传统 RPC 的样子。一个标准的请求-响应调用链路通常是这样客户端构造请求上下文把参数序列化通过网络传到服务端服务端执行完业务逻辑把结果序列化回传客户端收到响应这次调用才算结束。整个生命周期内客户端是发起方服务端是被动执行方角色是固定的通信方向默认是客户端→服务端→客户端。这套模型本身没有任何问题业界用了这么多年稳定可靠。但它有个隐含假设所有的业务交互都可以被拆成“一问一答”。在微服务架构里服务和服务之间确实是这种模式居多A 调 B 查个用户信息B 返回一个对象完事。可一旦你遇到“服务端需要持续向客户端推送进度”“客户端需要随时向服务端上报状态同时还要接受服务端下发的指令”这类需求传统 RPC 就开始别扭了。我做过的项目里最典型的是任务调度系统。控制端要下发任务给执行端执行端跑任务的过程中要持续回报进度日志和指标控制端可能中途还要根据实时数据调整任务参数。用传统 RPC 写进度上报得不停发请求、轮询任务状态调整参数得设计一套额外指令接口两个方向的数据又被拆成两套 API维护成本直接翻倍。还有一些场景更麻烦比如服务端有事件要广播给一批客户端传统 RPC 根本无法主动“找到”客户端只能靠客户端定期拉取实时性就打了折扣。1.2 全双工 RPC 的“双工”是指什么全双工这个词其实是从通信领域借来的。所谓“全双工”就是通信双方可以在同一个连接上同时收发数据不需要排队等对方说完。放到 RPC 的语境里意思是客户端可以调用服务端的方法服务端也可以调用客户端的方法两边的请求和响应可以在同一条链路上交错传输没有严格的“谁先谁后”限制。这个转变听起来不大但影响很深远。首先角色从“主从”变成了“对等”服务端不再是只能等着被调用它可以主动向客户端发起调用比如推送配置变更、下发命令、同步状态。其次单条连接上可以同时跑多个逻辑调用各个调用之间的数据包交错发送不用串行等待延迟显著降低。最重要的是这种设计让“长时间持活的会话型通信”变得自然——连接一旦建立两边就一直“泡”在一条全双工管道里随时可以说、随时可以听。有的老铁可能觉得这不就是 WebSocket 吗确实有不少相似点但 RPC 和 WebSocket 的层次不一样。WebSocket 只是一个传输通道它不管你传的内容是干嘛的你拿到消息还得自己解析语义、定义一个类似“方法调用”的包装格式。而全双工 RPC 是在传输之上构建了一套完整的调用语义方法名怎么映射、参数怎么序列化、错误怎么返回、超时怎么处理、消息怎么关联到一个具体的调用上下文这些都是 RPC 框架帮你处理好的。1.3 为什么选 .NET 环境来做这件事聊到 .NET 生态其实可选的全双工方案并不少。gRPC 原生支持双向流式调用bidirectional streaming.NET 对 gRPC 的支持在 3.0 之后就一直很稳SignalR 也能做全双工消息推送但它更偏向“消息中枢”而非“方法调用”还有 StreamJsonRPC 这类专门为 .NET 定制的库在某些场景也很能打。我这次选择在 .NET 里用 gRPC 双向流来实现全双工 RPC核心原因有三点。一是生态成熟.NET 的Grpc.Net.Client和Grpc.AspNetCore.Server经过多个大版本迭代性能和稳定性都有保证实在不想自己维护底层连接。二是协议跨语言proto 定义一次C#、Go、Java 都能生成客户端以后要是混编架构不需要推倒重来。三是 .NET 的异步模型和全双工流式通信高度契合IAsyncEnumerable配Channel可以很优雅地处理持续不断的数据流写起来代码量比事件回调那套少得多也更容易排查问题。当然如果你只是想快速搞定“服务端推送”一个小功能SignalR 是更轻的选择但如果你需要的是结构化、强类型、可跨语言的方法调用体系gRPC 双向流这种“真全双工 RPC”更合适。下面我展开讲实现方案时也会把两种选型的边界梳理清楚。2. 核心细节解析与实操要点2.1 全双工 RPC 背后的传输与协议基础先捋一下底层机制。gRPC 默认跑在 HTTP/2 之上HTTP/2 有个关键特性叫“多路复用”单条 TCP 连接可以同时承载多个独立的双向数据流。全双工 RPC 就是靠这个特性撑起来的一条连接上可以同时存在“客户端请求流”“服务端响应流”还能在流上并行发多个调用每个调用通过不同的消息帧区分。我知道有些老铁听到 HTTP/2 会下意识觉得“太重了”但实际上 .NET 里配置起来非常简单HttpClient默认支持 HTTP/2gRPC 客户端会自动协商。在局域网内部部署时延迟和吞吐都相当理想——我实测过压测几千个并发调用单连接维持通讯完全没问题。这里面有一个容易踩的概念坑全双工不代表“无序”。HTTP/2 的帧在传输层是乱序可能要重组但到应用层每个流内的消息是有顺序保障的。所以你在设计业务协议时必须明确“消息边界”是什么。gRPC 用的是 Length-Prefixed 消息格式每条消息前有固定长度的头部指明数据长度这样接收方就知道从哪到哪是一条完整消息不会发生粘包拆包的问题。这也是我提醒各位别自己拿 byte[] 裸写全双工协议的原因序列化和分包处理把脑子烧掉都不一定搞对直接用成熟框架省心得多。2.2 双向流式调用 vs 服务端推送的差异很多人把“服务端推送”和“全双工 RPC”混在一起这其实是两个层次的东西。服务端推送解决的是“数据从服务端流向客户端”的问题本质上还是单向的全双工 RPC 解决的是“两条方向都有数据流且相互独立”的问题。用 gRPC 举个例子。服务端推送可以用 Server Streaming 来实现声明一个返回流式响应的方法客户端调用一次服务端持续回推数据比如任务进度、日志、通知。但这时客户端如果想在接收过程中反过来说点什么比如“进度够了停止推送”或者“把推送频率调低一点”传统方式只能另起一个方法去通知服务端两条连接、两套状态非常别扭。双向流式调用Bidirectional Streaming则不同。客户端建立调用后两边各有一个流请求流从客户端流向服务端响应流从服务端流向客户端两边可以独立读写几乎同时进行。上面的停止推送需求可以直接在同一个调用里通过请求流发一个控制消息服务端收到后立即停止当前响应流。这就把“推”和“拉”“控制”统一到一个连接上了状态管理也集中了。2.3 消息并发与调用上下文的关联全双工链路一开第一个迎面而来的复杂问题是同一条流上跑了很多逻辑调用怎么把一个响应对应到正确的请求上传统 RPC 的做法是靠阻塞等待——发一个请求线程挂起等同一个响应回来。全双工 RPC 显然不能这么干否则一个调用没返回整条链路都被堵住了。正确做法是给每次调用分配一个唯一的调用 ID通常放在消息的元数据或头字段里把这个 ID 和本地的TaskCompletionSource关联起来。发送请求时带上 ID收到响应时按 ID 找到对应的等待任务让它完成。这样设计之后消息处理程序就变得非常纯粹专管接收和分发不关心业务逻辑。收到的消息先拆出 ID再判断这是请求还是响应按类型路由到对应的处理队列。我在实际项目里还加了一个轻量级“消息上下文”对象里面塞了调用 ID、对端标识、超时时间、取消令牌几件套让上层业务方法不用自己维护这些关注点。2.4 超时、保活与重连的关键参数选择全双工连接跑久了最常遇到的就是“半开连接”——链路对端已经物理断开但本地还没发现傻傻地继续往一个黑洞里写数据。所以全双工 RPC 的工程化先不说业务怎么设计连接的健康管理一定要做好。gRPC 里直接相关的有这几个参数我列一下自己的使用心得参数默认值我的建议理由KeepAliveTime无穷大不启用30 秒30 秒发一次 Ping既不会太频繁增加无效包也能在 1 分钟内感知多数链路断连KeepAliveTimeout20 秒10 秒如果 10 秒内没收到 Pong基本可以判定链路出了问题MaxConcurrentStreams100服务端默认较高按实际并发峰值设置全双工长连接上同时跑的调用多了这个参数就是“水龙头”的大小MaxReceiveMessageSize4 MB视单条消息大小调整传大对象时记得调大否则会直接抛异常而且这个错误很隐蔽HttpClient.Timeout100 秒设为Timeout.InfiniteTimeSpan因为这个是 HTTP 层超时不是 RPC 调用层超时用默认值会导致长连接被 HttpClient 扼杀掉这里多提一句超时不是设得越大越好。全双工 RPC 讲究“会话级超时”和“调用级超时”分离。连接是常驻的不能有总的截止时间但每一次具体的 RPC 调用一定要设置 deadline避免某个方法卡死把整条链路的处理线程池也拖死。gRPC 的CallOptions.WithDeadline()就是干这个的我一般设 10 秒到 30 秒根据调用类型区分。重连策略也很重要。我的经验是三步走先立即重试一次失败则退避 1 秒、2 秒、4 秒……指数增长到上限 30 秒连续失败达到 5 次后发一条告警同时保留连接对象等下一次心跳探测成功再做恢复。重连时尽量复用异步上下文别轻易新建整个客户端连接复用比建新连接省很多开销状态恢复也更快。3. 实操过程与核心环节实现3.1 定义 proto 契约并生成 .NET 服务端代码说实话第一次写全双工 RPC 的 proto 时我也绕了一点弯——双向流式方法的声明方式跟普通的 unary 方法长得不太一样不仔细看注释容易搞混。下面是一个非常简化的例子场景是远程命令通道客户端可以发命令给服务端服务端也能主动向客户端下发指令和返回执行结果。syntax proto3; package duplex; // 客户端上报 / 服务端返回的统一消息结构 message Frame { string call_id 1; string method 2; // 方法名比如 heartbeat、report_status bytes payload 3; // 对应方法的参数JSON 或二进制序列化 string error 4; // 错误信息为空表示成功 int64 timestamp 5; // 发送时间戳用于调试和延迟统计 } service DuplexRpcService { // 双向流式方法建立后持续双向通信 rpc OpenChannel(stream Frame) returns (stream Frame); }C# 服务端生成代码里会有一个OpenChannel的抽象方法入参是IAsyncStreamReaderFrame出参是IServerStreamWriterFrame。看着有点抽象实际用起来很直白。服务端方法可以想象成一个独立的“会话循环”MoveNextAsync()用来读取客户端发来的消息WriteAsync()用来往下游推数据。两边互相独立各自跑各自的循环这就是双工的基础。3.2 服务端实现会话管理与消息分发服务端的核心逻辑是管理好这个长期存在的流对象。我的做法是每个连接进来先注册到一个公共会话管理器再把“读”和“写”拆成两个独立后台任务来跑。读任务的逻辑就是一个循环持续从客户端流里读Frame按method字段做分发。分发可以是调本地方法、转给其他微服务、或者直接触发一个事件——这一层你可以把它理解成“服务端收到了一个 RPC 请求”。代码如下public override async Task OpenChannel( IAsyncStreamReaderFrame requestStream, IServerStreamWriterFrame responseStream, ServerCallContext context) { var session new Session(responseStream); // 负责写 SessionManager.Register(session); // 背景写任务把服务端主动下发的消息写入流 var writeTask session.RunAsync(context.CancellationToken); try { await foreach (var frame in requestStream.ReadAllAsync()) { var response await _dispatcher.DispatchAsync(frame, session); if (response ! null) { await session.WriteAsync(response); } } } finally { SessionManager.Unregister(session); session.Complete(); } }写任务那边单独用了一个ChannelFrame作为输出队列。业务代码想主动下发指令时不用直接碰IServerStreamWriter只需要往这个队列里塞一个 Frame由写任务统一循环取出并写入流。这么做的好处是避免了多个业务线程同时调WriteAsync导致写入交错和帧错乱。这是多线程环境里最容易出的 bug我刚开始没注意结果线上偶发出现“消息串味”排查过程极其痛苦。3.3 客户端实现双工调用与事件回调客户端这边建立连接之后同样要维护“读”和“写”两个方向的任务。但客户端多了一层需求它不仅要调用服务端还要响应服务端主动发来的请求。因此读任务不光要处理响应还要判断消息里有没有request语义。我用了一块简单的内存路由器来处理。收到 Frame 后如果method开头带server_command.前缀就当成服务端发来的请求路由到本地注册的命令处理器否则就当成一次调用的响应通过call_id找到对应的TaskCompletionSource并把结果塞进去。整体伪代码如下public async TaskFrame CallAsync(string method, object payload) { var callId Guid.NewGuid().ToString(N); var tcs new TaskCompletionSourceFrame(TaskCreationOptions.RunContinuationsAsynchronously); _pendingCalls[callId] tcs; var frame new Frame { CallId callId, Method method, Payload Serialize(payload), Timestamp DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; await _writeChannel.Writer.WriteAsync(frame); using var cts new CancellationTokenSource(TimeSpan.FromSeconds(15)); try { return await tcs.Task.WaitAsync(cts.Token); } catch (OperationCanceledException) { _pendingCalls.TryRemove(callId, out _); throw new TimeoutException($RPC method {method} timed out after 15 seconds); } }_pendingCalls是一个ConcurrentDictionarystring, TaskCompletionSourceFrame这是全双工 RPC 客户端回调映射的核心数据结构。读任务和写任务通过 call_id 来关联做到“发出去不管回来再找”这样同一个连接上可以同时发起任意数量的调用而不互相阻塞。3.4 双向流与半双工链路的桥接扩展看了上面的方案可能有老铁会问你说的全双工都是建立在 TCP/IP 双向通道上的如果底层只有一条单向链路怎么办比如传统串口通信是半双工的发和收必须轮流来怎么接进全双工 RPC我在一个硬件对接项目里遇到过这个问题。设备侧是串口半双工1 秒钟只能有一个方向的数据在线上走。我当时做了一个适配层把双向流 RPC 和串口半双工的差异隐藏掉上层继续“假装”全双工实际上往队列写消息时先加锁通过时间片轮转确保同一时刻只有一个方向在传输。具体实现方式是给每一条下发消息记录写入时间戳在收到设备响应之前不再发送下一条消息而是把后续消息排队同时上层调用仍然可以并发发起只是实际传输被串行化。这样做的好处是业务代码可以保持全双工 RPC 的编程模型不用考虑底层链路的方向限制。所以本质上“全双工 RPC”并不一定要求物理链路真的是全双工它更像是对上层暴露的一种“逻辑双工”能力。底层链路是半双工也好窄带宽也好只要适配层能保证方向切换的正确性和消息帧的完整性上层业务就只管调用就好了。4. 常见问题与排查技巧实录这块内容价值含量最高。全双工 RPC 的坑绝大多数不是在“能不能调通”上而是在“看起来正常但偶尔出错”的诡异问题里。我整理了自己实际踩过的一些雷都挺典型建议收藏。4.1 调用超时RPC 在 30 秒内未完成我项目最开始用 gRPC 时压测经常遇到“Cannot finish RPC call in 30 seconds”的报错第一反应是调超时参数但怎么调都没效果。后来才发现问题根本不在应用层的 timeout而是底层的HttpClient对 HTTP/2 长连接有自己的超时控制。gRPC 客户端在 .NET 里默认用的是SocketsHttpHandler如果ConnectTimeout或PooledConnectionLifetime设置不当长连接会被静默回收下一次 RPC 只能重新建连如果建连超时就报上面那串错误。正确做法是把GrpcChannelOptions里的HttpClient配置为长连接友好PooledConnectionLifetime设置成Timeout.InfiniteTimeSpan同时在服务端KeepAlive参数和客户端KeepAliveTime上保持节奏一致让连接持续有流量。我当时把这一组参数对齐之后30 秒超时的问题当场消失属于立竿见影的修复。4.2 curl 56 recv failure 与连接半开这个报错虽然是在用 curl 做联调时看到的但本质是同一个问题长连接已经半开服务端或中间设备悄悄断开了 TCP但客户端不知道继续往 socket 上写数据。于是发送端得到“recv failure: connection reset by peer”或直接超时。排查思路就一条确认链路有没有链路保活。TCP 层面的 keepalive 默认是关闭或超时很长通常是 2 小时等它发现断连黄花菜都凉了应用层的 RPC 保活必须自己接管。我的经验是客户端把KeepAliveTime设为 30 秒服务端开启 gRPC 的 KeepAlive 允许项这样即使没有业务流量连接也会定期有 Ping 帧流动。如果中间还挂了负载均衡设备也要确认这些设备不会因为“空闲超时”把连接踢掉否则应用层心跳没问题也会被中间层“精准打击”。4.3 HTTP/2 协议错误与 net::err_http2_protocol_error用浏览器或者某些客户端访问时出现ERR_HTTP2_PROTOCOL_ERROR常见原因有几个。一是服务端默认启用了 HTTP/2但 nginx 或其它反向代理层只支持 HTTP/1.1导致协议升级失败二是 HTTP/2 要求 TLS 证书解析正常如果证书的域名不匹配连接会直接崩掉。对应用层解决方法是确保代理层的grpc_pass或http2指令配置正确并且检查证书的 SAN 字段是否覆盖访问域名。这类错误往往不是代码逻辑问题而是整个链路里有某个环节没按 HTTP/2 的规范配置。4.4 证书错误net::err_cert_common_name_invalid这个报错我在本地开发时踩过一次本质是证书的“通用名”CN和访问的 IP 或域名不匹配。gRPC 的 TLS 握手阶段会校验主机名本地开发喜欢直接用 IP 访问而证书只签了 localhost于是一连就报SSL_UNRECOGNIZED_NAME_CE或者CERT_COMMON_NAME_INVALID。解决的思路有三个方向一是做一张包含 IP SAN 的自签名证书把IP:127.0.0.1加进去二是开发环境临时跳过证书校验三是正式环境用标准 CA 签发的证书确保 SAN 里写了正确的域名。我在本地调试时通常用第一个方案一劳永逸避免各种校验问题。4.5 ERR_BLOCKED_BY_ORB 与浏览器侧的拦截ERR_BLOCKED_BY_ORB这种问题多半跟浏览器安全机制有关。ORBOpaque Response Blocking是浏览器对跨源资源和私有网络访问的一种保护策略当页面发起请求的目标不是“合理可信”的源时浏览器会直接拦截响应表现为请求发出去但内容完全被屏蔽。全双工 RPC 在浏览器端跑的时候如果目标端口不属于众所周知的 Web 端口80/443或者响应头缺少正确的 CORS 配置就会被 ORB 拦下来。这种问题最容易出现的情况是开发环境前端在 localhost:3000后端 gRPC 服务在 localhost:50051跨端口 非标准端口属于“高风险”访问。解决方式是开发环境开启--disable-featuresBlockInsecurePrivateNetworkRequests之类的调试开关或者让后端返回合法的 CORS 头、预检响应支持。线上环境尽量统一域名和端口避免浏览器对私有网络访问的额外限制找上门。4.6 Docker 与端口转发场景下的连接异常全双工 RPC 跑在容器编排或 Docker 环境里时连接失败的形态往往更诡异。我之前遇到过服务在容器里正常但从宿主机通过“端口映射”访问就频繁超时的问题。排查下来是容器网络模式的iptables转发对长连接的空闲超时策略和宿主机不一样空闲连接被回收后客户端并不知道下一次 RPC 就直接挂在半开连接上。解决办法有两种一是把KeepAliveTime调低到 15 秒左右让连接始终活跃减少被回收的概率二是确认容器编排平台对长连接没有做过度的“空闲连接回收”。另外多副本部署时每个副本属于不同的网络命名空间连接要保持足够长的时间负载均衡策略要优先用“客户端亲和”或者“一致性哈希”而不是普通轮询否则一条长连接可能频繁在不同容器间跳转状态上下文就丢了。4.7 消息顺序错乱与重复响应全双工 RPC 容易出现的一个隐蔽 bug 是“响应收到了但上下文对应不上”表现为某些调用返回了别的方法的结果。这种问题大概率出在两个地方一是写侧并发调用了IServerStreamWriter.WriteAsync导致两帧交错写入流被污染二是读侧没有正确解析帧边界直接把半截消息当成完整消息处理了。排查时我有一个固定套路先看帧边界。把收到的原始字节流 dump 出来确认长度前缀是否正确。再看写侧并发。用Channel做输出队列统一由单一写协程处理这个我再强调一次是保证帧序的唯一稳妥方案。最后看业务层。确认call_id的分配和回填没有在重试逻辑里被错误复用重试时必须生成新的 call_id否则上一个超时调用的响应会落到下一个调用的等待对象上。5. 实操心得与工程化经验补充5.1 全双工 RPC 不是“银弹”注意适用边界用了一轮全双工 RPC 之后我的体会是这个模式和场景非常匹配但它的设计也不是没有代价。首先全双工连接是长连接型资源不像短连接那样用完即弃因此对连接的监管、重启、限流、心跳、负载均衡都要有周全方案。其次调试难度高于普通 RPC因为你没法用简单的“发起一次请求、看一次响应”来验证要同时观察两个方向的数据流。再次连接数是有限的如果一个服务端同时挂几千上万条全双工连接线程模型、内存占用、连接风暴都会成为瓶颈。所以在做技术选型时不要盲目“全双工到底”要先想清楚业务是真的需要双向实时交互还是只是需要服务端推送。后者用单向流或消息队列就足够前者才值得引入全双工 RPC。全双工意味着连接更复杂、运维更精细需要接受这个成本。5.2 建立连接生命周期监控与巡检工程上必须把全双工连接当成“第一等资源”来监控。我在项目里会给每个会话节点维护一个状态机新建、握手成功、工作中、重连中、已关闭。每次状态迁移都写结构化日志并在关键节点新建连接、心跳超时、组织性的重连打上报指标。这样线上如果出现“连接掉线”或“大批量断连”能快速定位是网络问题、服务端负载问题还是客户端触发的问题。我建议至少采集四个指标活跃连接数、每秒接收消息数、每秒发送消息数、平均调用延迟。另外要有一个“连接健康度”看板在连接掉线率突然升高时能第一时间收到告警别等用户反馈了才去排查。5.3 消息权重与控制通道与普通消息分离全双工连接上不同的 Frame 重要性差别很大控制类消息心跳、取消、配置变更必须优先处理普通业务消息可以稍慢。这个需求看起来简单实现起来如果不注意全挤在一个无界队列里业务流量一大控制消息可能无限期排队心跳检测就超时了连接反而被自己的流量卡死。我的做法是分两个优先级队列一个ChannelFrame给普通消息一个优先级更高、容量更小的ChannelFrame给控制消息。写任务同时监听两个队列控制队列有货就先写控制空闲了再写普通消息。另外对所有队列都要设置容量上限一旦写满说明业务方流量太大或消费端卡住了宁可主动断开重连也别让内存无限制增长。5.4 协议扩展与版本兼容的长期规划全双工 RPC 一旦上线协议升级就是在飞行中换飞机轮子所以版本兼容必须提前设计。我在 proto 里给每个消息都保留了version字段框架层会校验消息版本是否匹配。业务方法名用“命名空间 版本号”的方式管理比如v1.push_config、v2.push_config这样服务端和客户端可以独立升级先升一方不导致另一方崩掉。另外强烈建议在 proto 字段设计上多用 optional不用 required。新增字段永远用 new tag老字段永远不加复用的 trick。这样哪怕两边版本不一致也能尽量做到“新客户端 老服务端”和“老客户端 新服务端”都能正常工作。5.5 最后的一些零碎建议不要在业务方法里直接处理流写入统一通过写入队列走因为流对象不是线程安全的。完成或取消 RPC 时一定要把TaskCompletionSource从挂起字典里移除否则字典会越积越大这属于典型的“内存泄漏”。使用ServerCallContext里的CancellationToken及时处理客户端取消避免服务端任务无谓地继续跑。服务端统一限制MaxConcurrentStreams防止单个客户端把连接上的所有流都占满饿死其他客户端。客户端要设置DnsRefreshPeriod避免域名更新后客户端一直解析到老 IP——我在这上面吃过亏。日志里不要打全量 payload尤其是二进制大对象会瞬间把日志系统打崩只记录call_id、方法名和耗时就够了。结尾就我个人而言全双工 RPC 带来的最大思维转变就是调用不再是一个“回合制游戏”而是变成了一条“双向车道”。你不用再绞尽脑汁设计一堆轮询接口、回调接口、推送接口而是直接把两个方向的方法调用都放在同一套契约里写起来自然很多。如果你正好在做一个需要频繁双向交互的项目或者被服务端推送问题折腾到头大建议先画一张消息流向图再看看 gRPC 双向流能不能把整个交互统一起来。亲手实践一次比看十篇对比分析都管用。
返回列表