【问题标题】:Appending to a Response Body in C# .NET Core在 C# .NET Core 中附加到响应正文
【发布时间】:2020-12-22 01:27:34
【问题描述】:

我正在做一个项目,我在后端填充一些 pdf,然后我将这些 pdf 转换为 byte[] 列表,该列表被合并到一个非常大的数组中,最后通过响应体发回作为记忆流。 我的问题是这是大量数据,在获取要合并的字节数组列表的过程中,我使用了大量内存。 我想知道是否不是将最终合并的 byte[] 转换为 Memory Stream 并将其添加到响应正文中;我可以创建几个 Memory Stream 对象,在创建它们时将它们附加到 Response.Body 中吗?或者,我想知道是否有一种方法可以使用一个内存流并继续添加它作为为每个 pdf 文档创建每个新字节 []?

编辑:这可能有点啰嗦,但我对我原来的帖子太含糊了。在我想做的事情的核心,我有几个 pdf 文档,它们每个都有几页长。它们中的每一个都在下面的代码中表示为 filesToMerge 列表中的 byte[] 项之一。理想情况下,我想一个一个地浏览这些并将它们转换为内存流,并在一个循环中一个接一个地发送给客户端。但是,当我尝试执行此操作时,我收到响应正文已发送的错误。有没有办法在响应正文中附加一些内容,以便每次通过循环更新?

    [HttpGet("template/{formId}/fillforms")]
    public void FillForms(string formId/*, [FromBody] IList<IDictionary<string, string>> fieldDictionaries*/)
    {
        List<byte[]> filesToMerge = new List<byte[]>();

        // For testing
        var mockData = new MockData();
        IList<IDictionary<string, string>> fieldDictionaries = mockData.GetMock1095Dictionaries();


        foreach(IDictionary<string, string> dictionary in fieldDictionaries)
        {
            var populatedForm = this.dataRepo.PopulateForm(formId, dictionary);
            // write to rb
            filesToMerge.Add(populatedForm);
        }

        byte[] mergedFilesAsByteArray = this.dataRepo.GetMergedByteArray(filesToMerge);

        this.SendResponse(formId + "_filled.pdf", new MemoryStream(mergedFilesAsByteArray));
    }

    private void SendResponse(string formName, MemoryStream ms, IDictionary<string, string> fieldData = null)
    {
        Response.Clear();
        Response.ContentType = "application/pdf";
        Response.Headers.Add("content-disposition", $"attachment;filename={formName}.pdf");
        ms.WriteTo(Response.Body);
    }

【问题讨论】:

  • 这个问题有点开放式,虽然这个故事是对你在做什么的高级视图,很难知道你实际在做什么,以及我们如何在不看到实际情况的情况下减少你的分配代码
  • 您可以将您的方法 FillForm 更改为 IActionResult(公共 IActionResult FillForms)的返回类型,然后您将能够使用现有方法来公开文件或内容。请参阅此链接以获取示例 c-sharpcorner.com/article/fileresult-in-asp-net-core-mvc2

标签: c# .net-core memory-management


【解决方案1】:

内存流实际上只是带有一堆好方法的字节数组。所以切换到字节数组不会有太大帮助。很多人在处理字节数组和内存流时遇到的一个问题是,当您处理完数据时不会释放内存,因为它们占用了您正在运行的机器的内存,因此您很容易耗尽内存。因此,您应该以“using statements”为例,在不再需要数据时立即处理数据。内存流有一个称为 Dispose 的方法,它将释放流使用的所有资源

如果您想尽快从应用程序中传输数据,最好的方法是将流切割成更小的部分,然后在目的地以正确的顺序重新组合它们。你可以将它们削减到 1mb 或 126kb,无论你想要什么。当您将数据发送到目的地时,您还需要传递这部分的订单号,因为这种方法允许您并行发布数据并且不保证顺序。

将一个流拆分为多个流

private static List<MemoryStream> CreateChunks(Stream stream)
{
    byte[] buffer = new byte[4000000]; //set the size of your buffer (chunk)
    var returnStreams = new List<MemoryStream>();
    using (MemoryStream ms = new MemoryStream())
    {
        while (true) //loop to the end of the file
        {
            var returnStream = new MemoryStream();
            int read = stream.Read(buffer, 0, buffer.Length); //read each chunk
            returnStream.Write(buffer, 0, read); //write chunk to [wherever];
            if (read <= 0)
            { //check for end of file
                return returnStreams;
            }
            else
            {
                returnStream.Position = 0;
                returnStreams.Add(returnStream);
            }
        }
    }
}

然后,我遍历创建的流以创建要发布到服务的任务,每个任务都会发布到服务器。我会等待所有任务完成,然后再次调用我的服务器告诉它我已经完成上传,它可以将所有数据以正确的顺序组合成一个。我的服务有一个上传会话的概念,以跟踪所有部分以及它们将进入的顺序。它还会在每个部分进入时将它们保存到数据库中;在我的例子中是 Azure Blob 存储。

【讨论】:

  • 所以我的问题是如何将流切割成更小的部分。当我尝试发送多个流时,我收到响应已经开始的错误,我似乎无法找到一种方法将数据以较小的块附加到响应中。
  • @reusablePants 我已经更新了我的答案以提供清晰度和代码示例
  • 这可能是我没有正确解释问题的错。我试图基本上发回一堆pdf。每个 byte[] 代表一个 pdf。当我将它们合并为一个字节 [] 时,我没有问题并在客户端收到文档。但是,转换和存储每个字节数组直到我可以将它们发送回来的过程是耗尽所有内存的过程。
  • @reusablePants 你必须在一个 btye[] 中处理它们吗?如果所有 PDF 都在一个字节 [] 中,则真的没有办法减少内存负载。他们需要是PDF吗?您可以尝试将它们转换为更有效的文件类型。
  • 我不必在一个字节[]中处理它们。事实上,理想情况下,我希望一次发回一个字节 [],以便在数据发送到客户端后处理数据。
【解决方案2】:

不清楚为什么将多个MemoryStreams 的内容复制到Response.Body 时会出错。您当然应该能够做到这一点,尽管您需要确保在开始写入数据后不要尝试更改响应标头或状态代码(也不要在开始写入数据后尝试调用 Response.Clear()) .

这是一个启动响应然后写入数据的简单示例:

[ApiController]
[Route("[controller]")]
public class RandomDataController : ControllerBase {
    private readonly ILogger<RandomDataController> logger;
    private const String CharacterData = "abcdefghijklmnopqrstuvwxyz0123456789 ";

    public RandomDataController(ILogger<RandomDataController> logger) {
        this.logger = logger;
    }

    [HttpGet]
    public async Task Get(CancellationToken cancellationToken) {
        this.Response.ContentType = "text/plain";
        this.Response.ContentLength = 1000;

        await this.Response.StartAsync(cancellationToken);
        logger.LogInformation("Response Started");

        var rand = new Random();
        for (var i = 0; i < 1000; i++) {
            // You should be able to copy the contents of a MemoryStream or other buffer here instead of sending random data like this does.
            await this.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(CharacterData[rand.Next(0, CharacterData.Length)].ToString()), cancellationToken);
            Thread.Sleep(50); // This is just to demonstrate that data is being sent to the client as it is written
            cancellationToken.ThrowIfCancellationRequested();

            if (i % 100 == 0 && i > 0) {
                logger.LogInformation("Response In Flight {PercentComplete}", (Double)i / 1000);
            }
        }

        logger.LogInformation("Response Complete");
    }
}

您可以使用 netcat 验证这是否将数据流式传输回客户端:

% nc -nc 127.0.0.1 5000
GET /randomdata HTTP/1.1
Host: localhost:5000
Connection: Close

(在Connection: Close 之后输入一个额外的空行以开始请求)。当数据写入服务器上的Response.Body 时,您应该会看到数据出现在 netcat 中。

需要注意的一点是,这种方法涉及预先计算要发送的数据的长度。如果您无法预先计算响应的大小,或者不愿意,您可以查看Chunked Transfer Encoding,如果您开始将数据写入 Response.Body 而不指定Content-Length,ASP.Net 应该自动使用它.

【讨论】:

  • 我在 Mac 上使用 netcat,因此您的命令行可能会有所不同(某些系统不需要 -c 即可使用)。
  • 我已尝试实现您的示例,但出现错误。当我尝试将表示单个 pdf 文档的每个字节数组写入响应正文时,我的做法略有不同。但是当我使用相同的结构时,我收到错误“无法写入响应正文,响应已完成”
  • 你能分享产生错误的代码吗?
  • public async void FillFormsGetChunkedResponse(string formId, CancellationToken cancellationToken, [FromBody] IList&lt;IDictionary&lt;string, string&gt;&gt; fieldDictionaries) { this.Response.ContentType = "application/pdf"; foreach (IDictionary&lt;string, string&gt; dictionary in fieldDictionaries) { var populatedForm = this.dataRepo.PopulateForm(formId, dictionary); await this.Response.Body.WriteAsync((populatedForm),cancellationToken).AsTask(); Thread.Sleep(50); cancellationToken.ThrowIfCancellationRequested(); } }
  • 嗯,假设 this.dataRepo.PopulateForm 返回 Byte[] 我无法重现您遇到的问题。即使没有指定 ContentLength 并且没有 Response.StartAsync,ASP.Net 也会使用分块编码自动流回结果。我要提到的一件事是你的方法声明应该是public async Task 而不是public async void。创建一个方法async void 是一个非常糟糕的主意,因为调用者无法等待异步代码完成。
猜你喜欢
  • 2021-12-07
  • 2017-11-18
  • 1970-01-01
  • 2016-12-23
  • 1970-01-01
  • 2017-11-23
  • 1970-01-01
  • 2021-07-05
  • 2018-10-12
相关资源
最近更新 更多