【问题标题】:How to save & append to a serialized MessagePack binary file in C#?如何在 C# 中保存并附加到序列化的 MessagePack 二进制文件?
【发布时间】:2020-03-05 21:25:55
【问题描述】:

我正在尝试使用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);
}

我该如何解决这个问题?

【问题讨论】:

  • 为什么要附加二进制文件?没有意义。
  • @jdweng 我不是程序员,所以请理解我!我想接收数据并将其连续保存到磁盘。我以前只使用文本文件,但这里有人告诉我二进制文件在性能方面更好。但我不想制作多个二进制文件,这就是我遇到这个问题的方式。你能推荐我其他的方法吗?
  • 问题是检索数据。只需将二进制文件管理器连续放入文件中,您以后可能无法删除数据。除非每个文件都包含一个大小,否则您以后将无法获取数据。
  • 出于此目的(或任何其他目的)不使用 BinaryFormatter 的原因,请参阅 What are the deficiencies of the built-in BinaryFormatter based .Net serialization?

标签: c# serialization msgpack


【解决方案1】:

您要做的是将使用MessagePackSerializer 序列化的对象(此处为List&lt;struct_realTime&gt;追加到包含已序列化的类似对象序列的文件中,方式相同可以使用BinaryFormatterprotobuf-netJson.NET。稍后,您可能希望能够将整个序列反序列化为相同类型的对象列表或数组。

你的代码有三个问题,两个简单,一个基本。

简单的问题如下:

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

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

    [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);

演示小提琴 #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);

演示小提琴 #2 here.

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

其次,直接使用MessagePack specification,你可以手动寻找到文件的开头解析并重写一个合适的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;
        //https://*.com/questions/8241060/how-to-get-little-endian-data-from-big-endian-in-c-sharp-using-bitconverter-toin
        //https://*.com/a/8241127 by https://*.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);
}

注意事项:

演示小提琴#3 here.

【讨论】:

  • 感谢您的详细解释和代码!我从你身上学到了很多!再次感谢您!