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

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Jina AI
Jina AI
Stack Overflow Blog
Stack Overflow Blog
量子位
博客园_首页
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Forbes - Security
Forbes - Security
IT之家
IT之家
N
News and Events Feed by Topic
S
Security Affairs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Webroot Blog
Webroot Blog
Recorded Future
Recorded Future
L
LangChain Blog
Y
Y Combinator Blog
AI
AI
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
Help Net Security
Help Net Security
Cyberwarzone
Cyberwarzone
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google Online Security Blog
Google Online Security Blog
V2EX - 技术
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
I
Intezer
T
Tenable Blog
G
Google Developers Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
云风的 BLOG
云风的 BLOG
C
CXSECURITY Database RSS Feed - CXSecurity.com
有赞技术团队
有赞技术团队
O
OpenAI News
P
Proofpoint News Feed
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Last Week in AI
Last Week in AI
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
Blog — PlanetScale
Blog — PlanetScale

元视角

.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 年双十一交易额 - 元视角 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 健康检查的探索与实现 - 元视角 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 扩展之基于名称的注入实现 - 元视角
ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角
飞鸿踏雪 · 2021-05-26 · via 元视角

gRPC是微软在.NET Core 及其后续版本中主推的 RPC 框架,它使用 GoogleProtocol Buffers 作为序列化协议,使用 HTTP/2 作为通信协议,具有跨语言高性能双向流式调用等优点。考虑到,接下来要参与的是,一个以gRPC为核心而构建的微服务项目。因此,博主准备调研一下gRPC的相关内容,而首当其冲的,则是从 .NET Core 3.1 开始就有的拦截器,它类似于ASP.NET Core中的过滤器和中间件,体现了一种面向切面编程(AOP)的思想,非常适合在 RPC 服务调用的时候做某种统一处理,譬如参数校验、身份验证、日志记录等等。在今天这篇博客中,博主主要和大家分享的是,利用 .NET Core gRPC 中的拦截器实现日志记录的简单技巧,希望能给大家带来一点启发。

开源、多语言、高性能的 gRPC 开源、多语言、高性能的 gRPC

关于 Interceptor 类

Interceptor类是 gRPC 服务拦截器的基类,它本身是一个抽象类,其中定义了下面的虚方法:

public virtual AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>();
public virtual AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>();
public virtual AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>();
public virtual TResponse BlockingUnaryCall<TRequest, TResponse>();
public virtual Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>();
public virtual AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>();
public virtual Task DuplexStreamingServerHandler<TRequest, TResponse>();
public virtual Task ServerStreamingServerHandler<TRequest, TResponse>();
public virtual Task<TResponse> UnaryServerHandler<TRequest, TResponse>();

整体而言,如果从通信方式上来划分,可以分为:流式调用普通调用;而如果从使用方来划分,则可以分为:客户端服务端。进一步讲的话,针对流式调用,它还分为:"单向流" 和 “双向流"。关于这些细节上的差异,大家可以通过 gRPC官方文档 来了解,这里我们给出的是每一种方法对应的用途:

方法名描述
AsyncClientStreamingCall拦截异步客户端流式调用
AsyncDuplexStreamingCall拦截双向流式调用
AsyncUnaryCall拦截异步普通调用
BlockingUnaryCall拦截阻塞普通调用
AsyncServerStreamingCall拦截异步服务端流式调用
ClientStreamingServerHandler拦截客户端流式调用的服务端处理程序
DuplexStreamingServerHandler拦截双向流式调用的服务端处理程序
ServerStreamingServerHandler拦截服务端流式调用的服务端处理程序
UnaryServerHandler拦截普通调用的服务端处理程序

实现一个拦截器

好了,下面我们一起实现一个拦截器。这里,我们使用的是微软官方的例子:

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;
    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}

服务器端

实现服务器端的普通调用拦截,我们需要重写的方法是UnaryServerHandler:

public class GRPCServerLoggingInterceptor : Interceptor
{
    private readonly ILogger<GRPCServerLoggingInterceptor> _logger;
    public GRPCServerLoggingInterceptor(ILogger<GRPCServerLoggingInterceptor> logger)
    {
        _logger = logger;
    }
    
    // 重写 UnaryServerHandler() 方法
    public override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
      TRequest request, ServerCallContext context, 
      UnaryServerMethod<TRequest, TResponse> continuation
    )
    {
        var builder = new StringBuilder();

        // Call gRPC begin
        builder.AppendLine($"Call gRPC {context.Host}/{context.Method} begin.");

        // Logging Request
        builder.AppendLine(LogRequest(request));

        // Logging Response
        var reply = continuation(request, context);
        var response = reply.Result;
        var exception = reply.Exception;
        builder.AppendLine(LogResponse(response, exception));

        // Call gRPC finish
        builder.AppendLine($"Call gRPC {context.Host}/{context.Method} finish.");
        _logger.LogInformation(builder.ToString());

        return reply;
    }
    
    // 记录gRPC请求
    private string LogRequest<TRequest>(TRequest request)
    {
        var payload = string.Empty;
        if (request is IMessage)
            payload = JsonConvert.SerializeObject(
                (request as IMessage)
                .Descriptor.Fields.InDeclarationOrder()
                .ToDictionary(x => x.Name, x => x.Accessor.GetValue(request as IMessage))
            );
        return $"Send request of {typeof(TRequest)}:{payload}";
    }
    
    // 记录gRPC响应
    private string LogResponse<TResponse>(TResponse response, AggregateException exception)
    {
        var payload = string.Empty;
        if (exception == null)
        {
            if (response is IMessage)
                payload = JsonConvert.SerializeObject(
                  (response as IMessage)
                  .Descriptor.Fields.InDeclarationOrder()
                  .ToDictionary(x => x.Name, x => x.Accessor.GetValue(response as IMessage))
                );
            return $"Receive response of {typeof(TResponse)}:{payload}";
        }
        else
        {
            var errorMsgs = string.Join(";", exception.InnerExceptions.Select(x => x.Message));
            return $"Receive response of {typeof(TResponse)} throws exceptions: {errorMsgs}";
        }
    }
}

对于gRPC而言,每一个由.proto声明文件生成的类,都带有一个叫做Descriptor的属性,我们可以利用这个属性获得gRPC请求和响应的详细信息。所以,在LogRequest()LogResponse()两个方法中,我们均使用了这一思路来记录gRPC的报文信息,因为传输层的gRPC使用了二进制作为数据载体,这可以说是一种用可读性换取高效率的做法,不过幸运的是,我们在这里实现了这个小目标。

接下来,为了让这个拦截器真正生效,我们还需要修改一下Startup类中注册gRPC这部分的代码:

services.AddGrpc(options => options.Interceptors.Add<GRPCServerLoggingInterceptor>());

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

gRPC服务器端拦截器效果展示 gRPC服务器端拦截器效果展示

客户端

实现客户端的普通调用拦截,我们需要重写的方法是AsyncUnaryCall(),依样画葫芦即可:

public class GRPCClientLoggingInterceptor : Interceptor
{
    // 重写 AsyncUnaryCall() 方法
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation
    )
    {
        var builder = new StringBuilder();

        // Call gRPC begin
        builder.AppendLine($"Call gRPC {context.Host}/{context.Method} begin.");

        // Logging Request
        builder.AppendLine(LogRequest(request));

        // Logging Response
        var reply = continuation(request, context);
        var response = reply.ResponseAsync.Result;
        var exception = reply.ResponseAsync.Exception;
        builder.AppendLine(LogResponse(response, exception));

        // Call gRPC finish
        builder.AppendLine($"Call gRPC {context.Host}/{context.Method} finish.");
        Console.WriteLine(builder.ToString());

        return reply;
    }
}

类似地,为了让拦截器在客户端生效,我们需要这样:

using Grpc.Core.Interceptors;

var channel = GrpcChannel.ForAddress("https://localhost:5001");
// 简化写法
channel.Intercept(new GRPCClientLoggingInterceptor());
// 完整写法
var invoker = channel.CreateCallInvoker().Intercept(new GRPCClientLoggingInterceptor());
var client = new Greeter.GreeterClient(invoker);
await client.SayHelloAsync(new HelloRequest() { Name = "长安书小妆" });

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

gRPC客户端拦截器效果展示 gRPC客户端拦截器效果展示

客户端感觉不太好的一点就是,这个Interceptor传入的必须是一个实例,考虑到拦截器内部可能会依赖类似ILogger等等的组件,建议还是通过IoC容器来取得一个拦截器的实例,然后再传入Intercept()方法中。博主所在的项目中,则是非常“土豪”地使用了PostSharp,直接走动态编织的方案,果然,“这次第,怎一个羡字了得”。当然,gRPC的客户端,其实提供了日志相关的支持,不过,我个人感觉这个有一点无力:

var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddConsole();
    logging.SetMinimumLevel(LogLevel.Debug);
});
var channel = GrpcChannel.ForAddress(
    "https://localhost:5001",
    new GrpcChannelOptions { LoggerFactory = loggerFactory }
);

本文小结

本文主要分享了gRPC拦截器的使用技巧,gRPC支持一元调用(UnaryCall)、流式调用(StreamingCall)、阻塞调用(BlockingCall),因为区分客户端和服务器端,所以,实际上会有各种各样的组合方式。gRPC的拦截器实际上就是选择对应的场景去重写相应的方法,其中,拦截器的基类为Interceptor类,这里我们都是以普通的一元调用为例的,大家可以结合各自的业务场景,去做进一步的调整和优化。这里,我们使用IMessage类的Descriptor属性来“反射”报文中定义的字段,这样就实现了针对gRPC服务请求/响应的日志记录功能。关于gRPC中日志和诊断的更进一步的话题,大家可以参考微软的 官方文档 。好了,以上就是这篇博客的全部内容啦,谢谢大家!