惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
T
The Blog of Author Tim Ferriss
U
Unit 42
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
Vercel News
Vercel News
MongoDB | Blog
MongoDB | Blog
P
Proofpoint News Feed
D
DataBreaches.Net
The GitHub Blog
The GitHub Blog
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
Blog — PlanetScale
Blog — PlanetScale
I
InfoQ
Y
Y Combinator Blog
F
Full Disclosure
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
Google Developers Blog
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
S
SegmentFault 最新的问题
腾讯CDC
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Register - Security
The Register - Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Azure Blog
Microsoft Azure Blog
云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
F
Fortinet All Blogs
C
CXSECURITY Database RSS Feed - CXSecurity.com
Hugging Face - Blog
Hugging Face - Blog
T
Threatpost
GbyAI
GbyAI
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
T
The Exploit Database - CXSecurity.com
P
Palo Alto Networks Blog
L
LangChain Blog
T
Tenable Blog
C
Cisco Blogs
T
Threat Research - Cisco Blogs
Google Online Security Blog
Google Online Security Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
gRPC 流式传输极简入门指南 - 元视角
飞鸿踏雪 · 2022-02-18 · via 元视角

最近一直在研究 gRPC 的 ServerReflection,顾名思义,这是 gRPC 里提供的反射接口,当你需要获取某个接口的描述信息,或者是希望动态调用 gRPC 的时候,这一切就会变得非常有用,如果你经常使用 gRPC UI 这款工具来调试 gRPC 接口,那么,你一定会注意到一件事情,即它要求服务端必须支持 ServerReflection API,而这一点在 ASP.NET Core 中已经得到支持,对此感兴趣的朋友可以参考官方文档。当然,这并不是我想表达的重点(我就知道)。重点是什么呢?在使用 ServerReflection API 的过程中,我发现它采用了 gRPC 双向流的方式来进行交互,在过去的日子里,我研究过诸如 WebSocketServer-Sent Events 等等服务器推送的技术,我意识到这是一个非常接近的技术,所以,今天这篇文章,我们来一起聊聊 gRPC 中的流式传输。

从 HTTP/2 说起

首先,我想说,流式传输并不是一个新的概念,这一切就好像,即使你从来没有听过流媒体的概念,可这并不妨碍你追剧、刷短视频,隐隐然有种“不识庐山真面目,只缘身在此山中”的感觉。随着网络带宽和硬件水平的不断提升,越来越多的云服务变得像水、电、天然气一样寻常,以此作喻,流式传输,就像你打开水龙头,此时,水就会源源不断地流出来,并且可以做到随用随取。因此,流式传输实际上就是指通过网络传输媒体,例如音频、视频等的技术统称,服务器可以连续地、实时地向客户端发送数据,而客户端不必等所有数据发送完就可以访问这些数据。按照实现方式的不同,流式传输可以分为 实时流式传输顺序流式传输 两种,前者通常指RTP/RTCP,典型的场景是直播;后者通常是指由 Nginx、Apache 等提供支持的顺序下载。

HTTP/1.1 vs HTTP/2 HTTP/1.1 vs HTTP/2

如果你对 HTTP/2 有一定了解的话,就会知道它最为人所知的特性是多路复用。在 HTTP/1.1 的时代,同一个时刻只能对一个请求进行处理或者响应,换句话说,下一个请求必须要等当前请求处理完才能继续进行,与此同时,浏览器为了更快地加载页面资源,对同一个域名下的请求并发数进行了限制,所以,你会注意到一个有趣的现象,部分网站会使用多个 CDN 加速的域名,而这正是为了规避浏览器的这一限制,HTTP/1.1 时代,可以称为“半双工模式”。到了 HTTP/2 的时代,多路复用的特性让一次同时处理多个请求成为了现实,并且同一个 TCP 通道中的请求不分先后、不会阻塞,是真正的“全双工通信”。一个和本文更贴近的概念是流,HTTP/2 中引入了流(Stream) 和 帧(Frame) 的概念,当 TCP 通道建立以后,后续的所有操作都是以流的方式发送的,而二进制帧则是组成流的最小单位,属于协议层上的流式传输。

gRPC 中的流式传输

OK,现在我们正式开始 gRPC 流式传输的话题。首先,对于一个 gRPC 接口而言,它的起源是 Protobuf 定义。所以,一个最为直观的认识是从 Protobuf 定义入手:

  // 普通 RPC
  rpc SimplePing(PingRequest) returns (PingReply);

  // 客户端流式 RPC
  rpc ClientStreamPing(stream PingRequest) returns (PingReply);

  // 服务器端流式 RPC
  rpc ServerStreamPing(PingRequest) returns (stream PingReply);

  // 双向流式 RPC
  rpc BothStreamPing(stream PingRequest) returns (stream PingReply);

可以注意到,相比普通的 RPC 方法(UnaryCall),采用流式传输的 gRPC 接口,主要是多了一个stream关键字。当该关键字修饰参数时,表示这是一个客户端流式的 gRPC 接口;当该参数修饰返回值时,表示这是一个服务器端流式的 gRPC 接口;当该关键字同时修饰参数和返回值时,表示这是一个双向流式的 gRPC 接口。作为类比,双向流式的 gRPC 接口,约等于 WebSocket,即客户端、服务器端都可以以流的形式收/发数据;服务器端流式的 gRPC 接口,约等于 Server-Sent Events,即服务器端以流的形式发数据。同理,客户端流式的 gRPC 接口,即客户端以流的的形式发数据。

我为什么会突然对这个话题产生兴趣呢?个人以为,主要有两个原因:其一,是工作中使用流式传输的机会不多,即使遇到数据量特别大的场合,大家想到的一定是修改 gRPC 数据传输的大小,而不是采用流式传输的做法;其二,是我注意到像 IstioEnvoyNacos 等项目,内部都是用 gRPC 作为通信协议,当你需要实现一个控制平面的时候,你会发现那里有大量的流式 gRPC 接口等着你去实现。此前,我有一点关乎 gRPC 的想法,譬如动态地为 gRPC-JSON Transcoder 生成配置信息、利用 ServerReflection 扩展 Swagger 等等,毫无疑问,这一切都需要你去了解 gRPC 的流式传输,一组重要的 API 是 IAsyncStreamReader<T>IAsyncStreamReader<T>:

// 1、调用 ServerReflection 获取 gRPC 服务
var callResult = _serverReflectionClient.ServerReflectionInfo(
    deadline: deadline, 
    cancellationToken: cancellationToken
);

// 2、定义一个 Task 来解析服务端响应的流
var resolveServiceListTask = Task.Run(async () =>
{
    while (await callResult.ResponseStream.MoveNext(cancellationToken))
    {
        foreach (var service in callResult.ResponseStream.Current.ListServicesResponse.Service)
        {
            Console.WriteLine(service.Name);
        }
    }
});

// 3、客户端以流的方式写入参数
var request = new ServerReflectionRequest() { ListServices = "" };
await callResult.RequestStream.WriteAsync(request);
await callResult.RequestStream.CompleteAsync();

// 4、客户端以流的方式读出结果
await resolveServiceListTask;

如图所示,你大概可以理解 gRPC 流式传输的运作过程,RequestStream 实现了 IAsyncStreamWriter<T> 接口,负责流的写入,调用 CompleteAsync() 方法时表示数据已经写完;ResponseStream 实现了 IAsyncStreamReader<T> 接口,负责流的读取,典型的迭代器模式,如果你听说过 C# 8.0 里的异步流(AsyncStream),就应该会知道,它属于可枚举类(Enumerable)异步变体,我相信,MoveNext()Current 就不需要再做多余的解释啦,哈哈!

客户端流

对于客户端流式 gRPC 接口而言,客户端负责写入流,服务器端负责读取流,所以,ClientStreamPing() 会生成下面的方法签名,这里是一个简单的实现:

public override async Task<PingReply> ClientStreamPing(
    IAsyncStreamReader<PingRequest> requestStream, 
    ServerCallContext context)
{
    // 从 IAsyncStreamReader<T> 中读取流并放入队列
    var requestQueue = new Queue<string>();
    while (await requestStream.MoveNext())
    {
        requestQueue.Enqueue(requestStream.Current.RequestId);
    }
    
    // 从队列中取出数据并返回
    if (requestQueue.TryDequeue(out var requestId))
    {
        return new PingReply() { RequestId = requestId, Message = "OK" };
    }

    return new PingReply() { RequestId = string.Empty, Message = "" };
}

此时,客户端对应实现如下,继续沿用 IAsyncStreamWriter<T> 的套路:

var callResult = heartBeatClient.ClientStreamPing();
await callResult.RequestStream.WriteAsync(new PingRequest() { 
    RequestId = GetCurrentTimeStamp().ToString() 
});
await callResult.RequestStream.CompleteAsync();
var reply = await callResult.ResponseAsync;

服务器端流

服务器端的流式 gRPC,意味着服务端会通过 IAsyncStreamWriter<T> 以流的方式写入数据:

 public override Task ServerStreamPing(
     PingRequest request, 
     IServerStreamWriter<PingReply> responseStream, 
     ServerCallContext context)
{
    responseStream.WriteAsync(new PingReply() { 
        RequestId = request.RequestId, 
        Message = "OK" 
    });
    return Task.CompletedTask;
}

此时,客户端还是像使用普通方法一样调用即可:

var reply = heartBeatClient.ServerStreamPing(new PingRequest() { 
    RequestId = GetCurrentTimeStamp().ToString() 
});

双向流

一旦我们熟悉了这个套路,理解双向流就再没有什么难度,客户端和服务器端都用 IAsyncStreamWriter<T>IAsyncStreamWriter<T> 进行读写即可:

// 读数据
var requestQueue = new Queue<string>();
while (await requestStream.MoveNext())
{
    requestQueue.Enqueue(requestStream.Current.RequestId);
}

// 写数据
while (requestQueue.TryDequeue(out var requestId))
{
    await responseStream.WriteAsync(new PingReply() { 
        RequestId = requestId, 
        Message = "OK" 
    });
}

同理,客户端采用类似的做法,这里我们发 10 次心跳看看:

//  写数据
var callResult = heartBeatClient.BothStreamPing();

for (var i = 0; i < 10; i++)
{
    await callResult.RequestStream.WriteAsync(new PingRequest() { 
        RequestId = GetCurrentTimeStamp().ToString() 
    });
    Thread.Sleep(500);
}

await callResult.RequestStream.CompleteAsync();

// 读数据
while (await callResult.ResponseStream.MoveNext(CancellationToken.None))
{
    var reply4 = callResult.ResponseStream.Current;
}

此时,我们就可以得到下面的结果:

gRPC 双向流效果演示 gRPC 双向流效果演示

个人感觉,这个可以用在那些需要做双向通信的场合,譬如心跳检测、数据看板、日志监控等等,坦白来讲,相对于 WebSocketServer-Sent Events 等等服务器推送技术,gRPC 的双向流优势并不显著,唯一的优势可能是 HTTP/2 多路复用带来的性能上的提升。

本文小结

最近的状态一直不太好,因为工作中的琐事消耗了大量精力。因而,这篇平淡如白开水般的线性叙事,委实不能被称之为一篇博客,而这大概就是我这段时间的真实写照。作为一名双子座,我的好奇心常常引导着我去关注那些意外的收获,就像 gRPC 里的流式传输,本质上因为我想通过 ServerReflection 为 gRPC 构建一份 Swagger 风格的 API 文档,这个工作目前还有些悬而未决的问题。当然,在这个过程中,大致搞懂了如何去动态调用一个 gRPC 接口,因为 FluentGrpc.Gateway 目前依赖 gRPC 生成客户端代码,或者说这种建立在动态链接库上的方案有一定的缺陷。类似地,工作中发现 Envoy 的 gRPC-JSON Transcoder 插件,需要手动配置每一个 gRPC 服务,开始琢磨怎么能让这个过程更智能一点,发现需要 Envoy 的 xDS API、了解控制平面,这种感觉就像在挖一口井,能不能看到水我不知道,每一铲子下去都有意外收获,这种探险的感觉非常有趣,唯一的平衡点在于,主动的发现永远都会比被动的接受花更多的时间。这篇流水账主要介绍了 gRPC 里流式传输,即客户端流、服务器端流和双向流,如果你足够有心,不妨回过头看看这篇文章 ASP.NET Core gRPC 拦截器的使用技巧分享,我相信你会有不一样的感悟,好了,这就是全部的内容啦,谢谢大家!