将对象列表流式传输为单个响应,并带有进度报告

本文关键字:报告 响应 单个 列表 对象 传输 | 更新日期: 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());
        }
    }
}

上面的示例显示了如何通过将Streambyte[]响应包装在HttpResult中来向HTTP响应添加其他元数据,但是如果您愿意,也可以直接返回byte[]StreamIStreamWriter响应。

直接写入响应流

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; }
}