如何使用 ASP.NET Core 进行流式传输
Posted
技术标签:
【中文标题】如何使用 ASP.NET Core 进行流式传输【英文标题】:How to stream with ASP.NET Core 【发布时间】:2017-03-13 18:58:44 【问题描述】:如何在 ASP.NET Core 中正确地流式传输响应? 有一个这样的控制器(UPDATED CODE):
[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 浏览器显示
你好世界
,同时 Chrome/Postman 报错:
本地主机页面不工作
localhost 意外关闭连接。
ERR_INCOMPLETE_CHUNKED_ENCODING
附:我要流很多内容,所以我不能提前指定 Content-Length 标头。
【问题讨论】:
【参考方案1】:要流式传输应该像下载文件一样出现在浏览器中的响应,您应该使用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"
;
【讨论】:
实际上我的数据源不是Stream
- 它是 SomeReader 或 IEnumerable。然后我需要一个Stream
适配器,这很麻烦。有没有简单的方法可以写信给Response.Body
?附言对于某些人来说,它确实适用于 Chrome :)
@DmitryNogin:我写了一个FileCallbackResult
that may do what you need。
最新版本的 .net core 已过时。
用 Asp.Net Core 3.1 测试了 @StephenCleary 的 FileCallbackResult
,它像这里的其他 StreamWriter
答案一样将连续的结果流向消费者。它基本上是旧的PushStreamContent
的替代品。在消费者看来不一定是文件……而是文本流(或其他)。
但这会将现有的读取流复制到正文流。如何直接写入正文流?【参考方案2】:
@Developer4993 是正确的,要在解析整个响应之前将数据发送到客户端,有必要将Flush
发送到响应流。然而,他们的回答对于DELETE
和Synchronized.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();
);
【讨论】:
someReader.Read() 是什么意思?你能展示一下实现吗? @BigyanDevkotasomeReader.Read()
只是一个占位符,用于任何知道如何迭代大型源数据并返回小块数据而无需先将整个源加载到内存中的东西。可以是 StreamReader
或 DbReader
或 GetAsyncEnumerable()
或您自己的实现。问题是关于将数据流式传输到浏览器,而不是如何获取数据。
我不必实现它,这 3 行代码对我有用 (var sw = new StreamWriter(Response.Body)) sw.Write("something"); sw.Flush(); ,谢谢【参考方案3】:
即使之前写过Response.Body
,也可以返回null
或EmptyResult()
(它们是等价的)。如果该方法返回 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;
【讨论】:
为什么这是问题的答案? 请尝试添加一些解释来回答可能会有所帮助 我在答案中添加了解释。 投反对票。对于初学者来说,代码示例不会产生流式传输结果 - 正如问题所在。相反,它只会将数据流式传输到缓冲的响应中......对于流式传输响应没有真正的帮助。其次,那里的解释并不真正相关。【参考方案4】:我也想知道如何做到这一点,并发现
原始问题的代码实际上在 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!");
【讨论】:
您确定需要using
语句吗? ASP.NET Core 不会为您处理它们。相反,我希望您需要调用 StreamWriter.Close
以便刷新最终输出。 (Dispose
不会刷新。)
@EdwardBrey 虽然我同意 Response.Body 上的“使用”是不需要的,但您可以备份您在 StreamWriter 上的声明吗?该代码似乎只是在 Close() 上调用 Dispose(true) (.Net:github.com/microsoft/referencesource/blob/master/mscorlib/… .NetCore:github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/…)。在我看来,否则这将是非常不合理的。【参考方案5】:
这个问题有点老了,但我无法在任何地方找到更好的答案来解决我想要做的事情。要将当前缓冲的输出发送到客户端,您必须为要写入的每个内容块调用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
【讨论】:
.Net Core 没有 Flush 功能 @AlreadyLost 如果没有Flush()
,它就无法工作,试一试。 [asp.net核心2.2]【参考方案6】:
这样的事情可能会奏效:
[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);
【讨论】:
您的代码将无法工作,因为您在源流被使用之前就将其处理掉了。 仍然不好,因为从技术上讲,在此方法返回之前不会读取流。但是,鉴于MemoryStream
的 Dispose
不会处理流的内容,您将在此处侥幸逃脱。有了真正的 IO,你可能就不会这么幸运了。
...实际上,FileStreamResult
在完成时释放它传递的流。这里的using
声明完全是多余的。以上是关于如何使用 ASP.NET Core 进行流式传输的主要内容,如果未能解决你的问题,请参考以下文章