请求管道详解
大约 9 分钟约 2794 字
请求管道详解
简介
ASP.NET Core 的请求管道(Request Pipeline)是请求从进入到响应返回的全链路。理解 HttpContext 的结构、中间件的连接方式、响应压缩和静态文件处理的内部机制,有助于构建高效的请求处理管道。
特点
HttpContext 深入
内部结构
// HttpContext 是请求处理的核心对象
// 包含:Request、Response、User、Features、Items、TraceIdentifier
// Request 属性
public class HttpContextInspector
{
public void Inspect(HttpContext context)
{
var req = context.Request;
// 请求行
Console.WriteLine($"Method: {req.Method}"); // GET/POST/PUT/DELETE
Console.WriteLine($"Scheme: {req.Scheme}"); // https
Console.WriteLine($"Host: {req.Host}"); // localhost:5000
Console.WriteLine($"Path: {req.Path}"); // /api/users/123
Console.WriteLine($"PathBase: {req.PathBase}"); // /app(如果有)
Console.WriteLine($"QueryString: {req.QueryString}");// ?page=1&size=20
Console.WriteLine($"Protocol: {req.Protocol}"); // HTTP/1.1 或 HTTP/2
// 请求头
Console.WriteLine($"ContentType: {req.ContentType}");
Console.WriteLine($"ContentLength: {req.ContentLength}");
Console.WriteLine($"UserAgent: {req.Headers["User-Agent"]}");
Console.WriteLine($"Accept: {req.Headers["Accept"]}");
// 连接信息
var conn = context.Connection;
Console.WriteLine($"RemoteIP: {conn.RemoteIpAddress}");
Console.WriteLine($"RemotePort: {conn.RemotePort}");
Console.WriteLine($"LocalIP: {conn.LocalIpAddress}");
Console.WriteLine($"LocalPort: {conn.LocalPort}");
// 用户信息
var user = context.User;
Console.WriteLine($"IsAuthenticated: {user.Identity?.IsAuthenticated}");
Console.WriteLine($"Name: {user.Identity?.Name}");
}
}
// Features — 请求功能接口
// HttpContext 通过 Features 模式提供扩展能力
var feature = context.Features.Get<IHttpRequestFeature>();
var bodyFeature = context.Features.Get<IHttpRequestBodyDetectionFeature>();
var httpsFeature = context.Features.Get<IHttpsUpgradeFeature>();
// Items — 请求级别的键值存储(Dictionary<object, object?>)
context.Items["StartTime"] = DateTime.UtcNow;
context.Items["CorrelationId"] = Guid.NewGuid().ToString();
// TraceIdentifier — 请求追踪 ID
Console.WriteLine($"TraceId: {context.TraceIdentifier}");中间件注册顺序
正确的管道顺序
// ASP.NET Core 中间件的正确注册顺序非常重要
// 通用规则:异常处理 → HTTPS → 静态文件 → 认证 → 授权 → 路由 → 端点
var app = builder.Build();
// 1. 异常处理(最外层,捕获所有异常)
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// 2. HTTPS 重定向
app.UseHttpsRedirection();
// 3. 响应压缩
app.UseResponseCompression();
// 4. 静态文件
app.UseStaticFiles();
// 5. 路由
app.UseRouting();
// 6. CORS
app.UseCors("AllowAll");
// 7. 认证(谁在请求)
app.UseAuthentication();
// 8. 授权(能否访问)
app.UseAuthorization();
// 9. 限流
app.UseRateLimiter();
// 10. 会话
app.UseSession();
// 11. 自定义中间件
app.UseMiddleware<RequestLoggingMiddleware>();
// 12. 端点映射
app.MapControllers();
app.MapGrpcService<OrderServiceImpl>();
app.MapHub<ChatHub>("/chathub");
app.MapGet("/health", () => "Healthy");
app.Run();
// 为什么这个顺序?
// UseExceptionHandler 必须最外层 → 捕获所有异常
// UseStaticFiles 在 UseRouting 之前 → 静态文件不需要路由
// UseAuthentication 在 UseAuthorization 之前 → 先识别用户再判断权限
// UseRouting 在 UseEndpoints 之前 → 先匹配路由再执行响应压缩
Gzip/Brotli 配置
// 注册响应压缩服务
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true; // HTTPS 也启用压缩
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
{
"application/json",
"application/grpc",
"text/event-stream",
"image/svg+xml"
});
});
// 配置压缩级别
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal; // 最高压缩比
// Fastest — 最快压缩(压缩比低)
// Optimal — 最佳压缩比(CPU 消耗高)
// 推荐生产环境使用 Fastest 或 Level 4
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
app.UseResponseCompression();
// 注意:
// 1. 压缩小于 1KB 的响应没有意义(压缩后可能更大)
// 2. 已压缩的格式(JPEG、PNG、MP4)不应再压缩
// 3. Brotli 比 Gzip 压缩比高约 15-25%
// 4. HTTPS + 压缩需要注意 BREACH 攻击
// 自定义压缩条件
builder.Services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.ShouldCompress = context =>
{
// 不压缩小于 1KB 的响应
if (context.Response.ContentLength < 1024) return false;
// 不压缩 SSE
if (context.Response.ContentType?.Contains("text/event-stream") == true) return false;
return true;
};
});静态文件处理
StaticFileMiddleware
// 静态文件中间件配置
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath, "static")),
RequestPath = "/static",
ServeUnknownFileTypes = true,
DefaultContentType = "application/octet-stream",
OnPrepareResponse = context =>
{
// 添加缓存头
context.Context.Response.Headers.Append("Cache-Control", "public, max-age=600");
// 记录静态文件访问
var logger = context.Context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogDebug("静态文件: {File}", context.File.Name);
}
});
// 启用目录浏览(仅开发环境)
if (app.Environment.IsDevelopment())
{
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(builder.Environment.WebRootPath),
RequestPath = "/browse"
});
}
// 默认文件(index.html)
app.UseDefaultFiles(new DefaultFilesOptions
{
DefaultFileNames = new List<string> { "index.html", "default.html" }
});
// 静态文件缓存策略
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = context =>
{
var path = context.File.Name;
if (path.EndsWith(".js") || path.EndsWith(".css"))
{
// JS/CSS 文件:长缓存 + 内容哈希
context.Context.Response.Headers.Append("Cache-Control", "public, max-age=31536000, immutable");
}
else if (path.EndsWith(".png") || path.EndsWith(".jpg"))
{
// 图片:中等缓存
context.Context.Response.Headers.Append("Cache-Control", "public, max-age=86400");
}
else
{
// 其他文件:短缓存
context.Context.Response.Headers.Append("Cache-Control", "public, max-age=600");
}
}
});HTTPS 与安全头
HTTPS 强制和安全配置
端点路由深入
路由匹配与优先级
// 端点路由的核心:路由匹配 → 请求委托选择 → 执行
// UseRouting 将路由信息存入 HttpContext.Features
// UseEndpoints 根据 Feature 中的信息选择并执行对应端点
// 路由约束
app.MapGet("/api/orders/{id:int}", (int id) => $"订单 {id}"); // 仅匹配整数
app.MapGet("/api/orders/{id:guid}", (Guid id) => $"订单 {id}"); // 仅匹配 GUID
app.MapGet("/api/orders/{id:min(1)}", (int id) => $"订单 {id}"); // 最小值
app.MapGet("/api/orders/{slug:regex(^[a-z0-9-]+$)}", (string slug) => $"订单 {slug}");
app.MapGet("/api/files/{filename}.{extension}", (string filename, string extension) => $"文件 {filename}.{extension}");
// 路由组(.NET 8+)
var ordersGroup = app.MapGroup("/api/orders")
.WithTags("Orders")
.WithOpenApi();
ordersGroup.MapGet("/", () => "订单列表");
ordersGroup.MapGet("/{id:int}", (int id) => $"订单 {id}");
ordersGroup.MapPost("/", () => "创建订单");
// 路由组添加中间件
var adminGroup = app.MapGroup("/api/admin")
.RequireAuthorization("AdminOnly")
.AddEndpointFilter(async (context, next) =>
{
// 管理员日志
context.HttpContext.Response.Headers.Append("X-Admin", "true");
return await next(context);
});
adminGroup.MapGet("/dashboard", () => "管理员面板");
adminGroup.MapGet("/users", () => "用户管理");端点过滤器
// 端点过滤器(.NET 8+)— 轻量级的请求拦截
// 类似 Action Filter,但用于 Minimal API
// 1. 内联过滤器
app.MapGet("/api/products/{id:int}", (int id) => $"产品 {id}")
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument<int>(0);
if (id <= 0)
{
return Results.BadRequest("ID 必须大于 0");
}
return await next(context);
});
// 2. 过滤器工厂
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// 自动验证请求体
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument == null)
return Results.BadRequest("请求体不能为空");
var validationContext = new ValidationContext(argument);
var results = new List<ValidationResult>();
if (!Validator.TryValidateObject(argument, validationContext, results, true))
{
return Results.ValidationProblem(results.ToDictionary(
r => r.MemberNames.FirstOrDefault() ?? "",
r => new string[] { r.ErrorMessage ?? "验证失败" }));
}
return await next(context);
}
}
// 使用
app.MapPost("/api/orders", (CreateOrderRequest req) => Results.Ok())
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();异常处理深入
全局异常处理
// 自定义异常处理中间件
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IHostEnvironment _env;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger,
IHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
// 记录异常日志(包含 TraceId)
var traceId = context.TraceIdentifier;
_logger.LogError(ex, "未处理异常 [{TraceId}]", traceId);
context.Response.ContentType = "application/json";
var response = ex switch
{
BizException biz => new ErrorResponse
{
Code = biz.Code,
Message = biz.Message,
TraceId = traceId,
StatusCode = biz.StatusCode
},
ValidationException validation => new ErrorResponse
{
Code = "VALIDATION_ERROR",
Message = "参数验证失败",
Details = validation.Errors,
TraceId = traceId,
StatusCode = 400
},
NotFoundException notFound => new ErrorResponse
{
Code = "NOT_FOUND",
Message = notFound.Message,
TraceId = traceId,
StatusCode = 404
},
UnauthorizedAccessException unauthorized => new ErrorResponse
{
Code = "UNAUTHORIZED",
Message = "未授权访问",
TraceId = traceId,
StatusCode = 401
},
_ => new ErrorResponse
{
Code = "INTERNAL_ERROR",
Message = _env.IsDevelopment() ? ex.Message : "服务器内部错误",
TraceId = traceId,
StatusCode = 500,
// 生产环境不暴露堆栈信息
StackTrace = _env.IsDevelopment() ? ex.StackTrace : null
}
};
context.Response.StatusCode = response.StatusCode;
await context.Response.WriteAsJsonAsync(response);
}
}
public record ErrorResponse(
string Code,
string Message,
string TraceId,
int StatusCode,
object? Details = null,
string? StackTrace = null);
// 注册(放在最外层)
app.UseMiddleware<GlobalExceptionMiddleware>();请求日志中间件
结构化请求日志
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var startTime = DateTime.UtcNow;
var sw = Stopwatch.StartNew();
// 请求信息
var requestInfo = new
{
Method = context.Request.Method,
Path = context.Request.Path.Value,
QueryString = context.Request.QueryString.Value,
ContentType = context.Request.ContentType,
UserAgent = context.Request.Headers["User-Agent"].ToString(),
RemoteIP = context.Connection.RemoteIpAddress?.ToString(),
TraceId = context.TraceIdentifier
};
_logger.LogInformation("请求开始: {Method} {Path} [{TraceId}]",
requestInfo.Method, requestInfo.Path, requestInfo.TraceId);
try
{
await _next(context);
}
finally
{
sw.Stop();
_logger.LogInformation(
"请求完成: {Method} {Path} → {StatusCode} ({Elapsed}ms) [{TraceId}]",
requestInfo.Method,
requestInfo.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds,
requestInfo.TraceId);
}
}
}
// 注册(在异常处理之后,业务中间件之前)
app.UseMiddleware<RequestLoggingMiddleware>();// HTTPS 重定向
app.UseHttpsRedirection();
// HSTS(HTTP Strict Transport Security)
app.UseHsts();
// 安全头中间件
app.Use(async (context, next) =>
{
// 防止 MIME 嗅探
context.Response.Headers.XContentTypeOptions = "nosniff";
// 防止点击劫持
context.Response.Headers.XFrameOptions = "DENY";
// XSS 保护
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
// CSP
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
// Referrer 策略
context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
await next();
});
// 自定义 HTTPS 重定向选项
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});优点
缺点
总结
HttpContext 是请求处理的核心,通过 Features 模式提供可扩展能力。中间件注册顺序至关重要:异常处理 → HTTPS → 压缩 → 静态文件 → 路由 → 认证 → 授权 → 端点。响应压缩使用 Brotli(推荐)或 Gzip,注意小文件和已压缩格式不应再压缩。静态文件中间件支持自定义缓存策略,建议 JS/CSS 使用长缓存+内容哈希。安全头(X-Content-Type-Options、X-Frame-Options、CSP)是 Web 安全的基础防线。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《请求管道详解》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《请求管道详解》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《请求管道详解》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《请求管道详解》最大的收益和代价分别是什么?
