Table of Contents

如何扩展服务发现

使用目标解析程序来扩展已配置的目标地址集。 目标解析程序可用作与服务发现系统的集成点。

只需实现以下接口

public interface IDestinationResolver
{
    int Order { get; } // 执行优先级,数字越小优先级越高

    Task<IDestinationResolverState> ResolveDestinationsAsync(ClusterConfig cluster, List<DestinationConfig> destinationConfigs, CancellationToken cancellationToken);  
}

public sealed record DestinationConfig
{ 
    public string Address { get; init; } = default!;

    public string? Host { get; init; }
}

不过IDestinationResolverState 不是单纯静态数组,而是允许动态变动的抽象

如果不需要动态,你可以返回静态数组

而比如 dns 服务发现等场景,您可以根据需要随时刷新变化

public interface IDestinationResolverState : IReadOnlyList<DestinationState>, IDisposable
{
}

这里列举 dns 实现来说明或许会容易点

静态 dns

这里我们首先完成一个简单的静态 dns,只再第一次查询dns,解析ip,而后不再定时检查刷新

using System.Net;
using VKProxy.Config;
using VKProxy.ServiceDiscovery;

namespace ProxyDemo.IDestinationResolvers;

public class StaticDNS : IDestinationResolver
{
    public int Order => -1;// 确保比默认dns 优先级高

    public static async Task<IEnumerable<DestinationState>> QueryDNSAsync(ClusterConfig cluster, DestinationConfig destinationConfig, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        // 由于address 是 uri 格式,所以需要解析 host
        var originalUri = new Uri(destinationConfig.Address);
        var originalHost = destinationConfig.Host is { Length: > 0 } host ? host : originalUri.Authority;
        var hostName = originalUri.DnsSafeHost;

        // dns query
        var addresses = await Dns.GetHostAddressesAsync(hostName, cancellationToken).ConfigureAwait(false);

        // 修改 uri
        var uriBuilder = new UriBuilder(originalUri);
        return addresses.Select(i =>
        {
            // 修改 host 为 ip
            var addressString = i.ToString();
            uriBuilder.Host = addressString;

            return new DestinationState()
            {
                EndPoint = new IPEndPoint(i, originalUri.Port), // 设置ip ,提供给 非http 场景使用
                ClusterConfig = cluster,  // cluster 涉及健康检查配置,所以需要赋值
                Host = originalHost,  // 设置原始host,避免 http 访问失败
                Address = uriBuilder.Uri.ToString() // 设置修改后的 uri
            };
        });
    }

    public async Task<IDestinationResolverState> ResolveDestinationsAsync(ClusterConfig cluster, List<DestinationConfig> destinationConfigs, CancellationToken cancellationToken)
    {
        var tasks = destinationConfigs.Select(async i => await QueryDNSAsync(cluster, i, cancellationToken));
        await Task.WhenAll(tasks);

        return new StaticDestinationResolverState(tasks.SelectMany(i => i.Result).ToArray());
    }
}

配置

var app = Host.CreateDefaultBuilder(args)
    .ConfigureServices(i =>
    {
        i.AddSingleton<IDestinationResolver, StaticDNS>(); // 这一行
    })
    .UseReverseProxy()
    .Build();

await app.RunAsync();

动态 dns

这里我们再完成一个简单的动态 dns,不止第一次查询dns,解析ip,而后定时检查刷新检查是否变化

using System.Net;
using VKProxy.Config;
using VKProxy.ServiceDiscovery;

namespace ProxyDemo.IDestinationResolvers;

namespace ProxyDemo.IDestinationResolvers;

public class NonStaticDNS : DestinationResolverBase // 基于 DestinationResolverBase 可以简化重复编码,cluster destinationConfigs 都会被放在 FuncDestinationResolverState 中持久
{
    public override int Order => -2;

    public override async Task ResolveAsync(FuncDestinationResolverState state, CancellationToken cancellationToken)
    {
        var tasks = state.Configs.Select(async i => await StaticDNS.QueryDNSAsync(state.Cluster, i, cancellationToken));
        await Task.WhenAll(tasks);
        state.Destinations = tasks.SelectMany(i => i.Result).ToArray(); // 变更只需要赋值替换就好, 您还可以加入变更检查,在数据未变化时减少替换的影响

        // 这里用简单的 CancellationChangeToken 延迟触发变更
        var cts = new CancellationTokenSource();
        cts.CancelAfter(60000);

        new CancellationChangeToken(cts.Token).RegisterChangeCallback(o =>
        {
            if (o is FuncDestinationResolverState s)
            {
                // 只需再次调用 ResolveAsync
                ResolveAsync(s, new CancellationTokenSource(60000).Token).ConfigureAwait(false).GetAwaiter().GetResult();
            }
        }, state);
    }
}

配置

var app = Host.CreateDefaultBuilder(args)
    .ConfigureServices(i =>
    {
        i.AddSingleton<IDestinationResolver, NonStaticDNS>(); // 这一行
    })
    .UseReverseProxy()
    .Build();

await app.RunAsync();