.net6 自定义网关

发布于:2024-11-28 ⋅ 阅读:(19) ⋅ 点赞:(0)
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();

                //设置程序启动端口
                webBuilder.PortConfig(args);
            });
}
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;

        // 配置 NLog
        NLog.LogManager.LoadConfiguration("NLog.config");
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        services.Configure<List<RouteInfo>>(Configuration.GetSection("Routes"));
        services.AddSingleton<IMainRouteHandler, MainRouteHandler>();
        services.AddServiceSetup(Configuration);

        // 添加 NLog
        services.AddLogging(loggingBuilder =>
        {
            loggingBuilder.ClearProviders();
            loggingBuilder.AddNLog();
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.Map("{*path}", async context =>
            {
                var routeHandler = context.RequestServices.GetRequiredService<IMainRouteHandler>();
                await routeHandler.RequestAsync(context);
            });
        });
    }
}
    public interface IMainRouteHandler
    {
        Task RequestAsync(HttpContext context);
    }
public class DownstreamHostAndPort
{
    public string Host { get; set; }
    public int Port { get; set; }
}
public class RouteInfo
{
    /// <summary>
    /// 请求路径匹配模板
    /// </summary>
    public string UpstreamPathTemplate { get; set; }
    /// <summary>
    /// 下游路径模板,如果UpstreamPathTemplate为/{url}时,可以为空
    /// </summary>
    public string DownstreamPathTemplate { get; set; }
    /// <summary>
    /// http  https
    /// </summary>
    public string DownstreamScheme { get; set; }

    /// <summary>
    /// 下游ip和端口号,不能为空
    /// </summary>
    public DownstreamHostAndPort DownstreamHostAndPorts { get; set; }
}
public class MainRouteHandler : IMainRouteHandler
{
    private readonly HttpClient _httpClient;
    private readonly List<RouteInfo> _routeOptions;
    private readonly ILogger _logger;
    /// <summary>
    /// 标识通用路由
    /// </summary>
    private const string UNIVERSALROUTE = "/{url}";

    public MainRouteHandler(HttpClient httpClient, IOptions<List<RouteInfo>> routeOptions, ILogger<MainRouteHandler> logger)
    {
        _httpClient = httpClient;
        _routeOptions = routeOptions.Value;
        _logger = logger;
    }

    public async Task RequestAsync(HttpContext context)
    {
        try
        {
            var requestPath = context.Request.Path.Value;

            // 根据请求路径匹配路由
            var routeInfo = MatchRoute(requestPath);
            if (routeInfo == null)
            {
                var result = new Result()
                {
                    C = 8,
                    T = $"网关未匹配到路由"
                };
                _logger.LogError($"未匹配到路由:{requestPath}");
                context.Response.StatusCode = StatusCodes.Status404NotFound;
                await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(result));
                return;
            }

            // 检查请求协议
            var requestScheme = context.Request.Scheme; // "http" 或 "https"

            // 构建下游URL
            var downstreamUrl = BuildDownstreamUrl(routeInfo, requestPath, requestScheme);

            // 添加查询字符串参数
            var queryString = context.Request.QueryString.Value;
            if (!string.IsNullOrEmpty(queryString))
            {
                downstreamUrl += queryString;
            }

            var requestMessage = new HttpRequestMessage(new HttpMethod(context.Request.Method), downstreamUrl);

            // 复制请求头
            foreach (var header in context.Request.Headers)
            {
                requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
            }

            // 复制请求内容
            if (context.Request.ContentLength > 0)
            {
                requestMessage.Content = new StreamContent(context.Request.Body);
            }

            using (var responseMessage = await _httpClient.SendAsync(requestMessage))
            {
                //响应码
                context.Response.StatusCode = (int)responseMessage.StatusCode;

                //数据类型
                if (responseMessage.Content.Headers.ContentType != null)
                {
                    context.Response.ContentType = responseMessage.Content.Headers.ContentType.ToString();
                }

                //头信息
                foreach (var header in responseMessage.Headers)
                {
                    context.Response.Headers[header.Key] = header.Value.ToArray();
                }

                foreach (var header in responseMessage.Content.Headers)
                {
                    context.Response.Headers[header.Key] = header.Value.ToArray();
                }

                //移除 Transfer-Encoding 头,可以确保客户端能够正确处理和解析从下游服务返回的响应内容
                context.Response.Headers.Remove("Transfer-Encoding");

                await responseMessage.Content.CopyToAsync(context.Response.Body);
                await context.Response.Body.FlushAsync();
            }
        }
        catch (HttpRequestException ex)
        {
            var result = new Result()
            {
                C = 6,
                T = $"Request to downstream service failed: {ex.Message}"
            };
            _logger.LogError(ex, $"RequestImplAsync1 error\r\nMessage:{ex.Message}\r\nStackTrace:{ex.StackTrace}");
            // 处理 HTTP 请求异常
            context.Response.StatusCode = StatusCodes.Status502BadGateway;
            await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(result));
        }
        catch (Exception ex)
        {
            var result = new Result()
            {
                C = 7,
                T = $"An unexpected error occurred: {ex.Message}"
            };
            _logger.LogError(ex, $"RequestImplAsync2 error\r\nMessage:{ex.Message}\r\nStackTrace:{ex.StackTrace}");
            // 处理其他异常
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(result));
        }
    }

    /// <summary>
    /// 路由匹配
    /// </summary>
    /// <param name="requestPath"></param>
    /// <returns></returns>
    private RouteInfo MatchRoute(string requestPath)
    {
        if (_routeOptions == null) return null;

        //下游ip和端口不能为空
         var validRouteOptions = _routeOptions.Where(o =>
            o.DownstreamHostAndPorts != null 
            && (!string.IsNullOrWhiteSpace(o.DownstreamHostAndPorts.Host))
            && o.DownstreamHostAndPorts.Port > 0).ToList();

        var requestSegments = requestPath.Split('/', StringSplitOptions.RemoveEmptyEntries);

        //有上游模板的数据
        var searchRoutes = validRouteOptions.Where(route => (!string.IsNullOrWhiteSpace(route.UpstreamPathTemplate)) && route.UpstreamPathTemplate != UNIVERSALROUTE);

        // 先进行精确匹配
        foreach (var route in searchRoutes)
        {
            var templateSegments = route.UpstreamPathTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries);

            if (IsExactMatch(templateSegments, requestSegments))
            {
                return route;
            }
        }

        // 再进行模糊匹配,按权重排序
        var fuzzyMatches = searchRoutes
            .Where(route => IsFuzzyMatch(route.UpstreamPathTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries), requestSegments))
            .OrderByDescending(route => GetFuzzyMatchWeight(route.UpstreamPathTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries)))
            .ToList();

        //找到权重最大的一条
        var result = fuzzyMatches.FirstOrDefault();
        if (result != null) return result;

        //都没有找到,找通用模板
        return validRouteOptions.FirstOrDefault(o => o.UpstreamPathTemplate == UNIVERSALROUTE);
    }

    /// <summary>
    /// 精确匹配
    /// </summary>
    /// <param name="templateSegments"></param>
    /// <param name="requestSegments"></param>
    /// <returns></returns>
    private bool IsExactMatch(string[] templateSegments, string[] requestSegments)
    {
        if (templateSegments.Length != requestSegments.Length)
        {
            return false;
        }

        for (int i = 0; i < templateSegments.Length; i++)
        {
            if (templateSegments[i].StartsWith("{") && templateSegments[i].EndsWith("}"))
            {
                return false; // 精确匹配不允许模糊匹配段
            }

            if (!templateSegments[i].Equals(requestSegments[i], StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// 模糊匹配
    /// </summary>
    /// <param name="templateSegments"></param>
    /// <param name="requestSegments"></param>
    /// <returns></returns>
    private bool IsFuzzyMatch(string[] templateSegments, string[] requestSegments)
    {
        if (templateSegments.Length != requestSegments.Length)
        {
            return false;
        }

        for (int i = 0; i < templateSegments.Length; i++)
        {
            if (templateSegments[i].StartsWith("{") && templateSegments[i].EndsWith("}"))
            {
                continue; // 模糊匹配
            }

            if (!templateSegments[i].Equals(requestSegments[i], StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// 获取权重
    /// </summary>
    /// <param name="templateSegments"></param>
    /// <returns></returns>
    private int GetFuzzyMatchWeight(string[] templateSegments)
    {
        int weight = 0;
        for (int i = 0; i < templateSegments.Length; i++)
        {
            if (!(templateSegments[i].StartsWith("{") && templateSegments[i].EndsWith("}")))
            {
                weight += i; // 越靠后的精确匹配段权重越大
            }
        }
        return weight;
    }

    /// <summary>
    /// 拼接下游路径
    /// </summary>
    /// <param name="routeInfo"></param>
    /// <param name="requestPath"></param>
    /// <param name="requestScheme"></param>
    /// <returns></returns>
    private string BuildDownstreamUrl(RouteInfo routeInfo, string requestPath, string requestScheme)
    {
        string downstreamPath;
        var requestSegments = requestPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (routeInfo.UpstreamPathTemplate == UNIVERSALROUTE || string.IsNullOrWhiteSpace(routeInfo.DownstreamPathTemplate))
        {
            downstreamPath = string.Join('/', requestSegments);
        }
        else
        {
            var downstreamSegments = routeInfo.DownstreamPathTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries);
            //根据上游的位置,找到下游的位置
            var templateSegments = routeInfo.UpstreamPathTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < downstreamSegments.Length; i++)
            {
                if (downstreamSegments[i].StartsWith("{") && downstreamSegments[i].EndsWith("}"))
                {
                    var index = Array.FindIndex(templateSegments, o => o == downstreamSegments[i]);
                    if (index >= 0 && index < requestSegments.Length)
                    {
                        downstreamSegments[i] = requestSegments[index];
                    }
                    else
                    {
                        //匹配不到,返回请求路径
                        downstreamSegments = requestSegments;
                        break;
                    }
                }
            }

            downstreamPath = string.Join('/', downstreamSegments);
        }

        return $"{(string.IsNullOrWhiteSpace(routeInfo.DownstreamScheme) ? requestScheme : routeInfo.DownstreamScheme)}://{routeInfo.DownstreamHostAndPorts.Host}:{routeInfo.DownstreamHostAndPorts.Port}/{downstreamPath}";
    }
}
    public class Result
    {
        /// <summary>
        /// 消息编码0成功
        /// </summary>
        public int C { get; set; }

        /// <summary>
        /// 消息文本
        /// </summary>
        public string T { get; set; }

        /// <summary>
        /// 函数调用返回的数据
        /// </summary>
        public object D { get; set; }
    }
public static class ServiceSetup
{
    public static void AddServiceSetup(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddKestrelServerOptionsSetup();                                      //Kestrel配置
        services.AddCorsSetup();                                                      //跨域配置
    }

    /// <summary>
    /// KestrelServerOptions配置
    /// </summary>
    /// <param name="services"></param>
    public static void AddKestrelServerOptionsSetup(this IServiceCollection services)
    {
        services.Configure<KestrelServerOptions>(options =>
        {
            options.AllowSynchronousIO = true;
        }).Configure<IISServerOptions>(options => {
            options.AllowSynchronousIO = true;
        }).Configure<FormOptions>(options =>
        {
            options.ValueLengthLimit = int.MaxValue;
            options.MultipartBodyLengthLimit = int.MaxValue;
        });
    }

    /// <summary>
    /// 端口配置
    /// </summary>
    /// <param name="webBuilder"></param>
    /// <param name="args"></param>
    public static IWebHostBuilder PortConfig(this IWebHostBuilder webBuilder, string[] args)
    {
        // 读取端口号配置
        var config = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .Build();

        var port = config.GetValue<int?>("Kestrel:Endpoint:Http:Port") ??
                   args.Select(arg => arg.Split('='))
                       .Where(arg => arg.Length == 2 && arg[0].Equals("port", StringComparison.OrdinalIgnoreCase))
                       .Select(arg => int.TryParse(arg[1], out var p) ? p : (int?)null)
                       .FirstOrDefault() ?? 9000;

        webBuilder.UseUrls($"http://*:{port}");

        return webBuilder;
    }

    /// <summary>
    /// 跨域配置
    /// </summary>
    /// <param name="services"></param>
    public static void AddCorsSetup(this IServiceCollection services)
    {
        services.AddCors(options => options.AddPolicy("CorsPolicy",
          builder =>
          {
              builder.AllowAnyMethod()
                  .AllowAnyHeader()
                  .SetIsOriginAllowed(_ => true)
                  .AllowCredentials();
          }));
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >

<targets async="true">
    <target name="console" xsi:type="ColoredConsole" 
           layout="${date:format=HH\:mm\:ss.fff}|${level}|${stacktrace}|${message}"/>
    <target name="file" xsi:type="File" fileName="${basedir}/log/app_${date:yyyyMMdd_HH}.log" 
            layout="[${date:format=yyyy-MM-dd HH\:mm\:ss.fff}][${level}]《${message}》《${exception}》"/>
</targets>
<rules>
    <logger name="*" minlevel="trace" writeTo="console"></logger>
    <logger name="*" minlevel="trace" writeTo="file"></logger>
</rules>
</nlog>

{
  "Kestrel": {
    "Endpoint": {
      "Http": {
        "Port": 9000
      }
    }
  },
  "Routes": [
    {
      "UpstreamPathTemplate": "/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": {
        "Host": "192.168.195.100",
        "Port": 8080
      }
    },
    {
      "UpstreamPathTemplate": "/{application}/{webapi}/{control}/{action}",
      "DownstreamScheme": "http",
      "DownstreamPathTemplate": "/application/{webapi}/{control}/{action}",
      "DownstreamHostAndPorts": {
        "Host": "192.168.195.100",
        "Port": 8080
      }
    },
    {
      "UpstreamPathTemplate": "/api/{control}/{action}",
      "DownstreamScheme": "http",
      "DownstreamPathTemplate": "/application/webapi/{control}/{action}",
      "DownstreamHostAndPorts": {
        "Host": "192.168.195.100",
        "Port": 8080
      }
    }
  ]
}
网关说明:
  UpstreamPathTemplate:上游url匹配模板,用于匹配上游的url。大括号包裹时(参数匹配),意味着忽略比较当前层级路径
  DownstreamScheme:转发协议,http或https
  DownstreamPathTemplate:下游的url匹配模板,用于组装下游url。大括号包裹时(参数匹配),将从UpstreamPathTemplate中找到相同大括号的字符所在层级,与上游url的层级一致的字符进行替换
  DownstreamHostAndPorts:下游的ip和端口号

举例:
  上游url:/application/webapi/ERPDataDict/GetDict
  能匹配路由规则,权重依次递减(多个路由匹配到时,取最高权重路由):
    /application/webapi/ERPDataDict/GetDict
    /application/webapi/{control}/GetDict
    /application/webapi/ERPDataDict/{action}
    /application/webapi/{control}/{action}
    /{application}/webapi/{control}/{action}
    /application/{webapi}/{control}/{action}
    /{application}/{webapi}/{control}/{action}
    /{url}

规则说明:
1、UpstreamPathTemplate 为/{url},标识万能匹配。请求路径与其他路由都无法匹配时,将会走这个路由
2、UpstreamPathTemplate 不是万能匹配时,层级需要与上游路径层级一致
3、DownstreamPathTemplate 参数匹配时,参数需要在UpstreamPathTemplate中存在;
4、DownstreamHostAndPorts 是转发的ip和端口号,配置路由时,必须配置
5、当满足多个路由匹配时,取最高权重路由。权重规则是,最末级节点权重最高,依次递减
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
	<Nullable>enable</Nullable>
	<ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
	<PackageReference Include="NLog.Extensions.Logging" Version="5.3.4" />
	<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.4" />
  </ItemGroup>

</Project>