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

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
Forbes - Security
Forbes - Security
C
Cyber Attacks, Cyber Crime and Cyber Security
Latest news
Latest news
V2EX - 技术
V2EX - 技术
Google DeepMind News
Google DeepMind News
Cyberwarzone
Cyberwarzone
Vercel News
Vercel News
V
Vulnerabilities – Threatpost
I
InfoQ
GbyAI
GbyAI
有赞技术团队
有赞技术团队
雷峰网
雷峰网
阮一峰的网络日志
阮一峰的网络日志
A
Arctic Wolf
F
Full Disclosure
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Check Point Blog
Hugging Face - Blog
Hugging Face - Blog
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
Engineering at Meta
Engineering at Meta
The Register - Security
The Register - Security
T
Tor Project blog
T
Troy Hunt's Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security Affairs
W
WeLiveSecurity
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Stack Overflow Blog
Stack Overflow Blog
Apple Machine Learning Research
Apple Machine Learning Research
H
Heimdal Security Blog
S
Secure Thoughts
Y
Y Combinator Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Security Latest
Security Latest
Martin Fowler
Martin Fowler
G
Google Developers Blog
宝玉的分享
宝玉的分享
腾讯CDC
TaoSecurity Blog
TaoSecurity Blog
T
Threatpost
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Project Zero
Project Zero
Blog — PlanetScale
Blog — PlanetScale
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | 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 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 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 搭配 Swagger 实现微服务文档化 - 元视角
飞鸿踏雪 · 2021-09-28 · via 元视角

有人说,程序员最讨厌两件事情,一件是写文档,一件是别人不写文档,这充分展现了人类双标的本质,所谓的“严于律人”、“宽于律己”就是在说这件事情。虽然这种听来有点自私的想法,是生物自然选择的结果,可一旦人类的大脑皮层在进化过程中产生了“理性”,就会试图去纠正这种来自动物世界的阴暗面。所以,人类双标的本质,大概还是因为这个行为本身就有种超越规则、凌驾于众人之上的感觉,毕竟每个人生来就习惯这种使用特权的感觉。回到写文档这个话题,时下流行的微服务架构,最为显著的一个特点是:仓库多、服务多、接口多,此时,接口文档的重要性就凸显出来,因为接口本质上是一种契约,特别在前后端分离的场景中,只要前、后端约定好接口的参数、返回值,就可以独立进行开发,提供一份清晰的接口文档就显得很有必要。在 RESTful 风格的 API 设计中,Swagger 是最为常见的接口文档方案,那么,当我们开始构建以 gRPC 为核心的微服务的时候,我们又该如何考虑接口文档这件事情呢?今天我们就来一起探讨下这个话题。

protoc-gen-doc 方案

当视角从 RESTful 转向 gRPC 的时候,本质上是接口的描述语言发生了变化,前者是 JSON 而后者则是 Protobuf,因此,gRPC 服务的文档化自然而然地就落在 Protobuf 上。事实上,官方提供了 protoc-gen-doc 这个方案,如果大家阅读过我以前的博客,就会意识到这是 Protobuf 编译器,即 protoc 的插件,因为我们曾经通过这个编译器来生成代码、服务描述文件等等。protoc-gen-doc 这个插件的基本用法如下:

protoc \
  --plugin=protoc-gen-doc=./protoc-gen-doc \
  --doc_out=./doc \
  --doc_opt=html,index.html \
  proto/*.proto

其中,官方更推荐使用 Docker 来进行部署:

docker run --rm \
  -v $(pwd)/examples/doc:/out \
  -v $(pwd)/examples/proto:/protos \
  pseudomuto/protoc-gen-doc

默认情况下,它会生成 HTML 格式的接口文档,看一眼就会发现,就是那种传统的 Word 文档的感觉:

通过 protoc-gen-doc 生成的接口文档 通过 protoc-gen-doc 生成的接口文档

除此以外,这个插件还可以生成 Markdown 格式的接口文档,这个就挺符合程序员的审美,因为此时此刻,你眼前看到的这篇文章,就是通过 Markdown 写成的:

docker run --rm \
  -v $(pwd)/examples/doc:/out \
  -v $(pwd)/examples/proto:/protos \
  pseudomuto/protoc-gen-doc --doc_opt=markdown,docs.md

这个方案如果整合到 CI/CD 中还是挺不错的,传统的 Word 文档形式的接口文档,最主要的缺点是没有版本控制、无法实时更新,因此,对于团队间的协作是非常不利的,我本身挺讨厌这种 Word 文档发来发去的。有时候,只有接口文档是不完美的,因为懒惰的人类希望你能提供个调用示例,最好是直接Ctrl+CCtrl+V这种程度的,对此,博主只有仰天长叹:悠悠苍天,此何人哉……

Swagger 方案

考虑到,第一种方案没有办法对接口进行调试,所以,下面我们来尝试第二种方案,即整合 Swagger 的方案,可能有小伙伴会好奇,Swagger 还能和 Protobuf 这样混搭起来玩?目前,Swagger 是事实上的 OpenAPI 标准,我们只需要在 Protobuf 和 OpenAPI 规范间做一个适配层即可。还记得博主曾经为 ASP.NET MVC 编写的 Swagger 扩展吗?没错,我们要再次“整活”了,首先,这里给出的是 OpenAPI 规范的定义:

{
  "openapi": "3.0.1",
  "info": { },
  "servers": [ ],
  "paths": { },
  "components": { }
}

其中,info 节点里存放的是接口文档的基本信息,例如标题、作者、许可证等。servers 节点里存放的是接口所属服务的主机名、端口号等。paths 节点里存放的是每个 API 端点的信息,例如路由、请求参数、返回值等。components 节点里存放的是类型信息,例如请求参数、返回值中每个属性或者字段的具体类型等。一旦搞清楚了这些内容,我们发现这个里面最关键的两个信息是:pathscomponents,如果我们回过头来看 Protobuf 的声明文件,就会发现这两个东西,分别对应的是 rpcmessage,如下图所示:

Swagger 与 Protobuf 的对应关系 Swagger 与 Protobuf 的对应关系

通常情况下,我们使用 Swashbuckle.AspNetCore.Swagger 这个库来为 ASP.NET Core 项目提供 Swagger 支持,其中最为关键的是ISwaggerProvider接口,这里我们来尝试为 Protobuf 提供一个具体的实现:

public class GrpcSwaggerProvider : ISwaggerProvider
{
    private readonly ISchemaGenerator _schemaGenerator;
    private readonly SwaggerGeneratorOptions _options;
    private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider;
    private readonly GrpcSwaggerSchemaGenerator _swaggerSchemaGenerator;

    public GrpcSwaggerProvider(
        SwaggerGeneratorOptions options, 
        ISchemaGenerator schemaGenerator, 
        IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
        GrpcSwaggerSchemaGenerator swaggerSchemaGenerator
    )
    {
        _options = options;
        _schemaGenerator = schemaGenerator;
        _apiDescriptionsProvider = apiDescriptionsProvider;
        _swaggerSchemaGenerator = swaggerSchemaGenerator;
    }

    public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null)
    {
        if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info))
            throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key));

        var schemaRepository = new SchemaRepository(documentName);

        // Swagger Document
        var swaggerDoc = new OpenApiDocument
        {
            Info = info,
            Servers = BuildOpenApiServers(host, basePath),
            Paths = new OpenApiPaths() { },
            Components = new OpenApiComponents
            {
                Schemas = schemaRepository.Schemas,
                SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>(_options.SecuritySchemes)
            },
            SecurityRequirements = new List<OpenApiSecurityRequirement>(_options.SecurityRequirements)
        };

        // Swagger Filters
        var apiDescriptions = _apiDescriptionsProvider.GetApiDescriptions().Where(x => x.Properties["ServiceAssembly"]?.ToString() == documentName);
        var filterContext = new DocumentFilterContext(apiDescriptions, _schemaGenerator, schemaRepository);
        foreach (var filter in _options.DocumentFilters)
        {
            filter.Apply(swaggerDoc, filterContext);
        }

        // Swagger Schemas
        swaggerDoc.Components.Schemas = _swaggerSchemaGenerator.GenerateSchemas(apiDescriptions);
        var apiDescriptionsGroups = _apiDescriptionsProvider.ApiDescriptionGroups.Items.Where(x => x.Items.Any(y => y.Properties["ServiceAssembly"]?.ToString() == documentName));
        swaggerDoc.Paths = _swaggerSchemaGenerator.BuildOpenApiPaths(apiDescriptionsGroups);

        return swaggerDoc;
    }
}

这里的OpenApiDocument对应着 OpenAPI 规范中的定义的结构,我们需要返回一个OpenApiDocument,并对其ComponentsPaths属性进行填充,这部分工作由GrpcSwaggerSchemaGenerator类来完成。我们这里不会直接去解析 Protobuf 文件,而是利用Google.Protobuf.Reflection这个包来反射 Protobuf 生成的类,然后将其转化为 OpenAPI 规范中定义的结构,更多的细节,大家可以参考这里

接下来,在实现了ISwaggerProvider以后,我们还需要替换掉默认的实现:

public static void AddGrpcGateway(
  this IServiceCollection services, 
  IConfiguration configuration, 
  Action<Microsoft.OpenApi.Models.OpenApiInfo> setupAction = null, 
  string sectionName = "GrpcGateway"
)
{
    var configSection = configuration.GetSection(sectionName);
    services.Configure<GrpcGatewayOptions>(configSection);

    var swaggerGenOptions = new GrpcGatewayOptions();
    configSection.Bind(swaggerGenOptions);

    var swaggerGenSetupAction = BuildDefaultSwaggerGenSetupAction(swaggerGenOptions, setupAction);
    services.AddSwaggerGen(swaggerGenSetupAction);

    // Replace ISwaggerProvider
    services.Replace(new ServiceDescriptor(
        typeof(ISwaggerProvider),
        typeof(GrpcSwaggerProvider),
        ServiceLifetime.Transient
    ));

    // Replace IApiDescriptionGroupCollectionProvider
    services.Replace(new ServiceDescriptor(
        typeof(IApiDescriptionGroupCollectionProvider),
        typeof(GrpcApiDescriptionsProvider),
        ServiceLifetime.Transient
    ));

    // GrpcDataContractResolver
    services.AddTransient<GrpcDataContractResolver>();

    // GrpcSwaggerSchemaGenerator
    services.AddTransient<GrpcSwaggerSchemaGenerator>();

    // Configure GrpcClients
    services.ConfigureGrpcClients(swaggerGenOptions);

    // AllowSynchronousIO
    services.Configure<KestrelServerOptions>(x => x.AllowSynchronousIO = true);
    services.Configure<IISServerOptions>(x => x.AllowSynchronousIO = true);
}

接下来,就是见证奇迹的时刻,gRPC 和 Swagger 牵手成功。从此,查阅和调试 gRPC 接口,我们有了更时尚的做法:

 gRPC 成功牵手 Swagger gRPC 成功牵手 Swagger

调一下接口看看效果:

 通过 Swagger 调试 gRPC 接口 通过 Swagger 调试 gRPC 接口

可以注意到,此时,Swagger 中返回了我们期望的结果,事实上,只有 Swagger 还不足以令它运作起来,其中的诀窍是,博主利用终结点(Endpoints)动态创建了路由。关于这一点,博主曾在 ASP.NET Core gRPC 打通前端世界的尝试 这篇文章中提到过。最终,博主编写了一个更为完整的项目:FluentGrpc.Gateway,而关于 Swagger 的这部分内容则成为了这篇博客的内容,如果大家对这个项目感兴趣的话,欢迎大家去做进一步的探索,欢迎大家 Star 和 PR,而到这里,这篇博客差不多就可以结尾啦!

本文小结

有时候,博主会不由地感慨,整个微服务架构的落地过程中,服务治理是花费时间和精力最多的环节,除了保证接口的稳定性,更多的时候,其实是不同的服务间相互打交道。那么,除了口头传达外,最好的管理接口的方式是什么呢?显然是接口文档。本文分享了两种针对 gRPC 的服务文档化的方案,第一种是由官方提供的 protoc-gen-doc,它可以从 Protobuf 生成 HTML 或者 Markdown 格式的接口文档。第二种是由博主实现的 FluentGrpc.Gateway,它实现了从 Protobuf 到 Swagger 的转换,只需要在项目中引入这个中间件,就可以把 gRPC 带进 Swagger 的世界,不管是查阅接口还是调试接口,都多了一种玩法,如果你还需要给非开发人员提供接口文档,那么,我觉得你还可以试试 YAPI,只需要导入 Swagger 格式的服务描述信息即可,而这一步,我们已经实现了。好了,以上就是这篇博客的全部内容啦,谢谢大家!