解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
8.11 KiB
X
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Xml.Serialization;

namespace NewLife.Remoting;

/// <summary>端点类型。用于竞速时的优先级排序</summary>
public enum EndpointCategory
{
    /// <summary>内网IPv4</summary>
    InternalIPv4,
    /// <summary>内网IPv6</summary>
    InternalIPv6,
    /// <summary>公网IPv4</summary>
    ExternalIPv4,
    /// <summary>公网域名</summary>
    ExternalDomain,
    /// <summary>公网IPv6</summary>
    ExternalIPv6
}

/// <summary>Http服务节点</summary>
/// <remarks>
/// 表示一个可用于负载均衡和故障转移的Http服务端点。
/// 包含服务地址、权重、健康状态等信息,支持:
/// <list type="bullet">
/// <item><description>负载均衡:通过 <see cref="Weight"/> 属性控制流量分配比例</description></item>
/// <item><description>故障转移:通过 <see cref="NextTime"/> 属性实现节点屏蔽和恢复</description></item>
/// <item><description>健康监控:通过 <see cref="Times"/>、<see cref="Errors"/> 统计请求状态</description></item>
/// <item><description>连接复用:通过 <see cref="Client"/> 属性复用 HttpClient 实例</description></item>
/// <item><description>竞速调用:通过 <see cref="Rtt"/>、<see cref="Category"/> 实现智能排序</description></item>
/// </list>
/// </remarks>
public class ServiceEndpoint
{
    #region 属性
    /// <summary>名称。用于标识和日志记录</summary>
    public String Name { get; set; } = null!;

    /// <summary>URI名称。地址的规范化表示,如 http://127.0.0.1:8080</summary>
    public String UriName { get; set; } = null!;

    /// <summary>服务地址</summary>
    public Uri Address { get; set; } = null!;

    /// <summary>访问令牌。用于该节点的身份验证</summary>
    public String? Token { get; set; }

    /// <summary>总请求次数</summary>
    public Int32 Times { get; set; }

    /// <summary>错误次数</summary>
    public Int32 Errors { get; set; }

    /// <summary>创建时间。HttpClient 创建时间,每过一段时间清空重建,更新域名缓存</summary>
    [XmlIgnore, IgnoreDataMember]
    public DateTime CreateTime { get; set; }

    /// <summary>下次可用时间。节点出错时设置屏蔽期,在此时间之前不会被选中</summary>
    [XmlIgnore, IgnoreDataMember]
    public DateTime NextTime { get; set; }

    private HttpClient? _client;
    /// <summary>Http客户端。复用连接以提高性能</summary>
    [XmlIgnore, IgnoreDataMember]
    public HttpClient? Client
    {
        get => _client;
        set => _client = value;
    }

    /// <summary>原子获取或创建客户端,避免并发首次调用时重复创建导致连接池泄漏</summary>
    /// <param name="factory">客户端工厂</param>
    /// <returns>Http客户端</returns>
    internal HttpClient TryGetClient(Func<HttpClient> factory)
    {
        var client = _client;
        if (client != null) return client;

        var newClient = factory();
        var old = Interlocked.CompareExchange(ref _client, newClient, null);
        if (old != null)
        {
            // 并发下已有客户端,释放新建的,避免泄漏
            newClient.Dispose();
            return old;
        }
        return newClient;
    }
    #endregion

    #region 加权轮询属性
    /// <summary>权重。用于加权轮询负载均衡,默认1,值越大分配的请求越多</summary>
    public Int32 Weight { get; set; } = 1;

    /// <summary>轮询索引。加权轮询时记录当前轮次的使用次数</summary>
    internal Int32 Index;
    #endregion

    #region 竞速属性
    /// <summary>端点类别。用于竞速时的优先级排序</summary>
    [XmlIgnore, IgnoreDataMember]
    public EndpointCategory Category { get; set; }

    /// <summary>往返耗时。用于竞速时的延迟排序</summary>
    [XmlIgnore, IgnoreDataMember]
    public TimeSpan? Rtt { get; set; }

    /// <summary>最后成功时间</summary>
    [XmlIgnore, IgnoreDataMember]
    public DateTime LastSuccess { get; set; }

    /// <summary>最后失败时间</summary>
    [XmlIgnore, IgnoreDataMember]
    public DateTime LastFailure { get; set; }

    /// <summary>下一次探测时间</summary>
    [XmlIgnore, IgnoreDataMember]
    public DateTime NextProbe { get; set; }

    /// <summary>延迟分数。用于竞速时的启动延迟(毫秒)</summary>
    [XmlIgnore, IgnoreDataMember]
    public Int32 Score { get; set; }
    #endregion

    #region 构造
    /// <summary>实例化服务节点</summary>
    public ServiceEndpoint() { }

    /// <summary>实例化服务节点</summary>
    /// <param name="name">名称</param>
    /// <param name="address">服务地址</param>
    public ServiceEndpoint(String name, Uri address)
    {
        Name = name;
        SetAddress(address);
    }

    /// <summary>实例化服务节点</summary>
    /// <param name="name">名称</param>
    /// <param name="address">服务地址</param>
    public ServiceEndpoint(String name, String address) : this(name, new Uri(address)) { }
    #endregion

    #region 方法
    /// <summary>设置地址。同时生成UriName并设置端点类别</summary>
    /// <param name="uri">地址</param>
    /// <param name="internalAddress">是否内网地址</param>
    public void SetAddress(Uri uri, Boolean? internalAddress = null)
    {
        if (uri == null) return;

        Address = uri;
        UriName = GetUriName(uri);
        if (Name.IsNullOrEmpty()) Name = UriName;

        // 设置端点类别
        internalAddress ??= !Name.IsNullOrEmpty() && (Name.Contains("内网") || Name.Contains("Internal", StringComparison.OrdinalIgnoreCase));
        Category = InferCategory(uri, internalAddress ?? false);
    }

    /// <summary>推断端点类别</summary>
    /// <param name="uri">地址</param>
    /// <param name="internalAddress">是否内网地址</param>
    /// <returns></returns>
    private static EndpointCategory InferCategory(Uri uri, Boolean internalAddress)
    {
        if (!IPAddress.TryParse(uri.Host, out var ip))
            return EndpointCategory.ExternalDomain;

#if NETCOREAPP || NETSTANDARD2_1
        if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4();
#endif

        if (ip.AddressFamily == AddressFamily.InterNetwork)
            return internalAddress ? EndpointCategory.InternalIPv4 : EndpointCategory.ExternalIPv4;

        if (ip.AddressFamily == AddressFamily.InterNetworkV6)
            return internalAddress ? EndpointCategory.InternalIPv6 : EndpointCategory.ExternalIPv6;

        return EndpointCategory.ExternalDomain;
    }

    /// <summary>重置节点状态,清除屏蔽</summary>
    public void Reset()
    {
        NextTime = DateTime.MinValue;
        // 释放旧客户端,避免连接池资源泄漏
        Client?.Dispose();
        Client = null;
        CreateTime = DateTime.MinValue;
    }

    /// <summary>标记节点失败,设置屏蔽期</summary>
    /// <param name="shieldingSeconds">屏蔽时间(秒)</param>
    public void MarkFailure(Int32 shieldingSeconds)
    {
        Errors++;
        // 释放旧客户端,避免连接池资源泄漏
        Client?.Dispose();
        Client = null;
        NextTime = DateTime.Now.AddSeconds(shieldingSeconds);
        CreateTime = DateTime.MinValue;
    }

    /// <summary>检查节点是否可用</summary>
    /// <returns>如果节点未被屏蔽则返回true</returns>
    public Boolean IsAvailable() => NextTime < DateTime.Now;
    #endregion

    #region 辅助
    /// <summary>获取服务基地址名称</summary>
    /// <remarks>
    /// 返回URI的权威部分(scheme + host + port),并去除末尾“/”。
    /// 常用于生成可用于比较/分组的规范化地址,如:"http://127.0.0.1:8080"。
    /// </remarks>
    /// <param name="uri">服务地址</param>
    /// <returns>规范化后的服务基地址名称</returns>
    public static String GetUriName(Uri uri) => uri.GetLeftPart(UriPartial.Authority).TrimEnd('/');

    /// <summary>已重载。友好显示</summary>
    /// <returns></returns>
    public override String ToString() => $"{Name} {Address}";
    #endregion
}