使用 IAsyncEnumerable 流式处理文本行
Streaming lines of text using IAsyncEnumerable
我想将任意数量的纯文本行从 ASP.NET 服务器流式传输到 Blazor WebAssembly 客户端 (.NET 6.0)。
为了测试,我实现了以下虚拟 API:
[HttpGet("lines")]
public async IAsyncEnumerable<string> GetLines() {
for (var i = 0; i < 10; ++i) {
yield return "test\n";
await Task.Delay(1000);
}
}
在客户端我尝试了以下方法(遵循these ideas):
public async IAsyncEnumerable<string?> GetLines() {
var response = await HttpClient.GetAsync($"{apiRoot}/lines", HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode) {
var responseStream = await response.Content.ReadAsStreamAsync();
var lines = JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream);
await foreach (var line in lines) {
yield return line;
}
}
else {
Log.Error($"Server response code: {response.StatusCode}");
yield return null;
}
}
不幸的是,response.Content.ReadAsStreamAsync()
没有立即返回,而是缓冲了整个流(即 10 行“test\n”),在这种情况下需要 10 秒,然后缓冲内容被反序列化为IAsyncEnumerable<string>
.
可以使用 HttpClient.GetStreamAsync
:
观察到相同的行为
public async IAsyncEnumerable<string?> GetLines() {
var responseStream = await HttpClient.GetStreamAsync($"{apiRoot}/lines"); // buffers for 10 s
var linesAsync = JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream);
await foreach (var line in lines) {
yield return line;
}
}
我怎样才能改变这一点,以便从服务器发送的每一行都立即在客户端处理,没有任何缓冲?
这是客户端还是服务器端的问题?有没有办法禁用这种缓冲行为?
编辑: 经过更多实验,我发现直接调用 API(例如通过浏览器)确实显示了预期的流式传输行为,即个人一行一行地弹出,延迟 1.0 秒。所以这似乎是一个客户端问题,确实如此。
我找到了适合我的解决方法,因为我不需要任何 JSON 反序列化,因为我想流式传输原始字符串。
以下实现解决了客户端流式传输问题:
public async IAsyncEnumerable<string?> GetLines() {
using var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/lines");
request.SetBrowserResponseStreamingEnabled(true);
var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode) {
using var responseStream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);
string? line = null;
while ((line = await reader.ReadLineAsync()) != null) {
yield return line;
}
}
else {
Log.Error($"Server response code: {response.StatusCode}");
yield return null;
}
}
我想将任意数量的纯文本行从 ASP.NET 服务器流式传输到 Blazor WebAssembly 客户端 (.NET 6.0)。
为了测试,我实现了以下虚拟 API:
[HttpGet("lines")]
public async IAsyncEnumerable<string> GetLines() {
for (var i = 0; i < 10; ++i) {
yield return "test\n";
await Task.Delay(1000);
}
}
在客户端我尝试了以下方法(遵循these ideas):
public async IAsyncEnumerable<string?> GetLines() {
var response = await HttpClient.GetAsync($"{apiRoot}/lines", HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode) {
var responseStream = await response.Content.ReadAsStreamAsync();
var lines = JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream);
await foreach (var line in lines) {
yield return line;
}
}
else {
Log.Error($"Server response code: {response.StatusCode}");
yield return null;
}
}
不幸的是,response.Content.ReadAsStreamAsync()
没有立即返回,而是缓冲了整个流(即 10 行“test\n”),在这种情况下需要 10 秒,然后缓冲内容被反序列化为IAsyncEnumerable<string>
.
可以使用 HttpClient.GetStreamAsync
:
public async IAsyncEnumerable<string?> GetLines() {
var responseStream = await HttpClient.GetStreamAsync($"{apiRoot}/lines"); // buffers for 10 s
var linesAsync = JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream);
await foreach (var line in lines) {
yield return line;
}
}
我怎样才能改变这一点,以便从服务器发送的每一行都立即在客户端处理,没有任何缓冲? 这是客户端还是服务器端的问题?有没有办法禁用这种缓冲行为?
编辑: 经过更多实验,我发现直接调用 API(例如通过浏览器)确实显示了预期的流式传输行为,即个人一行一行地弹出,延迟 1.0 秒。所以这似乎是一个客户端问题,确实如此。
我找到了适合我的解决方法,因为我不需要任何 JSON 反序列化,因为我想流式传输原始字符串。
以下实现解决了客户端流式传输问题:
public async IAsyncEnumerable<string?> GetLines() {
using var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/lines");
request.SetBrowserResponseStreamingEnabled(true);
var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode) {
using var responseStream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);
string? line = null;
while ((line = await reader.ReadLineAsync()) != null) {
yield return line;
}
}
else {
Log.Error($"Server response code: {response.StatusCode}");
yield return null;
}
}