
通过ASP.NET Core开发反向代理服务器与API网关功能:从零构建你的流量调度中心
你好,我是源码库的技术博主。在微服务架构大行其道的今天,反向代理和API网关已成为系统不可或缺的“交通枢纽”。我曾经历过项目初期直接硬编码服务调用地址,后期服务拆分和部署变更时带来的“牵一发而动全身”的噩梦。今天,我将带你一起,使用我们熟悉的ASP.NET Core,从零开始构建一个兼具反向代理和基础API网关功能的应用。这个过程不仅有助于理解其底层原理,更能让你在需要定制特殊路由、鉴权或流量控制逻辑时,拥有完全的自主权。
一、项目初始化与核心库YARP的引入
首先,我们创建一个新的ASP.NET Core Web项目。这里我选择空模板,以便更清晰地展示核心结构。
dotnet new web -n MyCustomGateway
cd MyCustomGateway
接下来,引入本次实战的核心——YARP (Yet Another Reverse Proxy)。这是微软官方推出的一个高性能、可扩展的反向代理库,它完美地集成在ASP.NET Core的管道中,是我们构建自定义网关的基石。
dotnet add package Yarp.ReverseProxy
安装完成后,打开 `Program.cs` 文件。我们需要在这里完成服务和中间件的配置。
二、配置反向代理:让请求流动起来
反向代理的核心是配置:定义一组“路由”(Routes)和它们对应的“集群”(Clusters)。路由负责匹配传入的请求(比如根据路径),集群则定义了该请求将被转发到的一个或多个目标地址。
我们先在 `appsettings.json` 中定义一个简单的配置,将访问 `/service1/` 的请求转发到本地另一个运行在5001端口的服务。
{
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Path": "/service1/{**catch-all}"
},
"Transforms": [
{ "PathPattern": "{**catch-all}" }
]
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"destination1": {
"Address": "http://localhost:5001/"
}
}
}
}
}
}
这里有个踩坑提示:注意 `Path` 匹配和 `PathPattern` 转换。`{**catch-all}` 是一个通配参数,它匹配路径的剩余部分。在 `Match` 中我们捕获了整个路径,在 `Transforms` 中我们又将其原样设置为转发后的路径,这确保了 `/service1/api/users` 会被正确地转发到 `http://localhost:5001/api/users`,而不是错误地带上 `/service1` 前缀。
现在,在 `Program.cs` 中加载配置并启用YARP。
var builder = WebApplication.CreateBuilder(args);
// 1. 添加反向代理服务
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
// 2. 启用反向代理中间件
app.MapReverseProxy();
app.Run();
启动你的网关项目(假设在5000端口)和一个模拟的后端服务(在5001端口返回简单信息)。访问 `http://localhost:5000/service1/hello`,你应该能看到来自5001端口的响应。恭喜,一个基础的反向代理已经跑通了!
三、进阶为API网关:注入自定义逻辑
如果只是静态配置转发,那和Nginx差别不大。API网关的强大之处在于能在请求转发链路上轻松插入各种业务逻辑。YARP通过“管道”(Pipeline)的概念支持这一点。让我们实现两个常见功能:请求头验证和简单的响应缓存。
1. 自定义请求验证中间件
假设我们要求所有转到 `cluster1` 的请求必须携带一个特定的API密钥头。我们创建一个自定义的中间件。
// CustomAuthMiddleware.cs
public class CustomAuthMiddleware
{
private readonly RequestDelegate _next;
public CustomAuthMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext context)
{
// 检查请求是否指向我们关心的集群(可通过路由元数据或特定路径判断)
// 这里简单检查路径前缀
if (context.Request.Path.StartsWithSegments("/service1"))
{
if (!context.Request.Headers.TryGetValue("X-API-Key", out var key) || key != "MySecretKey")
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Missing or invalid API Key");
return; // 中断管道,不再转发
}
}
// 验证通过,继续执行代理管道
await _next(context);
}
}
在 `Program.cs` 中,我们需要将这个中间件插入到代理管道之前。
var app = builder.Build();
// 使用自定义认证中间件
app.UseMiddleware();
app.MapReverseProxy();
app.Run();
现在,不带 `X-API-Key: MySecretKey` 头请求 `/service1` 的请求将被拦截并返回401。
2. 实现简单的响应缓存
YARP允许我们通过实现 `IForwarderHttpClientFactory` 或使用 `ITransformProvider` 来深度定制行为。这里我们用一个更直接的方式演示思路:在代理执行前后通过事件钩子进行操作。我们可以创建一个自定义的 `IForwarderTransform` 来实现响应缓存逻辑,但为了清晰,我们先在自定义中间件里做一个概念演示。
// 在CustomAuthMiddleware的Invoke方法中,验证之后可以加入缓存逻辑
// 注意:这是一个简化演示,生产环境应使用分布式缓存(如Redis)并考虑缓存策略。
public async Task Invoke(HttpContext context, IMemoryCache cache)
{
if (context.Request.Path.StartsWithSegments("/service1/api/products") && context.Request.Method == "GET")
{
var cacheKey = context.Request.Path + context.Request.QueryString;
if (cache.TryGetValue(cacheKey, out string cachedResponse))
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(cachedResponse);
return; // 直接返回缓存,不再转发
}
// 缓存未命中,继续执行代理,但我们需要“窃听”响应
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context); // 执行代理,请求被转发到后端
// 如果响应成功,则缓存
if (context.Response.StatusCode == 200)
{
responseBody.Seek(0, SeekOrigin.Begin);
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
cache.Set(cacheKey, responseText, TimeSpan.FromSeconds(30)); // 缓存30秒
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
context.Response.Body = originalBodyStream;
}
else
{
await _next(context);
}
}
别忘了在 `Program.cs` 的 `builder.Services` 中添加 `IMemoryCache` 服务:`builder.Services.AddMemoryCache();`。
这个示例演示了在网关层拦截请求和响应的能力。在实际项目中,更复杂的限流、熔断、服务发现等功能,都可以通过类似的方式,在YARP提供的强大抽象之上进行构建。
四、动态配置与生产环境考量
硬编码在 `appsettings.json` 中的配置在服务动态上下线时不够灵活。YARP支持从任何来源(数据库、Consul、Apollo等)动态加载配置。你需要实现 `IProxyConfigProvider` 和 `IProxyConfig` 接口,并在服务注册时通过 `.LoadFromCustomProvider` 方法注入。
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.LoadFromCustomProvider(); // 添加自定义配置源
在你的 `MyCustomConfigProvider` 中,可以定期从数据库拉取最新的路由和集群信息,并调用 `IProxyConfig` 的 `SignalChange()` 方法来通知YARP热更新配置,无需重启网关服务。
最后的重要提示:自己构建网关给了你极大的灵活性,但也意味着你需要承担更多的责任,比如高性能转发、高可用部署、全面的日志监控和异常处理。对于大多数标准场景,直接使用成熟的开源网关(如Ocelot, 它本身也基于ASP.NET Core)或云厂商的托管网关可能是更高效的选择。但这次手把手的实践,无疑会让你在面对任何网关相关问题时,都能洞悉其本质,从容应对。
希望这篇教程能帮助你打开自定义API网关的大门。在源码库,我们始终相信,理解底层原理是成为高级开发者的必经之路。如果有任何问题或更深入的探讨,欢迎在评论区交流。 Happy coding!

评论(0)