YARP网关集成Steeltoe

    Yarp是微软开源的一个用.net实现的反向代理工具包 , 仓库地址 https://github.com/microsoft/reverse-proxy 

    官方默认使用配置文件配置路由,有老哥把它集成到数据库中做成了可配置热更新的方式 :  https://www.cnblogs.com/fanshaoO/p/14603159.html 

   由于部门使用spring cloud gateway作为网关,故想实现一个类似的功能 , 废话不多说,直接上代码 :

    SteeltoeLookupMiddleware :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Steeltoe.Discovery;
using Yarp.ReverseProxy.Middleware;

namespace Yarp.ReverseProxy.Steeltoe
{
    internal class SteeltoeLookupMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;
        private readonly SteeltoeProvider _steeltoeProvider;
        public SteeltoeLookupMiddleware(
        RequestDelegate next,
        ILogger<SteeltoeLookupMiddleware> logger,
        IDiscoveryClient discoveryClient = null)
        {
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _steeltoeProvider = new SteeltoeProvider(discoveryClient);
        }

        public Task Invoke(HttpContext context)
        {
            var proxyFeature = context.GetRequiredProxyFeature();

            var cluster = proxyFeature.ClusterSnapshot.Options;
            if (cluster.Metadata == null || !cluster.Metadata.TryGetValue("ServiceId", out var serviceId))
            {
                return _next(context);
            }
            var result = _steeltoeProvider.FindAffinitizedDestinations(context, serviceId);
            if (result.Status == Yarp.ReverseProxy.Service.SessionAffinity.AffinityStatus.OK)
            {
                proxyFeature.AvailableDestinations = result.Destinations;
            }
            return _next(context);
        }

    }
}

SteeltoeProvider :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Steeltoe.Common.Discovery;
using Steeltoe.Discovery;
using Yarp.ReverseProxy.Abstractions;
using Yarp.ReverseProxy.RuntimeModel;
using Yarp.ReverseProxy.Service.SessionAffinity;

namespace Yarp.ReverseProxy.Steeltoe
{
    public class SteeltoeProvider
    {
        public SteeltoeProvider(IDiscoveryClient discoveryClient = null)
        {
            _discoveryClient = discoveryClient;
        }

        private readonly IDiscoveryClient _discoveryClient;


        public AffinityResult FindAffinitizedDestinations(HttpContext context, string serviceId)
        {
            var instances = _discoveryClient?.GetInstances(serviceId);

            if (instances == null)
            {
                return new AffinityResult(null, AffinityStatus.AffinityKeyExtractionFailed);
            }

            if (instances.Count == 0)
            {
                return new AffinityResult(null, AffinityStatus.AffinityKeyNotSet);
            }

            var destinationInfos = instances.Select(CreateDestinationInfo).ToList();

            return new AffinityResult(destinationInfos, AffinityStatus.OK);
        }

        private DestinationInfo CreateDestinationInfo(IServiceInstance serviceInstance)
        {
            var destinationId = GetInstanceId(serviceInstance) ?? serviceInstance.ServiceId;
            var destinationInfo = new DestinationInfo(destinationId);
            var destination = new Destination
            {
                Address = serviceInstance.Uri.ToString()
            };
            var config = new DestinationConfig(destination);
            SetDestinationConfig(destinationInfo, config);
            return destinationInfo;
        }

        private void SetDestinationConfig(DestinationInfo destinationInfo, DestinationConfig config)
        {
            _setDestinationConfigInvoker.Invoke(destinationInfo, config);
        }

        private readonly Action<DestinationInfo, DestinationConfig> _setDestinationConfigInvoker = CreateDestinationConfigInvoker();

        private static Action<DestinationInfo, DestinationConfig> CreateDestinationConfigInvoker()
        {
            var destinationInfoInstance = Expression.Parameter(typeof(DestinationInfo), "destinationInfo");
            var destinationConfigInstance = Expression.Parameter(typeof(DestinationConfig), "destinationConfig");
            var property = typeof(DestinationInfo).GetProperty("Config");
            var body = Expression.Assign(Expression.Property(destinationInfoInstance, property), destinationConfigInstance);
            return Expression.Lambda<Action<DestinationInfo, DestinationConfig>>(body, destinationInfoInstance, destinationConfigInstance).Compile();
        }

        private readonly System.Collections.Concurrent.ConcurrentDictionary<Type, Func<IServiceInstance, string>> _getInstanceIdMap = new System.Collections.Concurrent.ConcurrentDictionary<Type, Func<IServiceInstance, string>>();

        private string GetInstanceId(IServiceInstance serviceInstance)
        {
            if (_getInstanceIdMap.TryGetValue(serviceInstance.GetType(), out var invoker))
            {
                return invoker?.Invoke(serviceInstance);
            }
            invoker = _getInstanceIdMap.GetOrAdd(serviceInstance.GetType(), type =>
              {
                  var property = type.GetProperty("InstanceId");
                  if (property == null || !property.CanRead)
                  {
                      return null;
                  }
                  var instance = Expression.Parameter(typeof(IServiceInstance), "instance");
                  var convertInstance = Expression.Convert(instance, type);
                  var body = Expression.Property(convertInstance, property);
                  return Expression.Lambda<Func<IServiceInstance, string>>(body, instance).Compile();

              });
            return invoker?.Invoke(serviceInstance);
        }

    }
}

使用方式也很简单 : 

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapReverseProxy(proxyPipeline =>
                {
                    proxyPipeline.UseAffinitizedDestinationLookup();
                    proxyPipeline.UseMiddleware<SteeltoeLookupMiddleware>();
                    proxyPipeline.UseProxyLoadBalancing();
                    proxyPipeline.UsePassiveHealthChecks();
                })
               ;
            });

需要注意几点:

1.为了避免所有集群都走注册中心,需要显式的配置一下 ServiceId在MetaData中

2.如果不配置 Destinations节点 , 返回的集群为null

{
  "ReverseProxy": {
    "Routes": [
      {
        "serviceApiInvoke": {
          "ClusterId": "serviceApiInvoke",
          "Match": {
            "Path": "/service-api-invoke/{*any}"
          },
          "Transforms": {
            { "PathRemovePrefix": "/service-api-invoke" }
          }
        }
      }
    ],
    "Clusters": {
      "serviceApiInvoke": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://example.com/"
          },
          "MetaData": {
            "ServiceId": "service-api-invoke"
          }
        }
      }
    }
  }
}

这样能初步替换spring cloud gateway了``

原文地址:https://www.cnblogs.com/pokemon/p/14677024.html