簡體   English   中英

ASP.NET Core 禁用響應緩沖

[英]ASP.NET Core Disable Response Buffering

我正在嘗試將動態構建的大型 JSON 文件流式傳輸到客戶端(可能是 500 MB+)。 出於各種原因,我試圖禁用響應緩沖,但主要是為了提高內存效率。

我嘗試直接寫入HttpContext.Response.BodyWriter但響應似乎在寫入輸出之前緩沖在內存中。 此方法的返回類型是Task

HttpContext.Response.ContentType = "application/json";
HttpContext.Response.ContentLength = null;
await HttpContext.Response.StartAsync(cancellationToken);
var bodyStream = HttpContext.Response.BodyWriter.AsStream(true);
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes("["), cancellationToken);
await foreach (var item in cursor.WithCancellation(cancellationToken)
    .ConfigureAwait(false))
{
    await bodyStream.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(item, DefaultSettings.JsonSerializerOptions), cancellationToken);
    await bodyStream.WriteAsync(Encoding.UTF8.GetBytes(","), cancellationToken);
    
    await bodyStream.FlushAsync(cancellationToken);
    await Task.Delay(100,cancellationToken);
}
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes("]"), cancellationToken);
bodyStream.Close();
await HttpContext.Response.CompleteAsync().ConfigureAwait(false);

注意:我意識到這段代碼非常hacky,試圖讓它工作,然后清理它

我正在使用Task.Delay來驗證在本地測試時響應沒有被緩沖,因為我沒有完整的生產數據。 我也嘗試過IAsyncEnumerableyield return ,但失敗了,因為響應太大,Kestrel 認為可枚舉是無限的。

我試過了

  1. KestrelServerLimits.MaxResponseBufferSize設置為一個很小的數字,甚至為 0;
  2. 使用HttpContext.Response.WriteAsync編寫
  3. 使用HttpContext.Response.BodyWriter.AsStream()編寫
  4. 使用管道編寫器模式和HttpContext.Response.BodyWriter編寫
  5. 刪除所有中間件
  6. 刪除對IApplicationBuilder.UseResponseCompression的調用

更新

  1. 嘗試在設置ContentType之前禁用響應緩沖(因此在任何寫入響應之前)但沒有效果
var responseBufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
responseBufferingFeature?.DisableBuffering();

更新的示例代碼

這很簡單地重現了這個問題。 在調用response.CompleteAsync()之前,客戶端不會收到任何數據。

[HttpGet]
[Route("stream")]
public async Task<EmptyResult> FileStream(CancellationToken cancellationToken)
{
    var response = DisableResponseBuffering(HttpContext);
    HttpContext.Response.Headers.Add("Content-Type", "application/gzip");
    HttpContext.Response.Headers.Add("Content-Disposition", $"attachment; filename=\"player-data.csv.gz\"");
    await response.StartAsync().ConfigureAwait(false);
    var memory = response.Writer.GetMemory(1024*1024*10);
    response.Writer.Advance(1024*1024*10);
    await response.Writer.FlushAsync(cancellationToken).ConfigureAwait(false);
    await Task.Delay(5000).ConfigureAwait(false);
    var str2 = Encoding.UTF8.GetBytes("Bar!\r\n");
    memory = response.Writer.GetMemory(str2.Length);
    str2.CopyTo(memory);
    response.Writer.Advance(str2.Length);
    await response.CompleteAsync().ConfigureAwait(false);
    return new EmptyResult();
}

private IHttpResponseBodyFeature DisableResponseBuffering(HttpContext context)
{
    var responseBufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
    responseBufferingFeature?.DisableBuffering();
    return responseBufferingFeature;
}

嘗試禁用響應期貨的緩沖:

HttpContext.Features.Get<IHttpResponseBodyFeature>().DisableBuffering()
//As mentioned in documentation, to take effect, call it before any writes

並在Utf8JsonWriter使用BodyWriter以提高效率:

 var pipe = context.HttpContext.Response.BodyWriter;
 await pipe.WriteAsync(startArray);
 using (var writer = new Utf8JsonWriter(pipe,
            new JsonWriterOptions
            {
                Indented = option.WriteIndented,
                Encoder = option.Encoder,
                SkipValidation = true
            }))
 {
      var dotSet = false;
      foreach (var item in enumerable)
      {
           if (dotSet)
               await pipe.WriteAsync(dot);
           JsonSerializer.Serialize(writer, item, itemType, option);
           await pipe.FlushAsync();
           writer.Reset();
           dotSet = true;
      }
 }
 await pipe.WriteAsync(endArray);

就我而言,它給出了結果:在第一次請求后,總內存分配比 newcoreapp2.2 增加了 80% 以上,但沒有更多的內存泄漏。

對於那些仍然感興趣的人,這段代碼在使用 curl 時會立即發送數據:

public async Task Invoke(HttpContext context)
{
    var g = context.Features.Get<IHttpResponseBodyFeature>();
    g.DisableBuffering(); // doesn't seem to make a difference

    context.Response.StatusCode = 200;
    context.Response.ContentType = "text/plain; charset=utf-8";
    //context.Response.ContentLength = null;

    await g.StartAsync();

    for (int i = 0; i < 10; ++i)
    {
        var line = $"this is line {i}\r\n";
        var bytes = utf8.GetBytes(line);
        // it seems context.Response.Body.WriteAsync() and
        // context.Response.BodyWriter.WriteAsync() work exactly the same
        await g.Writer.WriteAsync(new ReadOnlyMemory<byte>(bytes));
        await g.Writer.FlushAsync();
        await Task.Delay(1000);
    }

    await g.CompleteAsync();
}

我嘗試使用和不使用DisableBufering()以及寫入管道( IHttpResponseBodyFeature.WriterHttpContext.Response.Body )的變化似乎沒有區別。

在 curl 中,它會立即顯示消息,但是在 Chrome 和一些其他客戶端中,它會等待整個流顯示出來。

因此,我建議使用不等待整個流呈現它的客戶端來測試您的代碼行為。 另一個選項我仍在檢查aspnet core是否會在客戶端要求時自動選擇壓縮可能性,即使在管道中沒有配置壓縮。 所以我會推薦

使用 http.sys(使用 ASP.NET Core 6)時,我能夠做到這一點:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.WebHost.UseHttpSys();
        var app = builder.Build();
        app.MapGet("/", async (context) =>
        {
            context.Response.StatusCode = 201;
            await context.Response.StartAsync();
            await context.Response.WriteAsync("x"); // client gets status code after this line
            await context.Response.WriteAsync("Hello World!");
        });
        app.Run();
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM