当前位置:首页 > 服务端 > 如何实现Http请求报头的自动转发[设计篇]

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

2022年11月09日 21:14:51服务端6

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

目录
一、HeaderForwardObserver
二、HttpClientObserver
三、HeaderForwaderStartupFilter
四、HttpInvocationContext/HttpInvocationContextScope
五、OutgoingHeaderCollectionProvider
六、服务注册

一、HeaderForwardObserver

HeaderForwarder组件利用HeaderForwardObserver对HttpClient进行拦截,并将需要的报头添加到由它发出的请求消息中,我们曾经在《四种为HttpClient添加默认请求报头的解决方案》一文中介绍过这种方案,这也是大部分APM自动添加跟踪报头的解决方案。具体的原理其实很简单:当HttpClient发送请求过程中会利用DiagnosticListener触发一些列事件,并在事件中提供相应的对象,比如发送的HttpRequestMessage和接收的HttpResponseMessage。如果我们需要这个过程进行干预,只需要订阅相应的事件并将干预操作实现在提供的回调中。《ASP.NET Core 3框架揭秘》第8“诊断日志”具有对DiagnosticListener的详细介绍。

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

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

如下所示的是HeaderForwardObserver的定义。如代码片段所示,HeaderForwardObserver实现了IObserver<KeyValuePair<string, object>> 接。在实现的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中。

如何实现Http请求报头的自动转发[设计篇] _ JavaClub全栈架构师技术笔记

六、服务注册

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请求报头的自动转发[应用篇]
如何实现Http请求报头的自动转发[设计篇]

作者:Artech
来源链接:https://www.cnblogs.com/artech/p/http-header-forwarder-02.html

版权声明:
1、JavaClub(https://www.javaclub.cn)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。

2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。


本文链接:https://www.javaclub.cn/server/68982.html

标签: HTTP
分享给朋友:

“如何实现Http请求报头的自动转发[设计篇]” 的相关文章

Android5.0以下手机通过https请求服务器报SSLException异常的原因及解决方案

Android5.0以下手机通过https请求服务器报SSLException异常的原因及解决方案

问题描述:     最近突然收到了测试组提的bug,说是部分手机打开APP的时候会报服务器连接异常。然后我就用我的手机试了下,发现没有问题。然后用他们在缺陷描述中写的测试机型和系统版本进行了测试。发现确实会报服务区连接异常。然后通过本地调试,发现网络请求...

okhttp Fatal Exception thrown on Scheduler.Worker thread问题解决

我在项目里面同时使用了以下两个类库: compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' compile 'com.squareup.okhttp3:okhttp:3.5.0' 编译时没有错误,但是一调用...

图解springboot后端发送HttpGet和HttpPost请求

图解springboot后端发送HttpGet和HttpPost请求

图解springboot后端发送HttpGet和HttpPost请求 [提前声明] 文章由作者:张耀峰 结合自己生产中的使用经验整理,最终形成简单易懂的文章 写作不易,转载请注明,谢谢! 代码案例地址: ?https://github.com/My...

java项目常用工具类之http请求工具类

jdk1.8+spring4.3.12 一、问题描述及试用场景: 在项目开发中,经常用调用http接口,下面是封装apache的httpclient工具类。 二、样例代码: package org.egg.utils; im...

Chrome Uncaught Error: NETWORK_ERR: XMLHttpRequest Exception 101

Chrome中有时Ajax请求会出现这种错误“Uncaught Error: NETWORK_ERR: XMLHttpRequest”。   出现这类情况大概有这几种: 1:Web页面中请求本地文件数据【如请求的url:file:///c://1.txt】...

IDEA:http://fls.jetbrains-agent.com-- No response

起因: win10电脑关机,打开,自动更新了系统。 (误删除vmoptions的-javaagent 绿色 jar包,也会出现↓。。。) [ 2022年1月14日改vmoptions这玩意又替换的别人后而忘了加 -javaagent:D:\idea\je...

springboot 重定向(HttpServletResponse实现)

springboot 重定向     *************************** 示例   @RestController public class Hello3Controller...

http协议中cookie和session的区别

http协议中cookie和session的区别

1、 当客户端访问一个支持cookie的网站的时候,用户就会提供包括用户名在内的个人信息,把它提交到服务器,接着服务器在向客户端回传相应的超文本的同时,也会发回这些个人信息。当然,这些信息并不是存放在reponsebody里面,而是在response header里...

HTTP请求(方法,格式;Fidder抓包;get与post请求的区别)

HTTP请求(方法,格式;Fidder抓包;get与post请求的区别)

目录 一,HTTP请求方法 常用请求方法 其他请求方法 二,HTTP请求作用 三,HTTP请求整体格式 常见的请求体数据类型: 四,使用Fidder抓包工具进行验证 验证GET请求 验证POST请求 五,GET和POST...

java如何设置http请求头、请求头作用、idea测试请求等

java如何设置http请求头、请求头作用、idea测试请求等

本文涉及两种请求方式,即 get 和 post 。通过java后台设置请求头部 可以根据需求修改或者添加请求头信息。 修改请求头代码 根据不同的请求方式,在main方法中选择调用不同的方法(get/post ) package com....

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。