如何实现Http请求报头的自动转发之设计

news2024/11/25 7:06:35

HeaderForwarder组件不仅能够从当前接收请求提取指定的HTTP报头,并自动将其添加到任何一个通过HttpClient发出的请求中,它同时也提供了一种基于Context/ContextScope的编程模式是我们可以很方便地将任何报头添加到指定范围内的所有由HttpClient发出的请求中。现在我们来简单聊聊该组件的设计和实现原理。

一、HeaderForwardObserver

HeaderForwarder组件利用HeaderForwardObserver对HttpClient进行拦截,并将需要的报头添加到由它发出的请求消息中,我们曾经介绍过这种方案,这也是大部分APM自动添加跟踪报头的解决方案。具体的原理其实很简单:当HttpClient发送请求过程中会利用DiagnosticListener触发一些列事件,并在事件中提供相应的对象,比如发送的HttpRequestMessage和接收的HttpResponseMessage。如果我们需要这个过程进行干预,只需要订阅相应的事件并将干预操作实现在提供的回调中。

HeaderForwarder用来添加请求报头的是一个类型为HeaderForwardObserver的对象。在介绍该类型之前,我们得先来介绍如下这个IOutgoingHeaderCollectionProvider接口,顾名思义,它用来提供需要被添加的所有HTTP请求报头。

public interface IOutgoingHeaderCollectionProvider
{
    IDictionary<string, StringValues> GetHeaders();
}

如下所示的是HeaderForwardObserver的定义。如代码片段所示,HeaderForwardObserver实现了IObserver<KeyValuePair<stringobject>> 接。在实现的OnNext中,通过对事件名称(System.Net.Http.HttpRequestOut.Start)的比较订阅了HttpClient在发送请求前触发的事件,并从提供的参数提取出表示待发送请求的HttpRequestMessage对象(对应Request属性)。有了这个待发送的请求,我们只需要从构造函数中注入的IOutgoingHeaderCollectionProvider 对象提取出所有报头列表,并将其添加这个HttpRequestMessage对象中即可。

public sealed class HeaderForwardObserver : IObserver<KeyValuePair<string, object>>
{
    private static Func<object, HttpRequestMessage> _requestAccessor;
    private readonly IOutgoingHeaderCollectionProvider _provider;
   
    public HeaderForwardObserver(IOutgoingHeaderCollectionProvider provider)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
    }
   
    public void OnCompleted() { }
    public void OnError(Exception error) { }
    public void OnNext(KeyValuePair<string, object> value)
    {
        if (headers.Any() && value.Key == "System.Net.Http.HttpRequestOut.Start")
        {
             var headers = _provider.GetHeaders();
            _requestAccessor ??= CreateRequestAccessor(value.Value.GetType());
            var outgoingHeaders = _requestAccessor(value.Value).Headers;
            foreach (var kv in headers)
            {
                outgoingHeaders.Add(kv.Key, kv.Value.AsEnumerable());
            }
        }
    }

    private static Func<object, HttpRequestMessage> CreateRequestAccessor(Type type)
    {
        var requestProperty = type.GetProperty("Request");
        var payload = Expression.Parameter(typeof(object));
        var convertToPayload = Expression.Convert(payload, type);
        var getRequest = Expression.Call(convertToPayload, requestProperty.GetMethod);
        var convertToRequest = Expression.Convert(getRequest, typeof(HttpRequestMessage));
        return Expression.Lambda<Func<object, HttpRequestMessage>>(convertToRequest, payload).Compile();
    }
}

二、HttpClientObserver

HeaderForwardObserver借助于如下这个HttpClientObserver进行注册。如代码片段所示,HttpClientObserver 实现了IObserver<DiagnosticListener>接口,在实现的OnNext方法中,它创建出HeaderForwardObserver对象并将其订阅到HttpClient使用的DiagnosticListener对象上(该对象的名称为HttpHandlerDiagnosticListener)。

public sealed class HttpClientObserver : IObserver<DiagnosticListener>
{
    private readonly IOutgoingHeaderCollectionProvider _provider;
    public HttpClientObserver(IOutgoingHeaderCollectionProvider provider)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
    }    
    public void OnCompleted() { }
    public void OnError(Exception error) { }
    public void OnNext(DiagnosticListener value)
    {
        if (value.Name == "HttpHandlerDiagnosticListener")
        {
            value.Subscribe(new HeaderForwardObserver(_provider));
        }
    }
}

三、HeaderForwaderStartupFilter

我们将针对HttpClientObserver的注册实现在如下这个HeaderForwaderStartupFilter类型中。如代码片段所示,HeaderForwaderStartupFilter实现了IStartupFilter接口,针对HttpClientObserver的注册就实现在Configure方法中。

public sealed class HeaderForwaderStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app => {
            DiagnosticListener.AllListeners.Subscribe(app.ApplicationServices.GetRequiredService<HttpClientObserver>());
            next(app);
        };
    }
}

四、HttpInvocationContext/HttpInvocationContextScope

接下来我们讨论待转发HTTP报头的来源问题。带转发报头有两种来源,一种是从当前请求中提取出来的,另一种是手工添加到HttpInvocationContext上下文中。如下所示的是HttpInvocationContext的定义,我们添加的报头就存储在它的OutgoingHeaders 属性中,表示当前上下文的HttpInvocationContext对象存储在AsyncLocal<HttpInvocationContext>对象上。

public sealed class HttpInvocationContext
{
    internal static readonly AsyncLocal<HttpInvocationContext> _current = new AsyncLocal<HttpInvocationContext>();
    public static HttpInvocationContext Current => _current.Value;
    public IDictionary<string, StringValues> OutgoingHeaders { get; } = new Dictionary<string, StringValues>();
    internal HttpInvocationContext() { }
}

HttpInvocationContextScope用来控制HttpInvocationContext的范围(生命周期),从定义可以看出,只有在创建该Scope的using block范围为才能得到当前的HttpInvocationContext上下文。

public sealed class HttpInvocationContextScope : IDisposable
{
    public HttpInvocationContextScope()
    {
        HttpInvocationContext._current.Value = new HttpInvocationContext();
    }
    public void Dispose() => HttpInvocationContext._current.Value = null;
}

五、OutgoingHeaderCollectionProvider

HeaderForwardObserver添加到请求消息中的报头是通过注入的IOutgoingHeaderCollectionProvider对象提供的,现在我们来看看该接口的实现类型OutgoingHeaderCollectionProvider。我们说过,所有的报头具有两个来源,其中一个来源于当前接收的请求,但是并不是请求中携带的所有报头都需要转发,所以我们需要利用如下这个HeaderForwarderOptions类型来配置转发的报头名称。

public class HeaderForwarderOptions
    public ISet<string> AutoForwardHeaderNames { get; } = new HashSet<string>();
    public void AddHeaderNames(params string[] headerNames) => Array.ForEach(headerNames, it => AutoForwardHeaderNames.Add(it));
}

如下所示的是OutgoingHeaderCollectionProvider类型的定义。在实现的GetHeaders方法中,它利用注入的IHttpContextAccessor 对象得到当前HttpContext,并结合HeaderForwarderOptions上的配置得到需要自动转发的报头。然后通过当前HttpInvocationContext上下文你得到手工指定的报头,两者合并之后成为了最终需要添加到请求消息的报头列表。

public sealed class OutgoingHeaderCollectionProvider : IOutgoingHeaderCollectionProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ISet<string> _autoForwardedHeaderNames;

    public OutgoingHeaderCollectionProvider(IHttpContextAccessor httpContextAccessor, IOptions<HeaderForwarderOptions> optionsAccessor)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        _autoForwardedHeaderNames = (optionsAccessor?? throw new ArgumentNullException(nameof(optionsAccessor))).Value.AutoForwardHeaderNames;
    }

    public IDictionary<string, StringValues> GetHeaders()
    {
        var headers = new Dictionary<string, StringValues>();
        try
        {
            var incomingHeaders = _httpContextAccessor.HttpContext?.Request?.Headers;
            if (incomingHeaders != null)
            {
                foreach (var headerName in _autoForwardedHeaderNames)
                {
                    if (incomingHeaders.TryGetValue(headerName, out var values))
                    {
                        headers.Add(headerName, values);
                    }
                }
            }
        }
        catch (ObjectDisposedException) {}

        var outgoingHeaders = HttpInvocationContext.Current?.OutgoingHeaders;
        if (outgoingHeaders != null)
        {
            foreach (var kv in outgoingHeaders)
            {
                if (headers.TryGetValue(kv.Key, out var values))
                {
                    headers[kv.Key] = new StringValues(values.Concat(kv.Value).ToArray());
                }
                else
                {
                    headers.Add(kv.Key, kv.Value);
                }
            }
        }

        return headers;
    }
}

到目前为止,HeaderForwarder的核心成员均已介绍完毕,这些接口/类型之间的关系体现在如下所示的UML中。

六、服务注册

HeaderForwarder涉及的服务通过如下这个AddHeaderForwarder扩展方法进行注册

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddHeaderForwarder(this IServiceCollection services, Action<HeaderForwarderOptions> setup = null)
    {
        services = services ?? throw new ArgumentNullException(nameof(services));
        services.AddOptions();
        services.AddHttpContextAccessor();
        services.TryAddSingleton<IOutgoingHeaderCollectionProvider, OutgoingHeaderCollectionProvider>();
        services.TryAddSingleton<HttpClientObserver>();
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, HeaderForwaderStartupFilter>());
        if (null != setup)
        {
            services.Configure(setup);
        }
        return services;
    }
}

我们进一步定义了针对IHostBuilder接口的扩展方法,我们在前面演示实例中正是使用的这个方法。

public static class HostBuilderExtensions
{
    public static IHostBuilder UseHeaderForwarder(this IHostBuilder hostBuilder, Action<HeaderForwarderOptions> setup = null)
    {
        hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
        hostBuilder.ConfigureServices((_,services) => services.AddHeaderForwarder(setup));
        return hostBuilder;
    }
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/648191.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Java家政服务网站系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

opencv人与摄像头距离、角度检测

参考&#xff1a; https://chtseng.wordpress.com/2018/09/18/%E5%A6%82%E4%BD%95%E4%BC%B0%E7%AE%97%E5%89%8D%E6%96%B9%E4%BA%BA%E7%89%A9%E7%9A%84%E8%B7%9D%E9%9B%A2/ WeChat_20230611160620 1、cv2加载摄像头慢解决方法&#xff0c;单独重新cv2.VideoCapture() https://b…

新《终结者》电影剧本或交由 ChatGPT 书写

据外媒报道&#xff0c;詹姆斯・卡梅隆透露他正在创作下一部《终结者》电影的剧本&#xff0c;他同时表示新剧本的灵感得益于 OpenAI 的 ChatGPT。 打开豆瓣&#xff0c;1984年10月上映的那部「终结者」评分依然在8分以上。而后&#xff0c;终结者系列的电影不断上新&#xff…

基于Java房屋租售网站设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

考研计算机组成思维导图总结(408,833,834)

需要思维导图文件和图片的可以去我的Github上寻找&#xff0c;这份思维导图参考23年王道机组&#xff0c;同时添加了一部分833&#xff0c;834需要知识点。 注意&#xff1a;最后的机组知识点仍要以学院进行对照&#xff0c;这里只是提供参考 思维导图pdf和知识点图片 文章目…

<Linux> 《SUSE Linux 中SSH安全加固》

《SUSE Linux 中SSH安全加固》 1 说明2 安全加固2.1 更改 ssh 默认端口2.2 限制 ROOT 远程登陆2.3 修改默认登录时间2.4 升级旧版本2.5 当用户处于非活动时断线2.6 修改加密协议版本2.7 限制 IP 登录2.8 允许或禁止指定用户和组登录2.9 限制监听 IP2.10 最后修改配置文件的属性…

【JVM 监控工具】JVisualVM的使用

文章目录 前言二、启动JVisualVM三、安装插件四、使用 前言 JVisualVM是一个Java虚拟机的监控工具&#xff0c;要是需要对JVM的性能进行监控可以使用这个工具哦 使用这个工具&#xff0c;你就可以监控到java虚拟机的gc过程了 那么&#xff0c;这么强大的工具怎么下载呢&…

【HBZ分享】FactoryBean的应用 与 BeanFactroyPostProcessor的应用 与 BeanPostProcesser的应用

FactoryBean的应用实战 需要写一个A类来实现FactoryBean实现FactoryBean的3个方法&#xff0c;即getOject(), getObjectType(), isSingleton()注意&#xff1a;在通过xml的【 】标签或者通过注解方式将A类注入容器的时候&#xff0c;返回的实例不是A类&#xff0c;而是T类&…

HTML 基础

目录 HTML 结构 1.认识 HTML 标签 HTML 文件基本结构 标签层次结构 HTML 常见标签 注释标签 标题标签: h1-h6 段落标签: p 换行标签: br 格式化标签 图片标签: img 关于目录结构: 超链接标签: a 链接的几种形式: 表格标签 合并单元格 列表标签 表单标签 form …

【头歌-Python】Python第九章作业(初级)第 1、2、4 关

第1关&#xff1a;绘制温度曲线 任务描述 附件中是某月每天的最高温度和最低温度数据&#xff0c;请绘制本月的高温曲线&#xff08;红色、圆点标记坐标&#xff09;和低温曲线&#xff08;蓝色、星号标记坐标&#xff09;&#xff0c;线型、刻度如输出示例所示&#xff0c;线…

Rust每日一练(Leetday0031) 解码方法、复原 IP 地址

目录 91. 解码方法 Decode Ways &#x1f31f;&#x1f31f; 93. 复原 IP 地址 Restore IP Addresses &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 91. …

chatgpt赋能python:Python如何求平均

Python如何求平均 Python是一种非常流行的编程语言&#xff0c;它被广泛应用于各种领域&#xff0c;包括科学计算、数据分析、Web开发等。在这篇文章中&#xff0c;我们将介绍如何使用Python求平均值。 什么是平均数 平均数&#xff0c;也称为均值&#xff0c;是指一组数据的…

chatgpt赋能python:Python怎么求平均值?全面解析平均值计算方法

Python怎么求平均值&#xff1f;全面解析平均值计算方法 作为一种简洁易用的计算机编程语言&#xff0c;Python的应用范围已经越来越广泛。在Python中&#xff0c;我们可以轻松地进行各种统计和计算工作&#xff0c;其中求平均值是最常见的计算之一。在本文中&#xff0c;我们…

Redis过期策略

Redis 使用的过期删除策略是什么&#xff1f; Redis 是可以对 key 设置过期时间的&#xff0c;因此需要有相应的机制将已过期的键值对删除&#xff0c;而做这个工作的就是过期键值删除策略。 每当我们对一个 key 设置了过期时间时&#xff0c;Redis 会把该 key 带上过期时间存…

Redis 持久化-RDB和 持久化-AOF 的详细介绍以及区别

Redis 持久化-RDB 官方资料 在线文档: https://redis.io/topics/persistence 持久化方案 RDB&#xff08;Redis DataBase&#xff09; AOF&#xff08;Append Of File&#xff09; RDB 是什么? 在指定的时间间隔内将内存中的数据集快照写入磁盘&#xff0c; 也就Snapsh…

java中获取配置文件路径中含有中文,出现乱码的情况解决方案

问题背景&#xff1a;读取配置文件&#xff0c;但是读到的目录信息是中文乱码的。 第一步&#xff1a; 参考代码如上截图&#xff0c;方法即&#xff1a;读取jdbc配置&#xff0c;获取了配置文件&#xff08;jdbc.properties&#xff09;地址&#xff0c;然后加载这个文件读取…

chatgpt赋能python:Python中如何消除空格:从基础操作到高级技巧

Python中如何消除空格&#xff1a;从基础操作到高级技巧 Python是一种广泛使用的编程语言&#xff0c;深受许多工程师和开发者的喜爱。其中一个有用的技巧是如何消除字符串中的空格&#xff0c;特别是在处理和分析文本时。在本文中&#xff0c;我们将介绍三种不同的方法来解决…

springboot+vue+java学生选课成绩系统awwm9

主要内容&#xff1a; (1) 系统用户由三类组成&#xff1a;教师、学生和管理员 (2) 管理员负责的主要功能&#xff1a; 1. 用户进入登录界面&#xff0c;输入用户名&#xff0c;密码&#xff0c;选择管理员用户类型&#xff0c;然后点击“登录”按钮&#xff0c;后台验证…

Linux系统:stress-ng测压工具

目录 一、理论 1.stress工具简介与安装 2.语法及参数 3.具体安装 二、实验 1.运行8 cpu, 4 fork, 5 hdd, 4 io, 50 vm, 10小时 2.CPU测试 3.内存测试 4.IO测试 5.磁盘及I/O测试 三、问题 1. -bash: ./configure: 没有那个文件或目录 2. 下载yum源报错&#xff1a;未…