将对象列表流式传输为单个响应,并带有进度报告
本文关键字:报告 响应 单个 列表 对象 传输 | 更新日期: 2023-09-27 18:32:47
我的应用程序具有"导出"功能。在功能方面,它的工作原理如下:
当用户按下"导出"按钮(配置选项等后(时,应用程序首先运行一个相对快速的查询,以确定需要导出的所有对象的 ID。然后,对于每个对象,它执行一个计算,该计算可能需要相对较长的时间才能完成(每个对象最多 1 秒(。当这种情况发生时,用户正在观察一个进度条——这很容易渲染,因为我们知道预期的对象数量,以及到目前为止已经处理了多少对象。
出于所有常见原因,我想将此功能移动到 Web 服务。但是,在此过程中的另一个问题是,我们的用户通常有很多网络延迟。因此,如果我有 1000 行要处理,我就无法发出 1000 个请求。
我想做的是从服务返回自定义流。我可以将行计数写入流的前 4 个字节。客户端将读取这 4 个字节,初始化进度栏,然后继续读取流并动态反序列化它们,在反序列化每个字节时更新进度条。同时,服务器会在对象可用时将对象写入流中。
更有趣的是,由于我要发回一长串对象,我真的很想使用 protobuf-net 来减少开销。因此,我有几个问题:
- 我打算做的事情甚至可能吗?这有意义吗,还是有更好的方法?
- 如何从服务堆栈服务返回自定义流?
- 当我在客户端反序列化对象流时,如何在反序列化每个对象时获得某种通知?我需要它来更新进度条。
找到了这个答案,它做了我想要的,但并没有真正解决我的问题:使用 protobuf-net 进行懒惰的流驱动对象序列化
编辑:我应该提到我的客户端是一个桌面C#应用程序,它使用ServiceStack和 protobuf.net。
我建议在多个请求上对结果集进行分页(即使用 Skip/Take(,而不是尝试返回需要自定义响应、自定义序列化和自定义客户端来使用流响应的结果流。这是一种更无状态的方法,更适合HTTP,其中每个查询都可以独立缓存,更好地支持重试,即如果其中一个请求出现错误,您可以从上次成功的响应重试(即不必再次下载整个请求(以及更好的可调试性和现有HTTP工具的内省。
自定义流式处理响应
以下示例演示如何返回可观察的 StreamWriter 和自定义的可观察客户端以使用流式处理的响应:https://gist.github.com/bamboo/5078236它使用自定义 JSON 序列化来确保每个元素在刷新到流之前写入,以便使用流的客户端可以期望每次读取检索整个记录。如果使用二进制序列化程序(如协议缓冲区(,则此自定义序列化将更加困难。
在服务堆栈中返回二进制和流响应
ImageService 显示了在 ServiceStack 中返回二进制或流响应的不同方式:
在 httpResult 中返回流
public object Any(ImageAsStream request)
{
using (var image = new Bitmap(100, 100))
{
using (var g = Graphics.FromImage(image))
{
g.Clear(request.Format.ToImageColor());
}
var ms = new MemoryStream();
image.Save(ms, request.Format.ToImageFormat());
return new HttpResult(ms, request.Format.ToImageMimeType());
}
}
返回原始字节[]
public object Any(ImageAsBytes request)
{
using (var image = new Bitmap(100, 100))
{
using (var g = Graphics.FromImage(image))
{
g.Clear(request.Format.ToImageColor());
}
using (var m = new MemoryStream())
{
image.Save(m, request.Format.ToImageFormat());
var imageData = m.ToArray(); //buffers
return new HttpResult(imageData, request.Format.ToImageMimeType());
}
}
}
上面的示例显示了如何通过将Stream
和byte[]
响应包装在HttpResult
中来向HTTP响应添加其他元数据,但是如果您愿意,也可以直接返回byte[]
,Stream
或IStreamWriter
响应。
直接写入响应流
public void Any(ImageWriteToResponse request)
{
using (var image = new Bitmap(100, 100))
{
using (var g = Graphics.FromImage(image))
{
g.Clear(request.Format.ToImageColor());
}
base.Response.ContentType = request.Format.ToImageMimeType();
image.Save(base.Response.OutputStream, request.Format.ToImageFormat());
base.Response.Close();
}
}
返回自定义结果
public object Any(ImageAsCustomResult request)
{
var image = new Bitmap(100, 100);
using (var g = Graphics.FromImage(image))
{
g.Clear(request.Format.ToImageColor());
return new ImageResult(image, request.Format.ToImageFormat());
}
}
可以通过实现 IStreamWriter.WriteTo()
直接写入响应流的位置:
//Your own Custom Result, writes directly to response stream
public class ImageResult : IDisposable, IStreamWriter, IHasOptions
{
private readonly Image image;
private readonly ImageFormat imgFormat;
public ImageResult(Image image, ImageFormat imgFormat = null)
{
this.image = image;
this.imgFormat = imgFormat ?? ImageFormat.Png;
this.Options = new Dictionary<string, string> {
{ HttpHeaders.ContentType, this.imgFormat.ToImageMimeType() }
};
}
public void WriteTo(Stream responseStream)
{
using (var ms = new MemoryStream())
{
image.Save(ms, imgFormat);
ms.WriteTo(responseStream);
}
}
public void Dispose()
{
this.image.Dispose();
}
public IDictionary<string, string> Options { get; set; }
}