如何使用 ASP.NET Core 进行流式传输

How to stream with ASP.NET Core

如何在 ASP.NET Core 中正确流式传输响应? 有这样一个控制器(更新代码):

[HttpGet("test")]
public async Task GetTest()
{
    HttpContext.Response.ContentType = "text/plain";
    using (var writer = new StreamWriter(HttpContext.Response.Body))
        await writer.WriteLineAsync("Hello World");            
}

Firefox/Edge 浏览器显示

Hello World

,而Chrome/Postman报错:

The localhost page isn’t working

localhost unexpectedly closed the connection.

ERR_INCOMPLETE_CHUNKED_ENCODING

P.S。我要直播很多内容,所以不能提前指定Content-Lengthheader

类似这样的方法可能有效:

[HttpGet]
public async Task<IActionResult> GetTest()
{
    var contentType = "text/plain";
    using (var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World")))
    return new FileStreamResult(stream, contentType);

}

要流式传输应该像下载文件一样出现在浏览器中的响应,您应该使用 FileStreamResult:

[HttpGet]
public FileStreamResult GetTest()
{
  var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World"));
  return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))
  {
    FileDownloadName = "test.txt"
  };
}

我也想知道如何做到这一点,并且发现了 原始问题的代码实际上在 ASP.NET Core 2.1.0-rc1-final 上运行正常,Chrome(和其他少数浏览器)和 JavaScript 应用程序都不会因此类端点而失败。

我想添加的小东西只是设置 StatusCode 并关闭响应流以使响应完成:

[HttpGet("test")]
public void Test()
{
    Response.StatusCode = 200;
    Response.ContentType = "text/plain";
    using (Response.Body)
    {
        using (var sw = new StreamWriter(Response.Body))
        {
            sw.Write("Hi there!");
        }
    }
}

可以 return nullEmptyResult()(等价),即使之前写入 Response.Body。如果方法 returns ActionResult 也能够轻松使用所有其他结果(例如 BadQuery()),这可能很有用。

[HttpGet("test")]
public ActionResult Test()
{
    Response.StatusCode = 200;
    Response.ContentType = "text/plain";
    using (var sw = new StreamWriter(Response.Body))
    {
        sw.Write("something");
    }
    return null;
}

这个问题有点老了,但我在任何地方都找不到更好的答案来解决我想做的事情。要将当前缓冲的输出发送到客户端,您必须为要写入的每个内容块调用 Flush()。只需执行以下操作:

[HttpDelete]
public void Content()
{
    Response.StatusCode = 200;
    Response.ContentType = "text/html";

    // the easiest way to implement a streaming response, is to simply flush the stream after every write.
    // If you are writing to the stream asynchronously, you will want to use a Synchronized StreamWriter.
    using (var sw = StreamWriter.Synchronized(new StreamWriter(Response.Body)))
    {
        foreach (var item in new int[] { 1, 2, 3, 4, })
        {
            Thread.Sleep(1000);
            sw.Write($"<p>Hi there {item}!</p>");
            sw.Flush();
        }
    };
}

您可以使用以下命令使用 curl 进行测试:curl -NX DELETE <CONTROLLER_ROUTE>/content

@Developer4993 是正确的,要在解析整个响应之前将数据发送到客户端,有必要 Flush 到响应流。然而,他们的回答对于 DELETESynchronized.StreamWriter 都有些反常。此外,如果 I/O 是同步的,Asp.Net Core 3.x 将抛出异常。 这是在 Asp.Net Core 3.1:

中测试的
[HttpGet]
public async Task Get()
{
    Response.ContentType = "text/plain";
    StreamWriter sw;
    await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
    {
        foreach (var item in someReader.Read())
        {
            await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
            await sw.FlushAsync().ConfigureAwait(false);
        }
    }
}

假设 someReader 正在迭代数据库结果或某些 I/O 流,其中包含您不想在发送前缓冲的大量内容,这将向响应写入一大块文本每个 FlushAsync() 流。 出于我的目的,使用 HttpClient 的结果比浏览器兼容性更重要,但如果您发送足够的文本,您将看到 chromium 浏览器以流方式使用结果。浏览器一开始好像缓冲了一定数量。

最新的 IAsyncEnumerable 流变得更有用,您的源要么是时间密集型的,要么是磁盘密集型的,但可以一次产生一点:

[HttpGet]
public async Task<EmptyResult> Get()
{
    Response.ContentType = "text/plain";
    StreamWriter sw;
    await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
    {
        await foreach (var item in GetAsyncEnumerable())
        {
            await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
            await sw.FlushAsync().ConfigureAwait(false);
        }
    }
    return new EmptyResult();
}

您可以将 await Task.Delay(1000) 扔进任一 foreach 以演示连续流。

最后,@StephenCleary 的 FileCallbackResult 与这两个示例的工作方式相同。 Infrastructure 命名空间深处的 FileResultExecutorBase 只是有点可怕。

[HttpGet]
public IActionResult Get()
{
    return new FileCallbackResult(new MediaTypeHeaderValue("text/plain"), async (outputStream, _) =>
    {
        StreamWriter sw;
        await using ((sw = new StreamWriter(outputStream)).ConfigureAwait(false))
        {
            foreach (var item in someReader.Read())
            {
                await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
                await sw.FlushAsync().ConfigureAwait(false);
            }
        }
        outputStream.Close();
    });
}