前言

整理了一下.net core 一些常見的庫的原始碼閱讀,共32個庫,記100餘篇。

以下只是個人的原始碼閱讀,如有錯誤或者思路不正確,望請指點。

正文

github 地址為:

https://github.com/stefanprodan/AspNetCoreRateLimit

一般個人習慣先閱讀readme的簡介。

上面大概翻譯是:

AspNetCoreRateLimit 是ASP.NET Core 訪問速率限制的解決方案,設計基於ip地址和客戶端id用於控制用於web api和 mvc app的客戶端訪問速率。

這個包包含了IpRateLimitMiddleware and a ClientRateLimitMiddleware兩個中介軟體,用這兩個中介軟體你根據不同的場景能設定幾種不同的限制,

比如限制一個客戶端或者一個ip在幾秒或者15分鐘內訪問最大限制。您可以定義這些限制來處理對某個API的所有請求,也可以將這些限制限定在指定範圍的每個API URL或HTTP請求路徑上。

上面說了這麼多就是用來限流的,針對客戶端id和ip進行限流。

因為一般用的是ip限流,看下ip限制怎麼使用的,畢竟主要還是拿來用的嘛。

本來還想根據文件先寫個小demo,然後發現官方已經寫了demo。

直接看demo(只看ip限制部分的)。

先來看下ConfigureServices:

services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
services.AddInMemoryRateLimiting();

從上面看,那麼配置ip 限制的有兩個配置,一個配置是IpRateLimitOptions,另外一個配置是IpRateLimitPolicies。

那麼為什麼要設計成兩個配置呢?一個配置不是更香嗎?

官方設計理念是這樣的:

https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware#defining-rate-limit-rules

IpRateLimiting Configuration and general rules appsettings.json

IpRateLimitPolicies Override general rules for specific IPs appsettings.json

原來IpRateLimiting 是限制普遍的ip,而IpRateLimitPolicies 是限制一些特殊的ip。

比如說有些api對內又對外的,普遍的ip對外限制是1分鐘300次,如果有個大客戶特殊需求且固定ip的,需要限制是1分鐘是10000次的,那麼就可以這樣特殊處理,而不用另外寫code來維護,成本問題。

故而我們寫中介軟體元件的時候也可以參考這個來做,特殊的怎麼處理,普遍的怎麼處理,當然也不能盲目的設計。

然後看:AddInMemoryRateLimiting

public static IServiceCollection AddInMemoryRateLimiting(this IServiceCollection services)
{
services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
return services;
}

裡面注入了MemoryCacheIpPolicyStore、MemoryCacheClientPolicyStore、MemoryCacheRateLimitCounterStore、AsyncKeyLockProcessingStrategy。

分別看下這幾個東西。

MemoryCacheIpPolicyStore:

public class MemoryCacheIpPolicyStore : MemoryCacheRateLimitStore<IpRateLimitPolicies>, IIpPolicyStore
{
private readonly IpRateLimitOptions _options;
private readonly IpRateLimitPolicies _policies; public MemoryCacheIpPolicyStore(
IMemoryCache cache,
IOptions<IpRateLimitOptions> options = null,
IOptions<IpRateLimitPolicies> policies = null) : base(cache)
{
_options = options?.Value;
_policies = policies?.Value;
} public async Task SeedAsync()
{
// on startup, save the IP rules defined in appsettings
if (_options != null && _policies != null)
{
await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false);
}
}
}

這個是用例儲存IpRateLimitPolicies(ip限制)。

MemoryCacheIpPolicyStore 這個名字起的有點意思,MemoryCache 是記憶體快取,IpPolicy ip策略,store 儲存。

分別是儲存空間、物品、功能的組合。所以這個庫應該是外國人寫的,一般來說中國人會這樣改:IpPolicyMemoryCacheStore,估計是因為強調故而把MemoryCache放到前面去了。

這裡我剛開始有點不理解,本來已經可以讀取到了options,那麼按照options操作就很方便了。

那麼為啥要用快取到記憶體中呢?後來大體的通讀了一下,是因為_policies(特殊制定的ip規則)很多地方都要使用到,一方面是為了解耦,另外一方面呢,是因為下面這個。

[HttpPost]
public void Post()
{
var pol = _ipPolicyStore.Get(_options.IpPolicyPrefix); pol.IpRules.Add(new IpRateLimitPolicy
{
Ip = "8.8.4.4",
Rules = new List<RateLimitRule>(new RateLimitRule[] {
new RateLimitRule {
Endpoint = "*:/api/testupdate",
Limit = 100,
Period = "1d" }
})
}); _ipPolicyStore.Set(_options.IpPolicyPrefix, pol);
}

是可以動態設定特殊ip的一些配置的。 那麼裡面也考慮到了分散式的一些行為,比如把快取放到redis這種隔離快取中。

如果將_policies 封裝到memory cache 中,那麼和redis cache形成了一套介面卡。個人認為是從設計方面考慮的。

然後看下這個方法,裡面就是以IpRateLimiting的IpPolicyPrefix 作為key,然後儲存了IpRateLimitPolicies。

public async Task SeedAsync()
{
// on startup, save the IP rules defined in appsettings
if (_options != null && _policies != null)
{
await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false);
}
}

具體的SetAsync 如下:

public Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default)
{
var options = new MemoryCacheEntryOptions
{
Priority = CacheItemPriority.NeverRemove
}; if (expirationTime.HasValue)
{
options.SetAbsoluteExpiration(expirationTime.Value);
} _cache.Set(id, entry, options); return Task.CompletedTask;
}

然後這裡值得注意的是_options.IpPolicyPrefix,這個值如果是分散式那麼應該值得關注一下,因為我們有不同應用服務,如果希望不同的應用服務用到不同的ip限制,那麼IpPolicyPrefix 最好改成應用名,而不是使用預設值。

那麼看下MemoryCacheClientPolicyStore:

public class MemoryCacheClientPolicyStore : MemoryCacheRateLimitStore<ClientRateLimitPolicy>, IClientPolicyStore
{
private readonly ClientRateLimitOptions _options;
private readonly ClientRateLimitPolicies _policies; public MemoryCacheClientPolicyStore(
IMemoryCache cache,
IOptions<ClientRateLimitOptions> options = null,
IOptions<ClientRateLimitPolicies> policies = null) : base(cache)
{
_options = options?.Value;
_policies = policies?.Value;
} public async Task SeedAsync()
{
// on startup, save the IP rules defined in appsettings
if (_options != null && _policies?.ClientRules != null)
{
foreach (var rule in _policies.ClientRules)
{
await SetAsync($"{_options.ClientPolicyPrefix}_{rule.ClientId}", new ClientRateLimitPolicy { ClientId = rule.ClientId, Rules = rule.Rules }).ConfigureAwait(false);
}
}
}
}

這個就是client id的限制的快取的,和上面一樣就不看了。

MemoryCacheRateLimitCounterStore:

public class MemoryCacheRateLimitCounterStore : MemoryCacheRateLimitStore<RateLimitCounter?>, IRateLimitCounterStore
{
public MemoryCacheRateLimitCounterStore(IMemoryCache cache) : base(cache)
{
}
}

這裡面沒有啥子。但是從名字上猜測,裡面是快取每個ip請求次數的當然還有時間,主要起快取作用。

最後一個:AsyncKeyLockProcessingStrategy

public class AsyncKeyLockProcessingStrategy : ProcessingStrategy
{
private readonly IRateLimitCounterStore _counterStore;
private readonly IRateLimitConfiguration _config; public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, IRateLimitConfiguration config)
: base(config)
{
_counterStore = counterStore;
_config = config;
} /// The key-lock used for limiting requests.
private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock(); public override async Task<RateLimitCounter> ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default)
{
var counter = new RateLimitCounter
{
Timestamp = DateTime.UtcNow,
Count = 1
}; var counterId = BuildCounterKey(requestIdentity, rule, counterKeyBuilder, rateLimitOptions); // serial reads and writes on same key
using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false))
{
var entry = await _counterStore.GetAsync(counterId, cancellationToken); if (entry.HasValue)
{
// entry has not expired
if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
{
// increment request count
var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1; // deep copy
counter = new RateLimitCounter
{
Timestamp = entry.Value.Timestamp,
Count = totalCount
};
}
} // stores: id (string) - timestamp (datetime) - total_requests (long)
await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken);
} return counter;
}
}

估摸著是執行具體計數邏輯的,那麼等執行中介軟體的時候在看。

後面有寫入了一個:services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

那麼這個RateLimitConfiguration 是做什麼的呢?

public class RateLimitConfiguration : IRateLimitConfiguration
{
public IList<IClientResolveContributor> ClientResolvers { get; } = new List<IClientResolveContributor>();
public IList<IIpResolveContributor> IpResolvers { get; } = new List<IIpResolveContributor>(); public virtual ICounterKeyBuilder EndpointCounterKeyBuilder { get; } = new PathCounterKeyBuilder(); public virtual Func<double> RateIncrementer { get; } = () => 1; public RateLimitConfiguration(
IOptions<IpRateLimitOptions> ipOptions,
IOptions<ClientRateLimitOptions> clientOptions)
{
IpRateLimitOptions = ipOptions?.Value;
ClientRateLimitOptions = clientOptions?.Value;
} protected readonly IpRateLimitOptions IpRateLimitOptions;
protected readonly ClientRateLimitOptions ClientRateLimitOptions; public virtual void RegisterResolvers()
{
string clientIdHeader = GetClientIdHeader();
string realIpHeader = GetRealIp(); if (clientIdHeader != null)
{
ClientResolvers.Add(new ClientHeaderResolveContributor(clientIdHeader));
} // the contributors are resolved in the order of their collection index
if (realIpHeader != null)
{
IpResolvers.Add(new IpHeaderResolveContributor(realIpHeader));
} IpResolvers.Add(new IpConnectionResolveContributor());
} protected string GetClientIdHeader()
{
return ClientRateLimitOptions?.ClientIdHeader ?? IpRateLimitOptions?.ClientIdHeader;
} protected string GetRealIp()
{
return IpRateLimitOptions?.RealIpHeader ?? ClientRateLimitOptions?.RealIpHeader;
}
}

重點看:

public virtual void RegisterResolvers()
{
string clientIdHeader = GetClientIdHeader();
string realIpHeader = GetRealIp(); if (clientIdHeader != null)
{
ClientResolvers.Add(new ClientHeaderResolveContributor(clientIdHeader));
} // the contributors are resolved in the order of their collection index
if (realIpHeader != null)
{
IpResolvers.Add(new IpHeaderResolveContributor(realIpHeader));
} IpResolvers.Add(new IpConnectionResolveContributor());
}

這裡只看ip部分:

protected string GetRealIp()
{
return IpRateLimitOptions?.RealIpHeader ?? ClientRateLimitOptions?.RealIpHeader;
}

那麼這個IpHeaderResolveContributor是什麼呢?

public class IpHeaderResolveContributor : IIpResolveContributor
{
private readonly string _headerName; public IpHeaderResolveContributor(
string headerName)
{
_headerName = headerName;
} public string ResolveIp(HttpContext httpContext)
{
IPAddress clientIp = null; if (httpContext.Request.Headers.TryGetValue(_headerName, out var values))
{
clientIp = IpAddressUtil.ParseIp(values.Last());
} return clientIp?.ToString();
}
}

原來是配置是從header的哪個位置獲取ip。官網demo中給的是"RealIpHeader": "X-Real-IP"。從header部分的RealIpHeader獲取。

同樣,官方也預設提供了IpResolvers.Add(new IpConnectionResolveContributor());。

public class IpConnectionResolveContributor : IIpResolveContributor
{ public IpConnectionResolveContributor()
{ } public string ResolveIp(HttpContext httpContext)
{
return httpContext.Connection.RemoteIpAddress?.ToString();
}
}

從httpContext.Connection.RemoteIpAddress 中獲取ip,那麼問題來了,RemoteIpAddress 是如何獲取的呢? 到底X-Real-IP 獲取的ip準不準呢?會在.net core 細節篇中介紹。

回到原始。現在已經注入了服務,那麼如何把中介軟體注入進去呢?

在Configure 中:

app.UseIpRateLimiting();

將會執行中介軟體:IpRateLimitMiddleware

public class IpRateLimitMiddleware : RateLimitMiddleware<IpRateLimitProcessor>
{
private readonly ILogger<IpRateLimitMiddleware> _logger; public IpRateLimitMiddleware(RequestDelegate next,
IProcessingStrategy processingStrategy,
IOptions<IpRateLimitOptions> options,
IRateLimitCounterStore counterStore,
IIpPolicyStore policyStore,
IRateLimitConfiguration config,
ILogger<IpRateLimitMiddleware> logger
)
: base(next, options?.Value, new IpRateLimitProcessor(options?.Value, counterStore, policyStore, config, processingStrategy), config)
{
_logger = logger;
} protected override void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
{
_logger.LogInformation($"Request {identity.HttpVerb}:{identity.Path} from IP {identity.ClientIp} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.Count - rule.Limit}. Blocked by rule {rule.Endpoint}, TraceIdentifier {httpContext.TraceIdentifier}. MonitorMode: {rule.MonitorMode}");
}
}

檢視:RateLimitMiddleware

裡面就是具體的invoke中介軟體程式碼了。

因為篇幅有限,後一節invoke逐行分析其如何實現的。

以上只是個人看原始碼的過程,希望能得到各位的指點,共同進步。

另外.net core 細節篇整理進度為40%。