IdentityServer里有各种Endpoint,如TokenEndpoint,UserInfoEndpoint,Authorize Endpoint,Discovery Endpoint等等。Endpoint从字面意思来看是“终端节点"或者“终节点”的意思。无独有偶NetCore的路由也有Endpoint的概念。那么我们提出一个问题来,究竟什么是Endpoint?

先来看一段代码,如下所示:

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureServices(svcs => svcs.AddRouting()).Configure( app => app.UseRouting().UseEndpoints(
//test1
//endpoints => endpoints.MapGet("weather/{city}/{days}", //test2
endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast) ));
}); private static Dictionary<string, string> _cities = new Dictionary<string, string>() {
["001"]="Beijing",
["028"]="Chengdu",
["091"]="Suzhou" }; //RequestDelegate
public static async Task WeatherForecast(HttpContext context)
{
var city = (string)context.GetRouteData().Values["city"];
city = _cities[city]; //test1
//int days=int.Parse(context.GetRouteData().Values["days"].ToString());
//var report = new WeatherReport(city, days); //test2
int year = int.Parse(context.GetRouteData().Values["year"].ToString());
int month= int.Parse(context.GetRouteData().Values["month"].ToString());
int day=int.Parse(context.GetRouteData().Values["day"].ToString());
var report = new WeatherReport(city, new DateTime(year, month, day));
await RenderWeatherAsync(context, report);
} private static async Task RenderWeatherAsync(HttpContext context,WeatherReport report)
{
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync("<html><head><title>Weather</title></head><body>");
await context.Response.WriteAsync($"<h3>{report.City}</h3>");
foreach(var it in report.WeatherInfos)
{
await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}: ");
await context.Response.WriteAsync($"{it.Value.LowTemperature}--{it.Value.HighTemperature} <br/>");
}
await context.Response.WriteAsync("</body></html>");
}
}

上面的代码很清晰,如果路由格式匹配”weather/{city}/{year}.{month}.{day}”,那么就调用WeatherForecast方法,这里的app.UseRouting().UseEndpoints就用到了Endpoint模式。要了解Endpoint,我们需要抽丝剥茧一步一步了解几个类的方法:

1.我们先看最里面的endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast),这里实际上调用的是EndpointRouteBuilderExtensions类的MapGet方法,代码如下所示:

    /// <summary>
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add endpoints.
/// </summary>
public static class EndpointRouteBuilderExtensions
{
// Avoid creating a new array every call
private static readonly string[] GetVerb = new[] { "GET" };
private static readonly string[] PostVerb = new[] { "POST" };
private static readonly string[] PutVerb = new[] { "PUT" };
private static readonly string[] DeleteVerb = new[] { "DELETE" };
private static readonly string[] PatchVerb = new[] { "PATCH" };
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapGet(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, GetVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapPost(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, PostVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapPut(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, PutVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapDelete(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, DeleteVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PATCH requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapPatch(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, PatchVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified HTTP methods and pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMethods(
this IEndpointRouteBuilder endpoints,
string pattern,
IEnumerable<string> httpMethods,
RequestDelegate requestDelegate)
{
if (httpMethods == null)
{
throw new ArgumentNullException(nameof(httpMethods));
} var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate);
builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}");
builder.WithMetadata(new HttpMethodMetadata(httpMethods));
return builder;
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return Map(endpoints, RoutePatternFactory.Parse(pattern), requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
RequestDelegate requestDelegate)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
} if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
} const int defaultOrder = 0; //实例化RouteEndpointBuilder
var builder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; // Add delegate attributes as metadata
var attributes = requestDelegate.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes != null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} //Adds a RouteEndpoint to the IEndpointRouteBuilder that matches HTTP requests
/// for the specified pattern.
///可以理解为:将某种格式的路由pattern添加至IEndpointRouteBuilder的DataSource属性,为match做准备
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource == null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
} return dataSource.AddEndpointBuilder(builder);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapGet(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, GetVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapPost(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, PostVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapPut(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, PutVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapDelete(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, DeleteVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PATCH requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The <see cref="Delegate" /> executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapPatch(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, PatchVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified HTTP methods and pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapMethods(
this IEndpointRouteBuilder endpoints,
string pattern,
IEnumerable<string> httpMethods,
Delegate handler)
{
if (httpMethods is null)
{
throw new ArgumentNullException(nameof(httpMethods));
} var disableInferredBody = false;
foreach (var method in httpMethods)
{
disableInferredBody = ShouldDisableInferredBody(method);
if (disableInferredBody is true)
{
break;
}
} var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody);
// Prepends the HTTP method to the DisplayName produced with pattern + method name
builder.Add(b => b.DisplayName = $"HTTP: {string.Join(", ", httpMethods)} {b.DisplayName}");
builder.WithMetadata(new HttpMethodMetadata(httpMethods));
return builder; static bool ShouldDisableInferredBody(string method)
{
// GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies
return method.Equals(HttpMethods.Get, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Delete, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Head, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Options, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Trace, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Connect, StringComparison.Ordinal);
}
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return Map(endpoints, RoutePatternFactory.Parse(pattern), handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate handler)
{
return Map(endpoints, pattern, handler, disableInferBodyFromParameters: false);
} /// <summary>
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
/// requests for non-file-names with the lowest possible priority.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
/// <remarks>
/// <para>
/// <see cref="MapFallback(IEndpointRouteBuilder, Delegate)"/> is intended to handle cases where URL path of
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
/// result in an HTTP 404.
/// </para>
/// <para>
/// <see cref="MapFallback(IEndpointRouteBuilder, Delegate)"/> registers an endpoint using the pattern
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
/// </para>
/// </remarks>
public static RouteHandlerBuilder MapFallback(this IEndpointRouteBuilder endpoints, Delegate handler)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
} return endpoints.MapFallback("{*path:nonfile}", handler);
} /// <summary>
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
/// the provided pattern with the lowest possible priority.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
/// <remarks>
/// <para>
/// <see cref="MapFallback(IEndpointRouteBuilder, string, Delegate)"/> is intended to handle cases where no
/// other endpoint has matched. This is convenient for routing requests to a SPA framework.
/// </para>
/// <para>
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
/// </para>
/// <para>
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route constraint
/// to exclude requests for static files.
/// </para>
/// </remarks>
public static RouteHandlerBuilder MapFallback(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
} if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
} var conventionBuilder = endpoints.Map(pattern, handler);
conventionBuilder.WithDisplayName("Fallback " + pattern);
conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue);
return conventionBuilder;
} private static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate handler,
bool disableInferBodyFromParameters)
{
if (endpoints is null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern is null)
{
throw new ArgumentNullException(nameof(pattern));
} if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
} const int defaultOrder = 0; var routeParams = new List<string>(pattern.Parameters.Count);
foreach (var part in pattern.Parameters)
{
routeParams.Add(part.Name);
} var routeHandlerOptions = endpoints.ServiceProvider?.GetService<IOptions<RouteHandlerOptions>>(); var options = new RequestDelegateFactoryOptions
{
ServiceProvider = endpoints.ServiceProvider,
RouteParameterNames = routeParams,
ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false,
DisableInferBodyFromParameters = disableInferBodyFromParameters,
}; var requestDelegateResult = RequestDelegateFactory.Create(handler, options); var builder = new RouteEndpointBuilder(
requestDelegateResult.RequestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are
// explicit about the MethodInfo representing the "handler" and not the RequestDelegate? // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint.
builder.Metadata.Add(handler.Method); // Methods defined in a top-level program are generated as statics so the delegate
// target will be null. Inline lambdas are compiler generated method so they can
// be filtered that way.
if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName)
|| !TypeHelper.IsCompilerGeneratedMethod(handler.Method))
{
endpointName ??= handler.Method.Name;
builder.DisplayName = $"{builder.DisplayName} => {endpointName}";
} // Add delegate attributes as metadata
var attributes = handler.Method.GetCustomAttributes(); // Add add request delegate metadata
foreach (var metadata in requestDelegateResult.EndpointMetadata)
{
builder.Metadata.Add(metadata);
} // This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes is not null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource is null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
} return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder));
}
}
}

在这里我们重点看看下面签名的方法:

 /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
RequestDelegate requestDelegate)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
} if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
} const int defaultOrder = 0; //实例化RouteEndpointBuilder
var builder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; // Add delegate attributes as metadata
var attributes = requestDelegate.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes != null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} //Adds a RouteEndpoint to the IEndpointRouteBuilder that matches HTTP requests
/// for the specified pattern.
///可以理解为:将某种格式的路由pattern添加至IEndpointRouteBuilder的DataSource属性,为match做准备
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource == null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
} return dataSource.AddEndpointBuilder(builder);
}

在这个方法里有一个实例化RouteEndpointBuilder的语句:

  var builder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
};

其中RouteEndpointBuilder从字面意思看得出,就是RouteEndpoint的创建者,我们看下它的代码:

 public sealed class RouteEndpointBuilder : EndpointBuilder
{
/// <summary>
/// Gets or sets the <see cref="RoutePattern"/> associated with this endpoint.
/// </summary>
public RoutePattern RoutePattern { get; set; } /// <summary>
/// Gets or sets the order assigned to the endpoint.
/// </summary>
public int Order { get; set; } /// <summary>
/// Constructs a new <see cref="RouteEndpointBuilder"/> instance.
/// </summary>
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
/// <param name="routePattern">The <see cref="RoutePattern"/> to use in URL matching.</param>
/// <param name="order">The order assigned to the endpoint.</param>
public RouteEndpointBuilder(
RequestDelegate requestDelegate,
RoutePattern routePattern,
int order)
{
RequestDelegate = requestDelegate;
RoutePattern = routePattern;
Order = order;
} //生成Route
/// <inheritdoc />
public override Endpoint Build()
{
if (RequestDelegate is null)
{
throw new InvalidOperationException($"{nameof(RequestDelegate)} must be specified to construct a {nameof(RouteEndpoint)}.");
} var routeEndpoint = new RouteEndpoint(
RequestDelegate,
RoutePattern,
Order,
new EndpointMetadataCollection(Metadata),
DisplayName); return routeEndpoint;
}
}

再来看下Endpoint的代码:

   /// <summary>
/// Represents a logical endpoint in an application.
/// </summary>
public class Endpoint
{
/// <summary>
/// Creates a new instance of <see cref="Endpoint"/>.
/// </summary>
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
/// <param name="metadata">
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
/// </param>
/// <param name="displayName">
/// The informational display name of the endpoint. May be null.
/// </param>
public Endpoint(
RequestDelegate? requestDelegate,
EndpointMetadataCollection? metadata,
string? displayName)
{
// All are allowed to be null
RequestDelegate = requestDelegate;
Metadata = metadata ?? EndpointMetadataCollection.Empty;
DisplayName = displayName;
} /// <summary>
/// Gets the informational display name of this endpoint.
/// </summary>
public string? DisplayName { get; } /// <summary>
/// Gets the collection of metadata associated with this endpoint.
/// </summary>
public EndpointMetadataCollection Metadata { get; } /// <summary>
/// Gets the delegate used to process requests for the endpoint.
/// </summary>
public RequestDelegate? RequestDelegate { get; } /// <summary>
/// Returns a string representation of the endpoint.
/// </summary>
public override string? ToString() => DisplayName ?? base.ToString();
}

从Build()方法看得出,它利用路由匹配的RoutePattern,生成一个RouteEndpoint实例。RouteEndpoint叫做路由终点,继承自Endpoint。Endpoint有一个重要的RequestDelegate属性,用来处理当前的请求。看到这里,我们开始推断:所谓的Endpoint,无非就是用要匹配的pattern,构造一个RouteEndpoint的过程,其中RouteEndpoint继承自Endpoint。NetCore无非就是利用这个RouteEndpoint来匹配当前的Url,如果匹配得上,就执行RequestDelegate所代表的方法,上文就是WeatherForecast方法,如果匹配不上,则不执行WeatherForecast方法,仅此而已。

2.为了验证,我们再来看 app.UseRouting().UseEndpoints(),实际上调用的是EndpointRoutingApplicationBuilderExtensions类的两个方法:

   public static class EndpointRoutingApplicationBuilderExtensions
{
private const string EndpointRouteBuilder = "__EndpointRouteBuilder";
private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; /// <summary>
/// Adds a <see cref="EndpointRoutingMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <remarks>
/// <para>
/// A call to <see cref="UseRouting(IApplicationBuilder)"/> must be followed by a call to
/// <see cref="UseEndpoints(IApplicationBuilder, Action{IEndpointRouteBuilder})"/> for the same <see cref="IApplicationBuilder"/>
/// instance.
/// </para>
/// <para>
/// The <see cref="EndpointRoutingMiddleware"/> defines a point in the middleware pipeline where routing decisions are
/// made, and an <see cref="Endpoint"/> is associated with the <see cref="HttpContext"/>. The <see cref="EndpointMiddleware"/>
/// defines a point in the middleware pipeline where the current <see cref="Endpoint"/> is executed. Middleware between
/// the <see cref="EndpointRoutingMiddleware"/> and <see cref="EndpointMiddleware"/> may observe or change the
/// <see cref="Endpoint"/> associated with the <see cref="HttpContext"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseRouting(this IApplicationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
} VerifyRoutingServicesAreRegistered(builder); IEndpointRouteBuilder endpointRouteBuilder;
if (builder.Properties.TryGetValue(GlobalEndpointRouteBuilderKey, out var obj))
{
endpointRouteBuilder = (IEndpointRouteBuilder)obj!;
// Let interested parties know if UseRouting() was called while a global route builder was set
builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
}
else
{
endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
} //先交由EndpointRoutingMiddleware中间件匹配Endpoint,然后交由下面的EndpointMiddleware处理
return builder.UseMiddleware<EndpointRoutingMiddleware>(endpointRouteBuilder);
} /// <summary>
/// Adds a <see cref="EndpointMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>
/// with the <see cref="EndpointDataSource"/> instances built from configured <see cref="IEndpointRouteBuilder"/>.
/// The <see cref="EndpointMiddleware"/> will execute the <see cref="Endpoint"/> associated with the current
/// request.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <param name="configure">An <see cref="Action{IEndpointRouteBuilder}"/> to configure the provided <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <remarks>
/// <para>
/// A call to <see cref="UseEndpoints(IApplicationBuilder, Action{IEndpointRouteBuilder})"/> must be preceded by a call to
/// <see cref="UseRouting(IApplicationBuilder)"/> for the same <see cref="IApplicationBuilder"/>
/// instance.
/// </para>
/// <para>
/// The <see cref="EndpointRoutingMiddleware"/> defines a point in the middleware pipeline where routing decisions are
/// made, and an <see cref="Endpoint"/> is associated with the <see cref="HttpContext"/>. The <see cref="EndpointMiddleware"/>
/// defines a point in the middleware pipeline where the current <see cref="Endpoint"/> is executed. Middleware between
/// the <see cref="EndpointRoutingMiddleware"/> and <see cref="EndpointMiddleware"/> may observe or change the
/// <see cref="Endpoint"/> associated with the <see cref="HttpContext"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
} if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
} VerifyRoutingServicesAreRegistered(builder); VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder); configure(endpointRouteBuilder); // Yes, this mutates an IOptions. We're registering data sources in a global collection which
// can be used for discovery of endpoints or URL generation.
//
// Each middleware gets its own collection of data sources, and all of those data sources also
// get added to a global collection.
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
foreach (var dataSource in endpointRouteBuilder.DataSources)
{
if (!routeOptions.Value.EndpointDataSources.Contains(dataSource))
{
routeOptions.Value.EndpointDataSources.Add(dataSource);
}
} //交由EndpointMiddleware处理
return builder.UseMiddleware<EndpointMiddleware>();
} private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app)
{
// Verify if AddRouting was done before calling UseEndpointRouting/UseEndpoint
// We use the RoutingMarkerService to make sure if all the services were added.
if (app.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null)
{
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
nameof(IServiceCollection),
nameof(RoutingServiceCollectionExtensions.AddRouting),
"ConfigureServices(...)"));
}
} private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder)
{
if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj))
{
var message =
$"{nameof(EndpointRoutingMiddleware)} matches endpoints setup by {nameof(EndpointMiddleware)} and so must be added to the request " +
$"execution pipeline before {nameof(EndpointMiddleware)}. " +
$"Please add {nameof(EndpointRoutingMiddleware)} by calling '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' inside the call " +
$"to 'Configure(...)' in the application startup code.";
throw new InvalidOperationException(message);
} endpointRouteBuilder = (IEndpointRouteBuilder)obj!; // This check handles the case where Map or something else that forks the pipeline is called between the two
// routing middleware.
if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder))
{
var message =
$"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " +
$"To use Endpoint Routing with 'Map(...)', make sure to call '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' before " +
$"'{nameof(IApplicationBuilder)}.{nameof(UseEndpoints)}' for each branch of the middleware pipeline.";
throw new InvalidOperationException(message);
}
}
}

2-1首先看UseRouting()方法,这个方法其实就是调用EndpointRoutingMiddleware中间件进行Endpoint的匹配,我们可以看下它的几个方法:

internal sealed partial class EndpointRoutingMiddleware
{
private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched"; private readonly MatcherFactory _matcherFactory;
private readonly ILogger _logger;
private readonly EndpointDataSource _endpointDataSource;
private readonly DiagnosticListener _diagnosticListener;
private readonly RequestDelegate _next; private Task<Matcher>? _initializationTask; public EndpointRoutingMiddleware(
MatcherFactory matcherFactory,
ILogger<EndpointRoutingMiddleware> logger,
IEndpointRouteBuilder endpointRouteBuilder,
DiagnosticListener diagnosticListener,
RequestDelegate next)
{
if (endpointRouteBuilder == null)
{
throw new ArgumentNullException(nameof(endpointRouteBuilder));
} _matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
_next = next ?? throw new ArgumentNullException(nameof(next)); _endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
} public Task Invoke(HttpContext httpContext)
{
// There's already an endpoint, skip matching completely
var endpoint = httpContext.GetEndpoint();
if (endpoint != null)
{
Log.MatchSkipped(_logger, endpoint);
return _next(httpContext);
} // There's an inherent race condition between waiting for init and accessing the matcher
// this is OK because once `_matcher` is initialized, it will not be set to null again.
var matcherTask = InitializeAsync();
if (!matcherTask.IsCompletedSuccessfully)
{
return AwaitMatcher(this, httpContext, matcherTask);
} var matchTask = matcherTask.Result.MatchAsync(httpContext);
if (!matchTask.IsCompletedSuccessfully)
{
return AwaitMatch(this, httpContext, matchTask);
} return SetRoutingAndContinue(httpContext); // Awaited fallbacks for when the Tasks do not synchronously complete
static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task<Matcher> matcherTask)
{
var matcher = await matcherTask;
//根据httpContext进行匹配
await matcher.MatchAsync(httpContext);
await middleware.SetRoutingAndContinue(httpContext);
} static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
{
await matchTask;
await middleware.SetRoutingAndContinue(httpContext);
} } [MethodImpl(MethodImplOptions.AggressiveInlining)]
private Task SetRoutingAndContinue(HttpContext httpContext)
{
// If there was no mutation of the endpoint then log failure
var endpoint = httpContext.GetEndpoint();
if (endpoint == null)
{
Log.MatchFailure(_logger);
}
else
{
// Raise an event if the route matched
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey))
{
// We're just going to send the HttpContext since it has all of the relevant information
_diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext);
} Log.MatchSuccess(_logger, endpoint);
} return _next(httpContext);
} // Initialization is async to avoid blocking threads while reflection and things
// of that nature take place.
//
// We've seen cases where startup is very slow if we allow multiple threads to race
// while initializing the set of endpoints/routes. Doing CPU intensive work is a
// blocking operation if you have a low core count and enough work to do.
private Task<Matcher> InitializeAsync()
{
var initializationTask = _initializationTask;
if (initializationTask != null)
{
return initializationTask;
} return InitializeCoreAsync();
} private Task<Matcher> InitializeCoreAsync()
{
var initialization = new TaskCompletionSource<Matcher>(TaskCreationOptions.RunContinuationsAsynchronously);
var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null);
if (initializationTask != null)
{
// This thread lost the race, join the existing task.
return initializationTask;
} // This thread won the race, do the initialization.
try
{
//用EndpointDataSource初始化matcher
//EndpointDataSource来源于IEndpointRouteBuilder的DataSource属性
//见EndpointRouteBuilderExtensions类的201行
var matcher = _matcherFactory.CreateMatcher(_endpointDataSource); _initializationTask = Task.FromResult(matcher); // Complete the task, this will unblock any requests that came in while initializing.
initialization.SetResult(matcher);
return initialization.Task;
}
catch (Exception ex)
{
// Allow initialization to occur again. Since DataSources can change, it's possible
// for the developer to correct the data causing the failure.
_initializationTask = null; // Complete the task, this will throw for any requests that came in while initializing.
initialization.SetException(ex);
return initialization.Task;
}
} private static partial class Log
{
public static void MatchSuccess(ILogger logger, Endpoint endpoint)
=> MatchSuccess(logger, endpoint.DisplayName); [LoggerMessage(1, LogLevel.Debug, "Request matched endpoint '{EndpointName}'", EventName = "MatchSuccess")]
private static partial void MatchSuccess(ILogger logger, string? endpointName); [LoggerMessage(2, LogLevel.Debug, "Request did not match any endpoints", EventName = "MatchFailure")]
public static partial void MatchFailure(ILogger logger); public static void MatchSkipped(ILogger logger, Endpoint endpoint)
=> MatchingSkipped(logger, endpoint.DisplayName); [LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")]
private static partial void MatchingSkipped(ILogger logger, string? endpointName);
}
}

从Invoke方法看得出来,它根据当前的HttpContext进行Endpoint的匹配,如果当前的HttpContext路由格式匹配成功,那么将当前HttpContext传递给下一个中间件处理,这个从SetRoutingAndContinue方法看得出来。

2-2其次看下UseEndpoints()方法,这个方法就是调用EndpointMiddleware中间件,对上面匹配成功的HttpContext进行处理,并调用HttpContext的EndPoint的RequestDelegate处理当前请求。我们重点看下它的Invoke方法,重点关注var requestTask = endpoint.RequestDelegate(httpContext):

  public Task Invoke(HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
if (endpoint?.RequestDelegate != null)
{
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
!httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
{
ThrowMissingAuthMiddlewareException(endpoint);
} if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
!httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
{
ThrowMissingCorsMiddlewareException(endpoint);
}
} Log.ExecutingEndpoint(_logger, endpoint); try
{
//调用HttpContext的Endpoint的RequestDelegate方法处理当前请求
var requestTask = endpoint.RequestDelegate(httpContext);
if (!requestTask.IsCompletedSuccessfully)
{
return AwaitRequestTask(endpoint, requestTask, _logger);
}
}
catch (Exception exception)
{
Log.ExecutedEndpoint(_logger, endpoint);
return Task.FromException(exception);
} Log.ExecutedEndpoint(_logger, endpoint);
return Task.CompletedTask;
} return _next(httpContext); static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
{
try
{
await requestTask;
}
finally
{
Log.ExecutedEndpoint(logger, endpoint);
}
}
}

总结:从上面的分析,我们粗略的了解了netcore路由的Endpoint模式其实就是一种用匹配模式构建的终端节点,它主要用来对HttpContext进行路由的匹配,如果匹配成功,则执行Endpoint上的RequestDelegate方法。

NetCore路由的Endpoint模式的更多相关文章

  1. 前端路由两种模式:hash、history

    随着 ajax 的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是spa的兴起,前端路由系统随之开始流行. 从用户的角度看,前端路由主要实现了两个功能(使用ajax更新页面状态的情况下): 记 ...

  2. 【RabbitMQ】4、三种Exchange模式——订阅、路由、通配符模式

    前两篇博客介绍了两种队列模式,这篇博客介绍订阅.路由和通配符模式,之所以放在一起介绍,是因为这三种模式都是用了Exchange交换机,消息没有直接发送到队列,而是发送到了交换机,经过队列绑定交换机到达 ...

  3. 如何去除vue项目中的 # — vue路由的History模式

    前言 在创建的 router 对象中,如果不配置 mode,就会使用默认的 hash 模式,该模式下会将路径格式化为 #! 开头. 添加 mode: 'history' 之后将使用 HTML5 his ...

  4. Vue 的路由实现 Hash模式 和 History模式

    Hash 模式: Hash 模式的工作原理是onhashchange事件,Window对象可以监听这个事件... 可以通过改变路径的哈希值,来实现历史记录的保存,发生变化的hash 都会被浏览器给保存 ...

  5. 数据源管理 | 主从库动态路由,AOP模式读写分离

    本文源码:GitHub·点这里 || GitEE·点这里 一.多数据源应用 1.基础描述 在相对复杂的应用服务中,配置多个数据源是常见现象,例如常见的:配置主从数据库用来写数据,再配置一个从库读数据, ...

  6. Vue路由配置history模式

    我的博客: https://github.com/Daotin/fe-notes/issues vue需要node.js吗? 你可以用 script 标签的形式引入vue.min.js 这样的,不需要 ...

  7. Vue路由的hash模式与history模式的区别?

    1.首先router有两种模式:hash模式(默认).history模式(需配置mode: 'history') hash和history的区别?   hash                    ...

  8. Vue2+Koa2+Typescript前后端框架教程--03后端路由和三层模式配置

    昨天将Koa2的基础框架和自动编译调试重启服务完成,今天开始配置路由和搭建基础的三层架构模式. 路由中间件:koa-router,即路由导航,就是我们平时使用最广泛的get/post方法执行的URL路 ...

  9. .net core i上 K8S(五).netcore程序的hostip模式

    上一章讲了pod的管理,今天再分享一个pod的访问方式 1.Pod的HostIP模式 Pod的HostIP模式,可以通过宿主机访问pod内的服务,创建yaml文件如下 apiVersion: v1 k ...

随机推荐

  1. iTextSharp 提取签名图像

    原文 本文使用 iTextSharp 5.5.13.2,记录使用 iTextSharp 提取图片时,获得的知识点. pdf 中的签名并不是单纯的一张图片,它是由一张基础的底色图和一张蒙版图片组成.需要 ...

  2. python基础与数据类型(int, float, str, list)

    目录 python多版本共存 在cmd窗口进入不同版本的python环境 在pycharm中切换不同的版本 python语法之注释 python变量与常量 变量 变量的本质 变量的命名规范 常量 py ...

  3. 『忘了再学』Shell基础 — 19、使用declare命令声明变量类型

    目录 1.declare命令介绍 2.声明数组变量类型 3.声明变量为环境变量 4.声明只读属性 5.补充: 1.declare命令介绍 Shell中所有变量的默认类型是字符串类型,如果你需要进行特殊 ...

  4. [算法学习] 换根dp

    换根dp 一般来说,我们做题的树都是默认 \(1\) 为根的.但是有些题目需要计算以每个节点为根时的内容. 朴素的暴力:以每个点 \(u\) 作为 \(root\) 暴力dfs下去,复杂度\(O(n^ ...

  5. 如何用HMS Core位置和地图服务实现附近地点路径规划功能

    日常出行中,路径规划是很重要的部分.用户想要去往某个地点,获取到该地点的所有路径,再根据预估出行时间自行选择合适的路线,极大方便出行.平时生活中也存在大量使用场景,在出行类App中,根据乘客的目的地可 ...

  6. ExtJS 布局-Auto布局(Auto Layout)

    更新记录 2022年5月30日 开启本篇 1.说明 auto布局是大部分容器默认的布局类型. auto布局通常是从上到下进行堆叠,auto布局不会设置子组件的宽度,默认与容器一样的宽度. 类似于HTM ...

  7. 分享一个基于 netty 的 java 开源项目

    1.简介 中微子代理(neutrino-proxy)是一个基于 netty 的.开源的 java 内网穿透项目.遵循 MIT 许可,因此您可以对它进行复制.修改.传播并用于任何个人或商业行为. 2.项 ...

  8. 【黑马pink老师的H5/CSS课程】(一)基本介绍

    视频P4~P6 黑马程序员pink老师前端入门教程,零基础必看的h5(html5)+css3+移动 1.网页 1.1 什么是网页 网站是网页的集合,网页是构成网站的基本元素,常用html格式文件 1. ...

  9. CF141E Clearing Up 题解

    思路分析 自认为是一道很好的思维题. 直接看上去的想法是: 跑一个生成树,每一次加的边颜色交替进行,直到拉出生成树. 仔细想想,发现可能无法保证最后是一棵树而不是森林,也是说输出都是 \(-1\) . ...

  10. Django定时任务Django-crontab的使用

    在使用的django做测试平台时,,多多少少都会遇到需要定时任务的功能,比如定时执行任务,检查订单之类 的.可能是一段时间,比如每隔 10分钟执行一次,也可能是定点时间,比如 14:00 执行,也可能 ...