C#实现CSV到加密DAT文件的二进制转换与AES加密实践

📅 2026/7/13 9:35:09
C#实现CSV到加密DAT文件的二进制转换与AES加密实践
1. 项目概述为什么我们需要处理DAT文件在桌面应用、嵌入式上位机、游戏数据存储乃至一些特定的设备配置场景里.dat文件的身影无处不在。它不像.txt或.json那样一目了然也不像数据库那样有严格的结构.dat更像是一个“百宝袋”里面可以装下任何格式的原始二进制数据。最近我在为一个工业设备开发上位机数据配置工具时就遇到了一个典型需求设备固件要求将配置参数以加密的二进制.dat格式下发而产线工程师习惯用Excel编辑最终保存为CSV。我的任务就是架起这座从可读的CSV到加密的二进制DAT的桥梁。这不仅仅是简单的格式转换。CSV是明文、结构化的文本而DAT是紧凑、非结构化的二进制流。直接转换会丢失“紧凑”和“非结构化”的优势更关键的是设备端要求数据必须加密以防止参数被随意篡改。所以这个项目的核心挑战在于三点第一如何设计一个灵活的数据结构既能描述CSV中的各种类型整数、浮点数、字符串又能高效地序列化成二进制第二如何在这个转换过程中无缝地加入加密层确保数据在写入磁盘或传输时就已是密文第三如何实现反向的、可靠的解密与读取将二进制的“乱码”还原成可操作的内存对象。用C#来做这件事可以说是“专业对口”。System.IO命名空间下的BinaryWriter和BinaryReader为二进制读写提供了原子操作FileStream给了我们精细控制文件的能力而System.Security.Cryptography则封装了强大的加密算法。整个流程就像是一条精心设计的流水线原始数据从CSV“卸货”经过“结构化打包”序列化送入“加密车间”处理最后被“装箱”写入成DAT文件。反向流程则是“拆箱”、“解密”、“解包”。接下来我就把这套流水线的每个工位以及我调试过程中踩过的坑、总结的技巧毫无保留地分享出来。2. 核心设计构建可扩展的数据转换与加密架构直接上手写文件流和加密代码是鲁莽的。一个健壮的系统必须始于清晰的设计。我们的目标不是写一个一次性的脚本而是一个能应对多种数据类型、便于维护和扩展的模块。2.1 数据模型定义使用泛型与特性进行元数据描述首先我们需要一个模型来描述要转换的每一条数据。假设我们的CSV列包括ID(int),Name(string),Value(float),Timestamp(long)。在二进制世界里我们需要明确知道每个字段的类型和长度特别是变长类型如字符串。我选择定义一个泛型基类DataItemT并结合自定义特性来标记字段的二进制存储规则。using System; using System.Reflection; // 自定义特性用于标记字段在二进制流中的顺序和字符串长度 [AttributeUsage(AttributeTargets.Property)] public class BinaryOrderAttribute : Attribute { public int Order { get; } // 写入顺序 public int FixedStringLength { get; } // 固定长度字符串的尺寸-1表示变长 public BinaryOrderAttribute(int order, int fixedLength -1) { Order order; FixedStringLength fixedLength; } } // 数据项基类 public abstract class DataItemBase { // 所有派生类共享的序列化/反序列化逻辑可以放在这里 public abstract byte[] ToBytes(); public abstract void FromBytes(byte[] data); } // 具体的数据模型 public class DeviceConfig : DataItemBase { [BinaryOrder(1)] public int Id { get; set; } [BinaryOrder(2, 50)] // 固定长度为50字节的字符串不足部分补空字符 public string Name { get; set; } [BinaryOrder(3)] public float Value { get; set; } [BinaryOrder(4)] public long Timestamp { get; set; } // 实现序列化方法简化版后续会结合BinaryWriter public override byte[] ToBytes() { // 预留实现 return new byte[0]; } public override void FromBytes(byte[] data) { // 预留实现 } }设计考量为什么用特性Attribute而不是在代码里硬编码长度特性将元数据存储顺序、长度限制与数据模型本身声明在一起职责清晰。当模型变更时只需修改类定义无需深入业务逻辑代码。固定长度字符串虽然可能浪费空间但在二进制处理中非常普遍因为它简化了读取逻辑——你总是知道该读多少字节。2.2 加密方案选型AES与流式加密的配合加密是项目的安全核心。选择算法时我主要权衡了安全性、性能和.NET原生支持度。AESAdvanced Encryption Standard是目前对称加密的黄金标准速度快、安全性高且.NET的Aes类提供了完整实现。更关键的是加密模式的选择。这里有个大坑不要使用ECB模式。ECB模式对相同的明文块会产生相同的密文块在结构化数据中会导致模式泄露安全性很差。我选择CBC密码分组链接模式它需要一个初始化向量IV来增加随机性。IV不需要保密但必须唯一且不可预测通常和密文一起存储。为了将加密无缝集成到文件流中我采用CryptoStream。它可以包装任何一个Stream对象如FileStream在数据写入底层流之前自动加密在读取时自动解密。这种“流式”处理方式非常优雅无需我们手动将整个文件读入内存再加密。using System.Security.Cryptography; using System.IO; public class AesEncryptionHelper { private readonly byte[] _key; // 密钥必须妥善保存 private readonly byte[] _iv; // 初始化向量 public AesEncryptionHelper(byte[] key, byte[] iv) { if (key null || key.Length ! 32) // AES-256 需要32字节密钥 throw new ArgumentException(Key must be 32 bytes (256 bits) for AES-256.); if (iv null || iv.Length ! 16) // AES块大小是128位即16字节 throw new ArgumentException(IV must be 16 bytes (128 bits).); _key (byte[])key.Clone(); _iv (byte[])iv.Clone(); } // 创建一个用于加密写入的CryptoStream public CryptoStream CreateEncryptionStream(Stream baseStream) { using Aes aes Aes.Create(); aes.Key _key; aes.IV _iv; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; // 常见的填充方式 ICryptoTransform encryptor aes.CreateEncryptor(aes.Key, aes.IV); // CryptoStreamMode.Write 表示对这个流进行写入操作会被加密 return new CryptoStream(baseStream, encryptor, CryptoStreamMode.Write); } // 创建一个用于解密读取的CryptoStream public CryptoStream CreateDecryptionStream(Stream baseStream) { using Aes aes Aes.Create(); aes.Key _key; aes.IV _iv; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; ICryptoTransform decryptor aes.CreateDecryptor(aes.Key, aes.IV); // CryptoStreamMode.Read 表示从这个流读取操作会先解密 return new CryptoStream(baseStream, decryptor, CryptoStreamMode.Read); } }注意密钥管理是命门。绝对不要将密钥硬编码在代码中。在生产环境中密钥应来自安全的配置源如Azure Key Vault、HashiCorp Vault或由硬件安全模块HSM管理。IV可以随机生成但必须和密文一起保存通常写在文件开头。2.3 文件格式设计兼顾灵活性与读取效率一个完整的DAT文件不能只是一坨加密后的二进制数据。为了能正确解密和解析我们需要一个简单的“文件头”。我的设计如下偏移量字节长度字节内容说明04魔数 (Magic Number)例如0x44 0x41 0x54 0x31(DAT1)用于快速识别文件类型。416初始化向量 (IV)用于AES-CBC解密的IV。即使密钥相同不同文件的IV也应不同。204数据项数量 (N)文件中存储的数据记录条数int类型。244版本号 (Version)文件格式版本便于后续格式升级兼容。28开始加密的数据体使用上述IV和密钥加密后的实际数据。数据体内部分就是连续存储的N个DeviceConfig对象的二进制形式。这种设计的好处是在读取时可以先读取文件头获取IV和数据项数量然后初始化解密流再按数量循环读取数据项。结构清晰解析高效。3. 从CSV到加密DAT完整写入流程实现有了前面的设计现在我们可以动手实现核心的写入流程了。这个过程可以分解为解析CSV、转换对象、序列化、加密写入。3.1 CSV解析与数据清洗.NET本身没有专门的CSV解析器但我们可以用TextFieldParser在Microsoft.VisualBasic.FileIO中C#项目可引用或者简单的string.Split。对于简单的、格式规范的CSVSplit足够快。但要注意处理字段内包含逗号或换行符的情况这时最好使用成熟的库如CsvHelper。using System.Collections.Generic; using System.IO; using System.Linq; public ListDeviceConfig ParseCsvToConfigs(string csvFilePath) { var configs new ListDeviceConfig(); var lines File.ReadAllLines(csvFilePath); // 假设第一行是标题行跳过 foreach (var line in lines.Skip(1)) { var parts line.Split(,); if (parts.Length 4) continue; // 简单的错误处理 var config new DeviceConfig { Id int.Parse(parts[0]), Name parts[1].PadRight(50).Substring(0, 50), // 处理为固定50字符 Value float.Parse(parts[2]), Timestamp long.Parse(parts[3]) }; configs.Add(config); } return configs; }实操心得数据清洗至关重要。CSV来源可能很“脏”比如字符串超长、数字格式错误、空值等。PadRight和Substring是为了确保字符串长度固定。更健壮的做法是在DeviceConfig类中定义最大长度常量并在属性的setter中进行验证和截断。对于数字解析务必使用TryParse而不是Parse并做好错误日志记录避免因单行数据错误导致整个流程崩溃。3.2 二进制序列化使用BinaryWriter进行精确控制这是将内存对象转换为字节流的关键步骤。我们需要为DataItemBase实现真正的ToBytes方法并利用BinaryOrderAttribute。public override byte[] ToBytes() { using (var memoryStream new MemoryStream()) using (var writer new BinaryWriter(memoryStream)) { // 通过反射获取属性并按Order排序 var properties this.GetType() .GetProperties() .Where(p p.IsDefined(typeof(BinaryOrderAttribute), false)) .OrderBy(p p.GetCustomAttributeBinaryOrderAttribute().Order); foreach (var prop in properties) { var attr prop.GetCustomAttributeBinaryOrderAttribute(); var value prop.GetValue(this); switch (Type.GetTypeCode(prop.PropertyType)) { case TypeCode.Int32: writer.Write((int)value); break; case TypeCode.Single: writer.Write((float)value); break; case TypeCode.Int64: writer.Write((long)value); break; case TypeCode.String: string strValue (string)value ?? string.Empty; if (attr.FixedStringLength 0) { // 固定长度写入固定数量的字节用空字符填充 byte[] stringBytes System.Text.Encoding.UTF8.GetBytes(strValue); int bytesToWrite Math.Min(stringBytes.Length, attr.FixedStringLength); writer.Write(stringBytes, 0, bytesToWrite); // 填充剩余空间 for (int i bytesToWrite; i attr.FixedStringLength; i) writer.Write((byte)0); } else { // 变长先写入字符串长度再写入字节 writer.Write(strValue); } break; // 其他类型... default: throw new NotSupportedException($Type {prop.PropertyType} is not supported for binary serialization.); } } return memoryStream.ToArray(); } }为什么用BinaryWriter而不是BitConverterBinaryWriter封装了将基础类型写入流的细节并自动处理字节序.NET默认是小端序与x86架构一致。它更面向流式操作代码更简洁。如果你需要控制字节序大端序常用于网络协议则需要使用BitConverter并结合Array.Reverse。3.3 整合加密与文件写入现在我们将所有部分串联起来实现完整的加密DAT文件生成。public void WriteEncryptedDatFile(string outputDatPath, ListDeviceConfig configs, byte[] key) { // 1. 生成随机IV byte[] iv new byte[16]; using (var rng RandomNumberGenerator.Create()) { rng.GetBytes(iv); } // 2. 准备加密助手 var aesHelper new AesEncryptionHelper(key, iv); // 3. 创建文件流并写入文件头未加密部分 using (var fileStream new FileStream(outputDatPath, FileMode.Create, FileAccess.Write)) using (var writer new BinaryWriter(fileStream)) { // 写入魔数 DAT1 writer.Write(new byte[] { 0x44, 0x41, 0x54, 0x31 }); // 写入IV writer.Write(iv); // 写入数据项数量 writer.Write(configs.Count); // 写入版本号例如1 writer.Write(1); // 4. 创建加密流后续写入加密流的数据会被自动加密并写入底层fileStream using (var cryptoStream aesHelper.CreateEncryptionStream(fileStream)) using (var cryptoWriter new BinaryWriter(cryptoStream)) { foreach (var config in configs) { byte[] itemBytes config.ToBytes(); // 注意这里写入的是数据项的原始字节不需要再调用cryptoWriter.Write(int)等。 // 因为cryptoStream会加密所有流过它的字节。 cryptoWriter.Write(itemBytes); } // 非常重要必须FlushFinalBlock来确保加密的填充块被写入。 // 但BinaryWriter和CryptoStream的Dispose会处理。显式调用可以更可控。 cryptoStream.FlushFinalBlock(); } // fileStream和writer会自动关闭 } Console.WriteLine($加密DAT文件已生成: {outputDatPath}); }关键陷阱CryptoStream的关闭顺序。BinaryWriter和CryptoStream都必须正确关闭通过using语句或显式Dispose才能确保所有数据尤其是加密的最后一个填充块被完整写入底层流。如果先关闭了底层的FileStream再关闭CryptoStream会导致写入失败或数据损坏。使用嵌套的using语句是最安全的做法。4. 从加密DAT到内存对象完整读取与解密流程读取是写入的逆过程但需要更小心地处理边界和错误。4.1 读取文件头与初始化解密流首先我们需要读取未加密的文件头获取IV和数据项数量。public (byte[] iv, int itemCount, int version) ReadDatFileHeader(string inputDatPath) { using (var fileStream new FileStream(inputDatPath, FileMode.Open, FileAccess.Read)) using (var reader new BinaryReader(fileStream)) { // 读取并验证魔数 byte[] magic reader.ReadBytes(4); if (!magic.SequenceEqual(new byte[] { 0x44, 0x41, 0x54, 0x31 })) throw new InvalidDataException(不是有效的DAT1格式文件。); byte[] iv reader.ReadBytes(16); int itemCount reader.ReadInt32(); int version reader.ReadInt32(); return (iv, itemCount, version); } }4.2 实现反序列化BinaryReader与反射的配合我们需要为DeviceConfig实现FromBytes方法其逻辑与ToBytes对称。public override void FromBytes(byte[] data) { using (var memoryStream new MemoryStream(data)) using (var reader new BinaryReader(memoryStream)) { var properties this.GetType() .GetProperties() .Where(p p.IsDefined(typeof(BinaryOrderAttribute), false)) .OrderBy(p p.GetCustomAttributeBinaryOrderAttribute().Order); foreach (var prop in properties) { var attr prop.GetCustomAttributeBinaryOrderAttribute(); object value null; switch (Type.GetTypeCode(prop.PropertyType)) { case TypeCode.Int32: value reader.ReadInt32(); break; case TypeCode.Single: value reader.ReadSingle(); break; case TypeCode.Int64: value reader.ReadInt64(); break; case TypeCode.String: if (attr.FixedStringLength 0) { byte[] stringBytes reader.ReadBytes(attr.FixedStringLength); // 找到第一个空字符(0)的位置截断 int length Array.IndexOf(stringBytes, (byte)0); if (length 0) length stringBytes.Length; value System.Text.Encoding.UTF8.GetString(stringBytes, 0, length); } else { value reader.ReadString(); // BinaryReader.ReadString()会先读长度前缀 } break; default: throw new NotSupportedException($Type {prop.PropertyType} is not supported for binary deserialization.); } prop.SetValue(this, value); } } }4.3 整合解密与数据读取最后使用从文件头获取的IV和密钥创建解密流读取并解析数据体。public ListDeviceConfig ReadEncryptedDatFile(string inputDatPath, byte[] key) { var configs new ListDeviceConfig(); // 1. 读取文件头 var (iv, itemCount, version) ReadDatFileHeader(inputDatPath); Console.WriteLine($文件版本: {version}, 包含 {itemCount} 条记录。); // 2. 准备解密 var aesHelper new AesEncryptionHelper(key, iv); // 3. 打开文件流并定位到数据体开始位置文件头之后 using (var fileStream new FileStream(inputDatPath, FileMode.Open, FileAccess.Read)) { // 跳过文件头 (4魔数 16IV 4数量 4版本 28字节) fileStream.Seek(28, SeekOrigin.Begin); // 4. 创建解密流 using (var cryptoStream aesHelper.CreateDecryptionStream(fileStream)) using (var cryptoReader new BinaryReader(cryptoStream)) { for (int i 0; i itemCount; i) { // 关键问题我们怎么知道一个数据项有多长 // 方案A如果所有项长度固定可以直接计算。 // 方案B在写入时在每个数据项前写入其长度前缀推荐。 // 这里采用方案B我们在ToBytes后得到长度写入时先写长度。 // 因此我们需要修改写入逻辑在cryptoWriter.Write(itemBytes)前先Write(itemBytes.Length) // 读取时先读长度再读对应字节数。 int itemLength cryptoReader.ReadInt32(); // 先读取该项的字节长度 byte[] itemData cryptoReader.ReadBytes(itemLength); if (itemData.Length ! itemLength) { // 可能文件损坏或解密失败 throw new EndOfStreamException($预期读取 {itemLength} 字节但只读到 {itemData.Length} 字节。); } var config new DeviceConfig(); config.FromBytes(itemData); configs.Add(config); } } } return configs; }重要改进在写入循环中我们需要修改代码在写入每个itemBytes之前先写入它的长度cryptoWriter.Write(itemBytes.Length)。这样在读取时我们才能准确地知道该读取多少字节来重构一个完整的数据项。这是处理变长或复杂结构二进制数据的常用技巧。5. 性能优化与多线程处理实战当处理成千上万条记录时性能就成为必须考虑的问题。I/O操作文件读写和加密解密是主要瓶颈。5.1 使用缓冲流BufferedStream减少磁盘I/O直接在FileStream上叠加CryptoStream和BinaryWriter/Reader每次写入一个int或float都可能引发一次实际的磁盘写入效率极低。BufferedStream可以在内存中缓冲数据攒到一定量后再一次性写入磁盘显著提升性能。// 优化后的写入流程片段 using (var fileStream new FileStream(outputDatPath, FileMode.Create, FileAccess.Write)) using (var bufferedStream new BufferedStream(fileStream, 81920)) // 80KB缓冲区 using (var writer new BinaryWriter(bufferedStream)) { // ... 写入文件头 ... using (var cryptoStream aesHelper.CreateEncryptionStream(bufferedStream)) // 包装缓冲流 using (var cryptoWriter new BinaryWriter(cryptoStream)) { // ... 写入加密数据 ... } // 注意BufferedStream在Dispose时会自动Flush剩余数据。 }缓冲区大小选择默认缓冲区大小是4096字节4KB。对于大文件可以适当调大如81920字节80KB。但也不是越大越好需要根据可用内存和典型文件大小权衡。5.2 利用并行处理加速数据转换CSV解析和对象序列化是CPU密集型操作可以并行化。但写入文件是顺序操作不能并行。我们可以采用“生产者-消费者”模式。using System.Collections.Concurrent; using System.Threading.Tasks; public void WriteEncryptedDatFileParallel(string outputDatPath, ListDeviceConfig configs, byte[] key) { // ... 生成IV写入文件头这部分必须单线程... // 使用并行循环处理序列化 var serializedItems new ConcurrentBagbyte[](); Parallel.ForEach(configs, config { byte[] itemBytes config.ToBytes(); // 注意这里需要预先计算并添加长度前缀因为写入时需要。 using (var ms new MemoryStream()) using (var bw new BinaryWriter(ms)) { bw.Write(itemBytes.Length); bw.Write(itemBytes); serializedItems.Add(ms.ToArray()); } }); // 顺序写入必须保证顺序否则文件内容错乱 using (var fileStream new FileStream(outputDatPath, FileMode.Append, FileAccess.Write)) // 注意是Append模式因为文件头已写入 using (var bufferedStream new BufferedStream(fileStream)) using (var cryptoStream aesHelper.CreateEncryptionStream(bufferedStream)) { // 按原始顺序写入 foreach (var config in configs) { // 这里需要从serializedItems中按对应顺序取出数据ConcurrentBag不保证顺序。 // 因此并行化方案需要调整要么放弃顺序如果设备读取不依赖顺序要么使用并行计算但保留索引。 // 更简单的做法是只并行化ToBytes计算写入仍按顺序。 } } }并行化陷阱二进制文件写入必须严格保持顺序。上面的简单并行化会打乱顺序。一个更可行的方案是使用Parallel.ForEach配合线程本地存储和排序或者使用PLINQ的AsParallel().AsOrdered()但最终写入仍需一个同步循环。对于I/O瓶颈为主的场景并行化序列化带来的收益可能并不明显需要实际性能剖析。5.3 内存池ArrayPool减少GC压力在频繁序列化和反序列化大量小对象时会创建大量byte[]给垃圾回收GC带来压力。可以使用System.Buffers.ArrayPoolbyte.Shared来租用和归还数组复用内存。// 在ToBytes方法中的优化 public override byte[] ToBytes() { // 估算最大所需字节数例如4(int)50(string)4(float)8(long)66再加一些裕量。 const int maxBufferSize 128; byte[] buffer ArrayPoolbyte.Shared.Rent(maxBufferSize); try { using (var memoryStream new MemoryStream(buffer)) { memoryStream.SetLength(0); // 重置流位置但复用底层数组 using (var writer new BinaryWriter(memoryStream)) { // ... 写入逻辑 ... } // 返回实际使用的部分 return buffer.Take((int)memoryStream.Position).ToArray(); // 注意这里返回了新数组仍然有分配。更极致的做法是让调用方传入缓冲区。 // 但对于API简洁性此优化在复杂场景下收益需评估。 } } finally { ArrayPoolbyte.Shared.Return(buffer); } }使用建议对于性能极其敏感、且ToBytes被高频调用的场景此优化才有明显价值。大多数情况下保持代码简洁更重要。6. 异常处理、日志记录与数据完整性校验工业级代码必须健壮。文件可能损坏磁盘可能写满网络可能中断。6.1 结构化异常处理在每个可能失败的I/O和加密操作周围使用try-catch。try { WriteEncryptedDatFile(output.dat, configs, secretKey); } catch (IOException ioEx) { _logger.LogError(ioEx, 写入文件时发生I/O错误。路径{Path}, output.dat); // 通知用户可能磁盘已满或文件被占用 throw new ApplicationException(无法保存配置文件请检查磁盘空间和文件权限。, ioEx); } catch (CryptographicException cryptEx) { _logger.LogError(cryptEx, 加密过程中发生错误。); // 可能是密钥错误或数据损坏 throw new ApplicationException(数据加密失败请验证密钥。, cryptEx); } catch (UnauthorizedAccessException authEx) { _logger.LogError(authEx, 没有权限访问文件。); throw; } // 更上层可以捕获所有未处理异常6.2 添加数据校验和Checksum为了防止数据在存储或传输过程中损坏可以在文件尾附加一个校验和例如CRC32或SHA256哈希。// 写入完成后计算整个加密数据体的哈希并追加到文件末尾未加密 public void WriteWithChecksum(string outputDatPath, ListDeviceConfig configs, byte[] key) { // ... 写入文件头和加密数据体 ... // 假设加密数据体已写入fileStream当前位置在数据体之后 // 计算加密数据体的哈希需要将数据体缓存在内存或临时文件中 // 简化方案在写入加密数据体时同时计算其哈希。 using (var sha256 SHA256.Create()) { byte[] hash sha256.ComputeHash(encryptedDataBodyStream); // 将哈希值追加到文件末尾明文 writer.Write(hash); // 这里的writer是包装了原始FileStream的BinaryWriter } } // 读取时验证 public bool VerifyChecksum(string inputDatPath, byte[] key) { // ... 读取文件头解密数据体 ... // 读取文件末尾的哈希值 // 重新计算解密后或加密数据体的哈希值 // 比较两者是否一致 }注意校验和放在文件尾且未加密只能验证数据在写入后是否被意外修改不能防止恶意篡改。如果需要防篡改应使用基于密钥的消息认证码HMAC。6.3 详细的日志记录使用像Microsoft.Extensions.Logging这样的日志框架记录关键操作步骤、耗时、数据量以及警告和错误。_logger.LogInformation(开始转换CSV文件 {CsvPath} 到加密DAT文件 {DatPath}。, csvPath, datPath); _logger.LogDebug(共读取 {Count} 条配置记录。, configs.Count); var stopwatch Stopwatch.StartNew(); // ... 执行转换 ... stopwatch.Stop(); _logger.LogInformation(文件转换完成耗时 {ElapsedMs} 毫秒。, stopwatch.ElapsedMilliseconds);良好的日志是线上排查问题的第一手资料。7. 常见问题排查与调试技巧在实际开发中我遇到了不少“坑”。这里总结几个典型问题及其解决方法。7.1 “填充无效无法移除” CryptographicException这是解密时最常见的错误。原因1密钥或IV错误。这是最直接的原因。确保读取文件时使用的IV和密钥与写入时完全一致。IV是保存在文件头中的务必正确读取。原因2数据被截断或损坏。如果文件没有完整写入如程序崩溃、磁盘满解密流无法找到正确的填充结尾。确保写入流程中FlushFinalBlock()被正确调用并且所有流都被正确关闭使用using语句。原因3加密和解密模式/填充方式不匹配。确保两端都使用相同的CipherMode如CBC和PaddingMode如PKCS7。我曾在测试时一端用CBC另一端误用ECB导致此错误。调试方法首先写一个简单的测试用固定的密钥和IV加密一段已知明文如Hello, World!然后立即解密看是否成功。这可以隔离文件I/O问题。如果失败检查算法参数。如果成功再对比文件十六进制检查文件头和数据体部分是否完整。7.2 读取时字符串乱码或长度错误原因1编码不一致。BinaryWriter.Write(string)默认使用UTF-8编码写入带长度前缀的字符串。如果你用BinaryReader.ReadString()读取但写入时用的是自定义的字节数组就会出错。确保读写字符串的方式对称。我推荐对于固定长度字段统一使用Encoding.UTF8.GetBytes和GetString。原因2固定长度字符串未处理填充。写入时不足长度要补零读取时要找到第一个零字符截断。如果忘记处理字符串后面会带有一堆空字符\0。原因3字节序问题。虽然BinaryWriter在单一.NET环境内默认是小端序但如果你的DAT文件需要与其它平台如某个嵌入式设备可能是大端序交换就必须显式处理字节序。可以使用BitConverter.IsLittleEndian判断并在必要时用Array.Reverse()翻转数组。7.3 文件大小与预期不符原因加密填充。AES是块加密算法块大小128位16字节。如果明文长度不是16字节的整数倍就需要填充。PKCS7填充方式会至少填充1个字节最多16个字节。因此加密后的数据体大小会比原始数据体略大最多多16字节。这是正常的。计算验证原始数据体大小 N条记录 * (每条记录长度4字节长度前缀)。加密后数据体大小 ≈((原始数据体大小 15) / 16) * 16。文件总大小 28字节文件头 加密后数据体大小。7.4 性能瓶颈定位使用性能剖析工具如Visual Studio Diagnostic Tools, JetBrains dotTrace。通常瓶颈在于磁盘I/O使用BufferedStream并确保缓冲区大小合适。加密解密AES是计算密集型操作。对于超大文件如果允许可以考虑性能更快的加密模式如AES-CTR但.NET原生支持较弱或者使用硬件加速确保服务器支持AES-NI指令集。对象序列化/反序列化反射GetCustomAttribute在循环中调用有开销。可以在启动时缓存这些属性信息。// 缓存属性信息 private static readonly ListPropertyInfo _orderedProperties; static DeviceConfig() { _orderedProperties typeof(DeviceConfig) .GetProperties() .Where(p p.IsDefined(typeof(BinaryOrderAttribute), false)) .OrderBy(p p.GetCustomAttributeBinaryOrderAttribute().Order) .ToList(); } // 在ToBytes和FromBytes中使用缓存的 _orderedProperties处理DAT文件的加密读写与转换就像在二进制世界里搭建一座精密的桥梁。从明文的CSV到加密的二进制流每一个字节的安排都需深思熟虑。选择AES-CBC模式确保了安全性利用CryptoStream实现了加密与I/O的优雅耦合而自定义的文件头和数据项长度前缀则保证了数据的可解析性。性能优化、异常处理和日志记录是让这座桥梁从“能用”到“好用”的关键。最后多测试、多验证尤其是在边界条件下空文件、超大文件、错误密钥才能交付一个稳定可靠的数据转换模块。