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.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);
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
{
public string UpstreamPathTemplate { get; set; }
public string DownstreamPathTemplate { get; set; }
public string DownstreamScheme { get; set; }
public DownstreamHostAndPort DownstreamHostAndPorts { get; set; }
}
public class MainRouteHandler : IMainRouteHandler
{
private readonly HttpClient _httpClient;
private readonly List<RouteInfo> _routeOptions;
private readonly ILogger _logger;
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;
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();
}
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}");
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));
}
}
private RouteInfo MatchRoute(string requestPath)
{
if (_routeOptions == null) return null;
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);
}
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;
}
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;
}
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;
}
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
{
public int C { get; set; }
public string T { get; set; }
public object D { get; set; }
}
public static class ServiceSetup
{
public static void AddServiceSetup(this IServiceCollection services, IConfiguration configuration)
{
services.AddKestrelServerOptionsSetup();
services.AddCorsSetup();
}
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;
});
}
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;
}
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>