如今的应用部署逐渐向微服务化发展,导致一个完整的事务往往会跨越很多的应用或服务,出于分布式链路跟踪的需要,我们往往将从上游服务获得的跟踪请求报头无脑地向下游服务进行转发。本文介绍的这个名为HeaderForwarder的组件可以帮助我们完成针对指定HTTP请求报头的自动转发。本篇文章分为上下两篇,上篇通过三个例子介绍HeaderForwarder的应用场景,下篇则介绍该组件的设计与实现。[源代码从这里下载]
目录
一、自动转发指定的请求报头
二、添加任意需要转发的请求报头
三、在非ASP.NET Core应用中使用
一、自动转发指定的请求报头
假设整个分布式调用链路由如下图所示的三个应用构成。请求由控制台应用App1通过HttpClient向WebApp1(localhost:5000),该请求携带foo和bar两个需要被转发的跟踪报头。ASP.NET Core应用WebApp1在通过HttpClient调用WebApp2时,我们的组件会自动实现这对这两个请求报头的转发。
如下所示的是作为下游应用的WebApp2的定义。如代码片段所示,为了验证指定的跟踪报头是否在WebApp1中被我们的组件成功转发,我们将接收到的所有请求报头拼接成一个字符串作为响应内容。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(web => web.Configure(app=>app.Run(Process))) .Build() .Run(); static Task Process(HttpContext httpContext) { var headerString = string.Join(";", httpContext.Request.Headers.Select(it => $"{it.Key}={it.Value}")); return httpContext.Response.WriteAsync(headerString); } } }
WebApp1的所有代码定义如下。HeaderForwarder组件通过调用IHostBuilder的扩展方法UseHeaderForwarder进行注册,在调用该方法的时候我们指定了需要转发的请求报头名称(foo和bar)。在接收到请求之后,WebApp1会利用HttpClient调用WebApp2,并将得到结果作为相应的内容。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .UseHeaderForwarder(forwarder=>forwarder.AddHeaderNames("foo", "bar")) .ConfigureWebHostDefaults(web => web .ConfigureServices(svcs=>svcs.AddHttpClient()) .Configure(app => app.Run(Process))) .Build() .Run(); static async Task Process(HttpContext httpContext) { var httpClient = httpContext.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient(); var headerString = await httpClient.GetStringAsync("http://localhost:6000"); await httpContext.Response.WriteAsync(headerString); } } }
作为上游应用的App具有如下所示的定义。它直接利用HttpClient向WebApp1发送了一个请求,该请求携带了foo和bar这两个需要WebApp1转发的报头。如果WebApp1完成了针对这两个请求报头的转发,那么得到的响应内容将包含这两个报头的值,我们将这一验证逻辑体现在两个调试断言中。
class Program { static async Task Main(string[] args) { var httpClient = new HttpClient(); var request = new HttpRequestMessage { RequestUri = new Uri("http://localhost:5000"), Method = HttpMethod.Get }; request.Headers.Add("foo", "123"); request.Headers.Add("bar", "456"); var response = await httpClient.SendAsync(request); var headers = (await response.Content.ReadAsStringAsync()).Split(";"); Debug.Assert(headers.Contains("foo=123")); Debug.Assert(headers.Contains("bar=456")); } }
二、添加任意需要转发的请求报头
上面我们演示了HeaderForwarder组件自动提取指定的报头并自动转发的功能,实际上该组件还可以帮助我们将任意的报头添加到由HttpClient发出的请求消息中。假设WebApp1除了自动转发的foo和bar报头之外,还需要额外添加一个baz报头,我们可以对程序作如下的修改。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .UseHeaderForwarder(forwarder => forwarder.AddHeaderNames("foo", "bar")) .ConfigureWebHostDefaults(web => web .ConfigureServices(svcs => svcs.AddHttpClient()) .Configure(app => app.Run(Process))) .Build() .Run(); static async Task Process(HttpContext httpContext) { using (new HttpInvocationContextScope()) { HttpInvocationContext.Current.AddOutgoingHeader("baz", "789"); var httpClient = httpContext.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient(); var headerString = await httpClient.GetStringAsync("http://localhost:6000"); await httpContext.Response.WriteAsync(headerString); } } } }
如上面的代码片段所示,我们将针对HttpClient的调用置于HttpInvocationContextScope对象构建的上下文范围中。在调用HttpClient发送请求之前,我们通过Current静态属性得到当前的HttpInvocationContext上下文,并通过调用其AddOutgoingHeader方法设置待转发的baz报头。为了验证WebApp1针对baz报头的转发,我们将App的程序进行如下的改写。
class Program { static async Task Main(string[] args) { var httpClient = new HttpClient(); var request = new HttpRequestMessage { RequestUri = new Uri("http://localhost:5000"), Method = HttpMethod.Get }; request.Headers.Add("foo", "123"); request.Headers.Add("bar", "456"); var response = await httpClient.SendAsync(request); var headers = (await response.Content.ReadAsStringAsync()).Split(";"); Debug.Assert(headers.Contains("foo=123")); Debug.Assert(headers.Contains("bar=456")); Debug.Assert(headers.Contains("baz=789")); } }
如果涉及到多个HTTP调用都需要对相同的报头进行转发,上面介绍的这种基于HttpInvocationContextScope/HttpInvocationContext的编程模式会变得很方便。
using (new HttpInvocationContextScope()) { HttpInvocationContext.Current .AddOutgoingHeader("foo", "123") .AddOutgoingHeader("bar", "456") .AddOutgoingHeader("baz", "789"); var result1 = await httpClient.GetStringAsync("http://www.foo.com/"); var result2 = await httpClient.GetStringAsync("http://www.bar.com/"); var result3 = await httpClient.GetStringAsync("http://www.baz.com/"); }
三、在非ASP.NET Core应用中使用
在ASP.NET Core应用中,HeaderForwarder是通过调用IHostBuilder的扩展方法UseHeaderForwarder进行注册的,如果在控制台应用又该如何使用。其实很简单,HeaderForwarder针对请求(通过HttpClient发送)报头的添加是通过该注册提供的一个HttpClientObserver对象提供的,它实现了IObserver<DiagnosticListener>接口,我们只需要对该对象进行注册就可以了。
class Program { static async Task Main() { var httpClientObserver = new ServiceCollection() .AddHeaderForwarder() .BuildServiceProvider() .GetRequiredService<HttpClientObserver>(); DiagnosticListener.AllListeners.Subscribe(httpClientObserver); using (new HttpInvocationContextScope()) { HttpInvocationContext.Current .AddOutgoingHeader("foo", "123") .AddOutgoingHeader("bar", "456"); var headers = (await new HttpClient().GetStringAsync("http://locahost:5000")).Split(";"); Debug.Assert(headers.Contains("foo=123")); Debug.Assert(headers.Contains("bar=456")); Debug.Assert(headers.Contains("baz=789")); } } }
如上面的代码片段所示,我们调用扩展方法AddHeaderForwarder将HeaderForwarder相关的服务注册到创建的ServiceCollection对象上,并利用构建的IServiceProvider对象得到我们需要的HttpClientObserver对象,并将其添加到DiagnosticListener.AllListeners属性的IObservable<DiagnosticListener>列表中。有了HttpClientObserver的加持,设置请求报头的方式就可以通过上述的编程模式了。