Skip to content

Commit 4fd4056

Browse files
committed
🔖 1.24.10221.11803
1 parent 8704705 commit 4fd4056

File tree

3 files changed

+263
-1
lines changed

3 files changed

+263
-1
lines changed

src/BD.Common.Mvvm/Services.Implementation/FusilladeHttpClientFactory.cs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// https://github.com/dotnet/runtime/blob/v7.0.3/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs
22

33
using Fusillade;
4+
using Punchclock;
45
using Splat;
6+
using System.Reactive.Subjects;
7+
using System.Reactive.Threading.Tasks;
8+
59
#if ANDROID
610
using HttpHandlerType = Xamarin.Android.Net.AndroidMessageHandler;
711
#elif IOS || MACCATALYST
@@ -30,6 +34,10 @@ public FusilladeHttpClientFactory(HttpMessageHandler handler)
3034
{
3135
this.handler = handler;
3236
Locator.CurrentMutable.RegisterConstant(handler, typeof(HttpMessageHandler));
37+
OperationQueue operationQueue = new(12);
38+
NetCache.Speculative = new RateLimitedHttpMessageHandler2(handler, Priority.Speculative, 0, 5242880L, opQueue: operationQueue);
39+
NetCache.UserInitiated = new RateLimitedHttpMessageHandler2(handler, Priority.UserInitiated, opQueue: operationQueue);
40+
NetCache.Background = new RateLimitedHttpMessageHandler2(handler, Priority.Background, opQueue: operationQueue);
3341
}
3442

3543
public static HttpMessageHandler CreateHandler()
@@ -179,3 +187,229 @@ public void Dispose()
179187
GC.SuppressFinalize(this);
180188
}
181189
}
190+
191+
sealed class InflightRequest(Action onFullyCancelled)
192+
{
193+
int _refCount = 1;
194+
195+
public AsyncSubject<HttpResponseMessage> Response { get; protected set; }
196+
= new AsyncSubject<HttpResponseMessage>();
197+
198+
public void AddRef() => Interlocked.Increment(ref _refCount);
199+
200+
public void Cancel()
201+
{
202+
if (Interlocked.Decrement(ref _refCount) <= 0)
203+
{
204+
onFullyCancelled();
205+
}
206+
}
207+
}
208+
209+
/// <summary>
210+
/// A http handler which will limit the rate at which we can read.
211+
/// </summary>
212+
/// <remarks>
213+
/// Initializes a new instance of the <see cref="RateLimitedHttpMessageHandler2"/> class.
214+
/// </remarks>
215+
/// <param name="handler">The handler we are wrapping.</param>
216+
/// <param name="basePriority">The base priority of the request.</param>
217+
/// <param name="priority">The priority of the request.</param>
218+
/// <param name="maxBytesToRead">The maximum number of bytes we can read.</param>
219+
/// <param name="opQueue">The operation queue on which to run the operation.</param>
220+
/// <param name="cacheResultFunc">A method that is called if we need to get cached results.</param>
221+
sealed class RateLimitedHttpMessageHandler2(HttpMessageHandler handler, Priority basePriority, int priority = 0, long? maxBytesToRead = null, OperationQueue? opQueue = null, Func<HttpRequestMessage, HttpResponseMessage, string, CancellationToken, Task>? cacheResultFunc = null) : LimitingHttpMessageHandler(handler)
222+
{
223+
readonly int _priority = (int)basePriority + priority;
224+
readonly Dictionary<string, InflightRequest> _inflightResponses = new();
225+
long? _maxBytesToRead = maxBytesToRead;
226+
227+
/// <summary>
228+
/// Generates a unique key for a <see cref="HttpRequestMessage"/>.
229+
/// This assists with the caching.
230+
/// </summary>
231+
/// <param name="originalRequestUri"></param>
232+
/// <param name="request">The request to generate a unique key for.</param>
233+
/// <returns>The unique key.</returns>
234+
public static string UniqueKeyForRequest(
235+
string originalRequestUri,
236+
HttpRequestMessage request)
237+
{
238+
// https://github.com/reactiveui/Fusillade/blob/2.4.67/src/Fusillade/RateLimitedHttpMessageHandler.cs#L54-L89
239+
240+
using var s = new MemoryStream();
241+
s.Write(Encoding.UTF8.GetBytes(originalRequestUri));
242+
s.Write("\r\n"u8);
243+
s.Write(Encoding.UTF8.GetBytes(request.Method.Method));
244+
s.Write("\r\n"u8);
245+
static void Write(Stream s, IEnumerable<object> items)
246+
{
247+
foreach (var item in items)
248+
{
249+
var str = item.ToString();
250+
if (!string.IsNullOrEmpty(str))
251+
s.Write(Encoding.UTF8.GetBytes(str));
252+
s.Write("|"u8);
253+
}
254+
}
255+
Write(s, request.Headers.Accept);
256+
s.Write("\r\n"u8);
257+
Write(s, request.Headers.AcceptEncoding);
258+
s.Write("\r\n"u8);
259+
var referrer = request.Headers.Referrer;
260+
if (referrer == default)
261+
s.Write("http://example"u8);
262+
else
263+
s.Write(Encoding.UTF8.GetBytes(referrer.ToString()));
264+
s.Write("\r\n"u8);
265+
Write(s, request.Headers.UserAgent);
266+
s.Write("\r\n"u8);
267+
if (request.Headers.Authorization != null)
268+
{
269+
var parameter = request.Headers.Authorization.Parameter;
270+
if (!string.IsNullOrEmpty(parameter))
271+
s.Write(Encoding.UTF8.GetBytes(parameter));
272+
s.Write(Encoding.UTF8.GetBytes(request.Headers.Authorization.Scheme));
273+
s.Write("\r\n"u8);
274+
}
275+
s.Position = 0;
276+
var bytes = SHA384.HashData(s);
277+
var str = bytes.ToHexString();
278+
return str;
279+
}
280+
281+
/// <summary>
282+
/// Generates a unique key for a <see cref="HttpRequestMessage"/>.
283+
/// This assists with the caching.
284+
/// </summary>
285+
/// <param name="request">The request to generate a unique key for.</param>
286+
/// <returns>The unique key.</returns>
287+
public static string UniqueKeyForRequest(HttpRequestMessage request)
288+
{
289+
var requestUriString = ImageHttpClientService.ImageHttpRequestMessage.GetOriginalRequestUri(request);
290+
return UniqueKeyForRequest(requestUriString, request);
291+
}
292+
293+
/// <inheritdoc />
294+
public override void ResetLimit(long? maxBytesToRead = null) => _maxBytesToRead = maxBytesToRead;
295+
296+
/// <inheritdoc />
297+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
298+
{
299+
if (request is null)
300+
{
301+
throw new ArgumentNullException(nameof(request));
302+
}
303+
304+
var method = request.Method;
305+
if (method != HttpMethod.Get && method != HttpMethod.Head && method != HttpMethod.Options)
306+
{
307+
return base.SendAsync(request, cancellationToken);
308+
}
309+
310+
var cacheResult = cacheResultFunc;
311+
if (cacheResult == null && NetCache.RequestCache != null)
312+
{
313+
cacheResult = NetCache.RequestCache.Save;
314+
}
315+
316+
if (_maxBytesToRead < 0)
317+
{
318+
var tcs = new TaskCompletionSource<HttpResponseMessage>();
319+
#if NETSTANDARD2_0
320+
tcs.SetCanceled();
321+
#else
322+
tcs.SetCanceled(cancellationToken);
323+
#endif
324+
return tcs.Task;
325+
}
326+
327+
var key = UniqueKeyForRequest(request);
328+
var realToken = new CancellationTokenSource();
329+
var ret = new InflightRequest(() =>
330+
{
331+
lock (_inflightResponses)
332+
{
333+
_inflightResponses.Remove(key);
334+
}
335+
336+
realToken.Cancel();
337+
});
338+
339+
lock (_inflightResponses)
340+
{
341+
if (_inflightResponses.TryGetValue(key, out var value))
342+
{
343+
var val = value;
344+
val.AddRef();
345+
cancellationToken.Register(val.Cancel);
346+
347+
return val.Response.ToTask(cancellationToken);
348+
}
349+
350+
_inflightResponses[key] = ret;
351+
}
352+
353+
cancellationToken.Register(ret.Cancel);
354+
355+
var queue = new OperationQueue();
356+
357+
queue.Enqueue(
358+
_priority,
359+
null!,
360+
async () =>
361+
{
362+
try
363+
{
364+
var resp = await base.SendAsync(request, realToken.Token).ConfigureAwait(false);
365+
366+
if (_maxBytesToRead != null && resp.Content?.Headers.ContentLength != null)
367+
{
368+
_maxBytesToRead -= resp.Content.Headers.ContentLength;
369+
}
370+
371+
if (cacheResult != null && resp.Content != null)
372+
{
373+
var ms = new MemoryStream();
374+
#if NET5_0_OR_GREATER
375+
var stream = await resp.Content.ReadAsStreamAsync(realToken.Token).ConfigureAwait(false);
376+
#else
377+
var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);
378+
#endif
379+
await stream.CopyToAsync(ms, 32 * 1024, realToken.Token).ConfigureAwait(false);
380+
381+
realToken.Token.ThrowIfCancellationRequested();
382+
383+
var newResp = new HttpResponseMessage();
384+
foreach (var kvp in resp.Headers)
385+
{
386+
newResp.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
387+
}
388+
389+
var newContent = new ByteArrayContent(ms.ToArray());
390+
foreach (var kvp in resp.Content.Headers)
391+
{
392+
newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
393+
}
394+
395+
newResp.Content = newContent;
396+
397+
resp = newResp;
398+
await cacheResult(request, resp, key, realToken.Token).ConfigureAwait(false);
399+
}
400+
401+
return resp;
402+
}
403+
finally
404+
{
405+
lock (_inflightResponses)
406+
{
407+
_inflightResponses.Remove(key);
408+
}
409+
}
410+
},
411+
realToken.Token).ToObservable().Subscribe(ret.Response);
412+
413+
return ret.Response.ToTask(cancellationToken);
414+
}
415+
}

src/BD.Common/Net/Http/ImageHttpClientService.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,34 @@ public ImageHttpRequestMessage(HttpMethod method, [StringSyntax(StringSyntaxAttr
170170
}
171171

172172
public string OriginalRequestUri { get; }
173+
174+
/// <summary>
175+
/// 默认请求地址
176+
/// </summary>
177+
public const string DefaultRequestUri = "/";
178+
179+
/// <summary>
180+
/// 从请求消息中获取原始请求地址
181+
/// </summary>
182+
/// <param name="request"></param>
183+
/// <param name="defaultRequestUri"></param>
184+
/// <returns></returns>
185+
public static string GetOriginalRequestUri(HttpRequestMessage request, string defaultRequestUri = DefaultRequestUri)
186+
{
187+
string? originalRequestUri;
188+
if (request is ImageHttpRequestMessage request2)
189+
{
190+
// 对于一些重定向的 Url 使用原始 Url 进行唯一性的计算
191+
originalRequestUri = request2.OriginalRequestUri;
192+
}
193+
else
194+
{
195+
originalRequestUri = request.RequestUri?.ToString()!;
196+
}
197+
if (string.IsNullOrEmpty(originalRequestUri))
198+
originalRequestUri = defaultRequestUri;
199+
return originalRequestUri;
200+
}
173201
}
174202

175203
async Task<MemoryStream?> GetImageMemoryStreamCoreAsync(

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<ImplicitUsings>enable</ImplicitUsings>
1111
<IsTrimmable>true</IsTrimmable>
1212
<!--<Version>1.yy.1MMdd.1hhmm</Version>-->
13-
<Version>1.23.11113.11410</Version>
13+
<Version>1.24.10221.11803</Version>
1414
<PackageIconUrl>https://avatars.githubusercontent.com/u/79355691?s=200&amp;v=4</PackageIconUrl>
1515
<Company>江苏蒸汽凡星科技有限公司</Company>
1616
<Copyright>©️ $(Company). All rights reserved.</Copyright>

0 commit comments

Comments
 (0)