如何在 C# 中保存并附加到序列化的 MessagePack 二进制文件?

How to save & append to a serialized MessagePack binary file in C#?

我正在尝试使用 MessagePack 来保存多个结构列表,因为我读到它的性能优于 BinaryFormatter 序列化。

我想做的是接收实时时间序列数据,定时定时存盘(追加)到磁盘,比如一个列表的元素个数是100。我的问题是:

1) 在这种情况下,序列化结构列表并将其异步保存到磁盘是否更好?

2) 如何使用 MessagePack 将其简单地保存到磁盘?

public struct struct_realTime
{
    public int indexNum { get; set; }
    public string currentTime { get; set; }
    public string currentType { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        List<struct_realTime> list_temp = new List<struct_realTime>(100000);

        for (int num=0; num < 100000; num++)
        {
            list_temp.Add(new struct_realTime
            {
                indexNum = 1,
                currentTime = "time",
                currentType = "type",
            });
        }

        string filename = "file.bin";

        using (var fileStream = new FileStream(filename, FileMode.Append, FileAccess.Write))
        {
            byte[] bytes = MessagePackSerializer.Serialize(list_temp);
            Console.WriteLine(MessagePackSerializer.ToJson(bytes));
        }
    }
}

当我 运行 这段代码时,它创建 file.bin 并打印出 100000 个结构,但文件是 0 字节。

当我使用 BinaryFormatter 时,我这样做:

using (var fileStream = new FileStream("file.bin", FileMode.Append))
{
    BinaryFormatter formatter = new BinaryFormatter();
    formatter.Serialize(fileStream, list_temp);
}

我该如何解决这个问题?

您要做的是追加一个使用MessagePackSerializer to a file containing an already-serialized sequence of similar objects, in the same way it is possible with BinaryFormatter, protobuf-net or 序列化的对象(此处List<struct_realTime>)。稍后,您可能希望能够将整个序列反序列化为相同类型的对象列表或数组。

您的代码存在三个问题,两个简单,一个基本。

简单题如下:

  • 您实际上并没有写信给 fileStream。相反,请执行以下操作:

    // Append each list_temp sequentially
    using (var fileStream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite))
    {
        MessagePackSerializer.Serialize(fileStream, list_temp);
    }
    
  • 您还没有用 [MessagePackObject] attributes 标记 struct_realTime。这可以实现,例如如下:

    [MessagePackObject]
    public struct struct_realTime
    {
        [Key(0)]
        public int indexNum { get; set; }
        [Key(1)]
        public string currentTime { get; set; }
        [Key(2)]
        public string currentType { get; set; }
    }
    

完成后,您现在可以重复将 list_temp 序列化到一个文件...但是之后您将无法读取它们!这是因为 MessagePackSerializer 似乎在反序列化根对象时读取了 整个文件 ,跳过了文件中附加的任何其他数据。因此像下面这样的代码将失败,因为从文件中只读取了一个对象:

List<List<struct_realTime>> allItemsInFile = new List<List<struct_realTime>>();
using (var fileStream = File.OpenRead(filename))
{
    while (fileStream.Position < fileStream.Length)
    {
        allItemsInFile.Add(MessagePackSerializer.Deserialize<List<struct_realTime>>(fileStream));                   
    }
}
Assert.IsTrue(allItemsInFile.Count == expectedNumberOfRootItemsInFile);

演示 fiddle #1 here.

像下面这样的代码将失败,因为流中的(第一个)根对象不是对象数组的数组,而只是一个数组:

List<List<struct_realTime>> allItemsInFile;
using (var fileStream = File.OpenRead(filename))
{
    allItemsInFile = MessagePackSerializer.Deserialize<List<List<struct_realTime>>>(fileStream);
}
Assert.IsTrue(allItemsInFile.Count == expectedNumberOfRootItemsInFile);

演示 fiddle #2 here.

由于 MessagePackSerializer 似乎缺乏从流中反序列化多个根对象的能力,您有什么选择?首先,您可以反序列化 List<List<struct_realTime>>,附加到它,然后将整个内容序列化回文件。大概出于性能原因您不想这样做。

其次,使用MessagePack specification directly, you could manually seek to the beginning of the file to parse and rewrite an appropriate array 32 format header,然后查找到文件末尾并使用MessagePackSerializer序列化并追加新项目。以下扩展方法可以完成这项工作:

public static class MessagePackExtensions
{
    const byte Array32 = 0xdd;
    const int Array32HeaderLength = 5;

    public static void AppendToFile<T>(Stream stream, T item)
    {
        if (stream == null)
            throw new ArgumentNullException(nameof(stream));
        if (!stream.CanSeek)
            throw new ArgumentException("!stream.CanSeek");

        stream.Position = 0;
        var buffer = new byte[Array32HeaderLength];
        var read = stream.Read(buffer, 0, Array32HeaderLength);
        stream.Position = 0;
        if (read == 0)
        {
            FormatArray32Header(buffer, 1);
            stream.Write(buffer, 0, Array32HeaderLength);
        }
        else
        {
            var count = ParseArray32Header(buffer, read);
            FormatArray32Header(buffer, count + 1);
            stream.Write(buffer, 0, Array32HeaderLength);
        }

        stream.Position = stream.Length;
        MessagePackSerializer.Serialize(stream, item);
    }

    static void FormatArray32Header(byte [] buffer, uint value)
    {
        buffer[0] = Array32;
        buffer[1] = unchecked((byte)(value >> 24));
        buffer[2] = unchecked((byte)(value >> 16));
        buffer[3] = unchecked((byte)(value >> 8));
        buffer[4] = unchecked((byte)value);
    }

    static uint ParseArray32Header(byte [] buffer, int readCount)
    {
        if (readCount < 5 || buffer[0] != Array32)
            throw new ArgumentException("Stream was not positioned on an Array32 header.");
        int i = 1;
        //
        // by https://whosebug.com/users/23354/marc-gravell
        var value = unchecked((uint)((buffer[i++] << 24) | (buffer[i++] << 16) | (buffer[i++] << 8) | buffer[i++]));
        return value;
    }
}

它可以用来附加你的 list_temp 如下:

// Append each entry sequentially
using (var fileStream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
    MessagePackExtensions.AppendToFile(fileStream, list_temp);
}

稍后,要反序列化整个文件,请执行:

List<List<struct_realTime>> allItemsInFile;
using (var fileStream = File.OpenRead(filename))
{
    allItemsInFile = MessagePackSerializer.Deserialize<List<List<struct_realTime>>>(fileStream);
}

备注:

  • MessagePack 协议有 3 种不同的数组格式:

    • fixarray存储一个长度最多为15个元素的数组。
    • array 16存储一个长度为(2^16)-1个元素的数组。
    • array 32存储一个长度为(2^32)-1个元素的数组。


    扩展方法要求根数组为array 32,以消除当新大小变得大于fixarray或[的容量时重新格式化整个数组的需要=31=]。然而,MessagePackSerializer 将始终写入最紧凑的格式,因此不能保证追加到先前由 MessagePackSerializer 序列化的集合。

  • 如果您想使用不需要数组计数或文件开头大小的快速二进制序列化程序,从而支持开箱即用的追加操作,请考虑 . For details see I have a Single File And need to serialize multiple objects randomly. How can I in c#? and How to append object to a file while serializing using c# protobuf-net?.

    有关如何使用此序列化程序的一般概述,请参阅 https://github.com/protobuf-net/protobuf-net#protobuf-net and Protobuf-net: the unofficial manual。您需要使用类似于 MessagePackSerializer.

  • 的属性来标记您的类型

演示 fiddle #3 here.