加强版二进制读写器
Ray Koopa 著
Conmajia 译
2019 年 1 月 17 日已获作者本人授权.
简介
本文讨论如何扩展 .NET 原生的 BinaryReader
和 BinaryWriter
类以支持更多新的常用的特性. 这些 API 可以通过 NuGet > Syroot.IO.BinaryData 安装
PM> Install-Package Syroot.IO.BinaryData -Version 4.0.4
> dotnet add package Syroot.IO.BinaryData --version 4.0.4
> paket add Syroot.IO.BinaryData --version 4.0.4
GitHub 上的百科主要关注实现方面
背景
每次我要用到二进制数据加载BinaryReader
和 BinaryWriter
类. 普通数据还好
- 处理以不同于本机字节顺序存储的数据
- 处理非 .NET
比如以 0 结尾的字符串, - 读写重复的数据类型而不用一遍又一遍地循环
- 临时用不同编码的字符串读写数据流
- 文件内高级定位
例如临时定位新位置,
一开始我只是写点扩展方法BinaryReader
BinaryWriter
的外挂. 但是使用中我发现BinaryDataReader
和 BinaryDataWriter
. 接下来看看我是如何实现上面列出的各个特性的吧.
实现和用法
字节顺序
.NET 本身没有规定数据的字节顺序System.BitConverter.IsLittleEndian
字段可用
ByteOrder systemByteOrder = BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian;
这里我引入了一个枚举类型 ByteOrder
区分大小端字节顺序
public enum ByteOrder : ushort
{
BigEndian = 0xFEFF,
LittleEndian = 0xFFFE
}
ByteOrder
属性则用来指定读写类的字节顺序
public ByteOrder ByteOrder
{
get
{
return _byteOrder;
}
set
{
_byteOrder = value;
_needsReversion = _byteOrder != ByteOrder.GetSystemByteOrder();
}
}
我分别重写了 BinaryDataReader
和 BinaryDataWriter
的所有 Read
Write
. 重写的方法由 _needsReversion
决定要不要改变字节顺序
public override Int32 ReadInt32()
{
if (_needsReversion)
{
byte[] bytes = base.ReadBytes(sizeof(int));
Array.Reverse(bytes);
return BitConverter.ToInt32(bytes, 0);
}
else
{
return base.ReadInt32();
}
}
用 BitConverter.ToXXX()
这系列方法能轻松实现字节数组和多字节数据的互相转换. 不过 Decimal
类型有点怪
用法
BinaryDataReader
BinaryDataWriter
默认用的本机字节顺序. 要改变字节顺序ByteOrder
属性. 任何时候都可以修改这个属性
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
int intInSystemOrder = reader.ReadInt32();
reader.ByteOrder = ByteOrder.BigEndian;
int intInBigEndian = reader.ReadInt32();
reader.ByteOrder = ByteOrder.LittleEndian;
int intInLittleEndian = reader.ReadInt32();
}
重复的数据类型
处理 3D 格式文件的时候ReadMatrix
ReadSingles(T[])
这种for
之类的循环它在内部处理好
public Int32[] ReadInt32s(int count)
{
return ReadMultiple(count, ReadInt32);
}
private T[] ReadMultiple<T>(int count, Func<T> readFunc)
{
T[] values = new T[count];
for (int i = 0; i < values.Length; i++)
{
values[i] = readFunc.Invoke();
}
return values;
}
用法
调用对应数据类型的 Read
Write
则是把数组写到数据流.
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
int[] fortyFatInts = reader.ReadInt32s(40);
}
不同的字符串格式
字符串可以保存为不同的二进制格式. 默认的读写器类只支持带无符号整数前缀的字符串. 工作中我处理的多数字符串都是 0 结尾\0
0
ReadString
WriteString
BinaryStringFormat
ByteLengthPrefix
无符号字节型前缀: ( uint8
.) WordLengthPrefix
: 有符号双字节型前缀( Int16
.) DwordLengthPrefix
有符号四字节型前缀: ( Int32
.) ZeroTerminated
没有前缀: , 0
数值( \0
作为结束符.) NoPrefixOrTermination
既没有前缀: 也没有结束符, 必须知道字符串长度才能操作.,
用法
使用对应的重载方法就可以读取相应的格式
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
using (BinaryDataWriter writer = new BinaryDataWriter(stream))
{
string magicBytes = reader.ReadString(4); // 没有前缀和结束符,需要知道长度
if (magicBytes != "RIFF")
{
throw new InvalidOperationException("Not a RIFF file.");
}
string zeroTerminated = reader.ReadString(BinaryStringFormat.ZeroTerminated);
string netString = reader.ReadString();
string anotherNetString = reader.ReadString(BinaryStringFormat.DwordLengthPrefix);
writer.Write("RAW", BinaryStringFormat.NoPrefixOrTermination);
}
NoPrefixOrTermination
需要知道读取的字符数量BinaryStringFormat
. 它有自己的重载方法ReadString
重载.
临时字符串编码
可以在 .NET 默认的读写类构造函数里指定字符串的编码ReadString
Write(string)
的时候把对应编码传入就好. 标准 .NET 读写类没法在运行时改变字符串编码Encoding
用法
调用 ReadString
Write(string)
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream, Encoding.ASCII))
using (BinaryDataWriter writer = new BinaryDataWriter(stream, Encoding.ASCII))
{
string unicodeString = reader.ReadString(BinaryStringFormat.DwordLengthPrefix, Encoding.UTF8);
string asciiString = reader.ReadString();
Console.WriteLine(reader.Encoding.CodePage);
}
不同的日期/ 时间格式
不光字符串有不同的二进制格式DateTime
日期BinaryDateTimeFormat
枚举指定时间格式
CTime
C 语言标准库的: time_t
格式.NetTicks
.NET 默认的: DateTime
格式.
用法
用脚趾头也想得到BinaryStringFormat
枚举来指定相应格式的类似方法处理日期ReadDateTime()
WriteDateTime()
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
DateTime cTime = reader.ReadDateTime(BinaryDateTimeFormat.CTime);
}
高级流定位 ( 临时定位)
临时查找另一个位置using
IDisposable
的方式实现临时定位. 调用 TemporarySeek(long)
SeekTask
类的实例
public class SeekTask : IDisposable
{
public SeekTask(Stream stream, long offset, SeekOrigin origin)
{
Stream = stream;
PreviousPosition = stream.Position;
Stream.Seek(offset, origin);
}
public Stream Stream { get; private set; }
/// <summary>
/// 获取任务执行完后需要返回的绝对位置.
/// </summary>
public long PreviousPosition { get; private set; }
/// <summary>
/// 返回前一个位置.
/// </summary>
public void Dispose()
{
Stream.Seek(PreviousPosition, SeekOrigin.Begin);
}
}
用法
TemporarySeek
用起来比看起来容易多了. 调用的时候用个 using
块
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
int offset = reader.ReadInt32();
using (reader.TemporarySeek(offset, SeekOrigin.Begin))
{
byte[] dataAtOffset = reader.ReadBytes(128);
}
int dataAfterOffsetValue = reader.ReadInt32();
}
例子代码里int
using
块创建一个临时定位实例
字节块对齐
有些文件格式为了配合硬件读取速度BinaryDataReader
BinaryDataWriter
类里了
/// <summary>
/// 对齐到下一个多字节数据块位置.
/// </summary>
/// <param name="alignment">数据块大小</param>
public void Align(int alignment)
{
Seek((-Position % alignment + alignment) % alignment);
}
用法
假设要处理的文件是按 0x200
Align
的用法如下
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataReader reader = new BinaryDataReader(stream))
{
string header = reader.ReadString(4);
reader.Align(0x200); // 定位到下一个数据块位置
}
数据流属性的快捷方式
有些常用的属性Length
Position
Seek
之类的BaseStream
里访问. 我把这些东西都提炼成属性
/// <summary>
/// 获取和设置在数据流中的位置.
/// property.
/// </summary>
public long Position
{
get { return BaseStream.Position; }
set { BaseStream.Position = value; }
}
用法
很简单
Random random = new Random();
using (MemoryStream stream = new MemoryStream(...))
using (BinaryDataWriter writer = new BinaryDataWriter(stream))
{
while (writer.Position < 0x4000) // 直接访问 Position
{
writer.Write(random.Next());
}
}
要关注的
优化读写类的性能是最重要的unsafe
的代码
别忘了检查 NuGet 上面的更新
历史
- 2016-09-18
首版发布.: - 2019-01-17
更新了 NuGet 包链接.: - 2019-01-17
中文版发布.:
许可
本文以及任何相关的源代码和文件都是根据 GNU
关于作者
Ray Koopa.
if(jQuery('#no-reward').text() == 'true') jQuery('.bottom-reward').addClass('hidden');
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?