如何用protobuf-net多次序列化ConcurrentDictionary到同一文件并反序列化?
解决protobuf-net序列化/反序列化ConcurrentDictionary到同一文件的问题
一、序列化阶段(多次追加到文件)
直接连续调用Serializer.Serialize会导致多个protobuf数据粘在一起,反序列化时无法识别边界。正确做法是每次写入前先记录序列化数据的长度,再写入protobuf内容,为后续读取拆分做标记。
实现代码
using System.IO; using System.Collections.Concurrent; using ProtoBuf; // 假设MyClass已正确标记[ProtoContract]、[ProtoMember]等特性 public class CommunicationRecorder { private readonly string _filePath; public CommunicationRecorder(string filePath) { _filePath = filePath; if (!File.Exists(_filePath)) { using (File.Create(_filePath)) {} } } public void RecordState(ConcurrentDictionary<string, MyClass> state) { using (var fs = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.Read)) { // 用内存流预序列化获取长度 using (var ms = new MemoryStream()) { Serializer.Serialize(ms, state); // 先写入数据长度(用Fixed32格式,protobuf-net原生支持) Serializer.SerializeWithLengthPrefix(fs, (int)ms.Length, PrefixStyle.Fixed32); // 写入实际序列化数据 ms.Position = 0; ms.CopyTo(fs); } } } }
二、反序列化阶段(读取文件重建状态序列)
读取时先读取每个数据块的长度标记,再按长度读取对应字节,反序列化为ConcurrentDictionary,依次获取每次序列化的状态。
实现代码
public class CommunicationPlayer { private readonly string _filePath; public CommunicationPlayer(string filePath) { _filePath = filePath; } public IEnumerable<ConcurrentDictionary<string, MyClass>> Playback() { using (var fs = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { while (fs.Position < fs.Length) { // 先读取长度标记 if (!Serializer.TryDeserializeWithLengthPrefix(fs, PrefixStyle.Fixed32, out int length)) break; // 读取对应长度的字节数据 var buffer = new byte[length]; fs.Read(buffer, 0, length); // 反序列化为字典 using (var ms = new MemoryStream(buffer)) { yield return Serializer.Deserialize<ConcurrentDictionary<string, MyClass>>(ms); } } } } }
三、关键注意事项
- 自定义类型与继承处理:确保所有涉及的类型(包括MyClass、父类、嵌套类型)正确标记特性:
- 基类需添加
[ProtoInclude(子类型编号, typeof(子类型))],示例:[ProtoContract] [ProtoInclude(1, typeof(MyDerivedClass))] public class MyBaseClass { [ProtoMember(1)] public int Id { get; set; } } [ProtoContract] public class MyDerivedClass : MyBaseClass { [ProtoMember(2)] public string Name { get; set; } } - 所有需要序列化的属性/字段必须标记
[ProtoMember(唯一编号)],编号不可重复。
- 基类需添加
- 性能优化:
- 高频写入时可复用FileStream,避免频繁打开关闭文件;
- 直接使用
Serializer.SerializeWithLengthPrefix序列化字典,替代手动写长度的步骤,代码更简洁:
反序列化对应改为:Serializer.SerializeWithLengthPrefix(fs, state, PrefixStyle.Fixed32);while (Serializer.TryDeserializeWithLengthPrefix(fs, PrefixStyle.Fixed32, out ConcurrentDictionary<string, MyClass> state)) { yield return state; }
- 线程安全:ConcurrentDictionary读取本身线程安全,但如果序列化时字典有大量写入操作,可短暂加锁(如
ReaderWriterLockSlim)保证状态一致性。
四、常见问题排查
若反序列化报错,检查以下点:
- 序列化、反序列化时的类结构(特性标记、字段/属性、继承关系)是否完全一致;
- 长度前缀的格式(如Fixed32)是否前后匹配;
- 文件是否损坏(如写入中断导致最后一个数据块不完整),可添加校验和验证。
内容的提问来源于stack exchange,提问作者MrBott_a
相关产品推荐
相关产品推荐

