概述

SpanReader 是一个高性能的只读字节流解析器,用于零分配地从内存缓冲区或数据流中读取二进制数据。它是一个 ref struct,专为解析网络协议帧(如 Redis、MySQL 协议)或自定义二进制格式而设计。

核心特性

  • 零分配读取:基于 Span<byte>ReadOnlySpan<byte>,避免不必要的内存分配
  • 自动流扩展:支持从底层 Stream 动态读取更多数据
  • 多数据类型支持:内置基础类型(整数、浮点、字符串)的读取方法
  • 7位压缩整数:兼容 .NET 标准的压缩格式
  • 结构体直接反序列化:利用内存布局直接读取结构体
  • 字节序控制:支持大端和小端字节序
  • 数据包切片:支持零拷贝的数据包切片操作

构造方式

基础构造

// 从 ReadOnlySpan<byte> 构造
var data = new byte[] { 1, 2, 3, 4 };
var reader = new SpanReader(data.AsSpan());

// 从 Span<byte> 构造
Span<byte> buffer = stackalloc byte[100];
var reader = new SpanReader(buffer);

// 从 IPacket 构造
IPacket packet = GetPacket();
var reader = new SpanReader(packet);

流扩展构造

// 支持从数据流读取更多数据
var stream = new NetworkStream(socket);
var reader = new SpanReader(stream, initialPacket, bufferSize: 8192);
reader.MaxCapacity = 1024 * 1024; // 限制最大读取 1MB

主要属性

属性

类型

说明

Span

ReadOnlySpan<byte>

当前数据片段

Position

int

已读取字节数

Capacity

int

总容量

FreeCapacity

int

尚未读取的剩余字节数

IsLittleEndian

bool

是否小端字节序(默认 true)

MaxCapacity

int

最大容量限制(0 表示不限制)

基础操作

位置控制

var reader = new SpanReader(data);

// 前进指定字节数
reader.Advance(4);

// 获取剩余数据片段
var remaining = reader.GetSpan();
var remainingWithHint = reader.GetSpan(sizeHint: 10);

确保数据可用

// 确保至少有 10 字节可读(必要时从流中补齐)
reader.EnsureSpace(10);

数据类型读取

基础数值类型

var reader = new SpanReader(data);

// 读取基础类型
byte b = reader.ReadByte();
short s = reader.ReadInt16();
ushort us = reader.ReadUInt16();
int i = reader.ReadInt32();
uint ui = reader.ReadUInt32();
long l = reader.ReadInt64();
ulong ul = reader.ReadUInt64();
float f = reader.ReadSingle();
double d = reader.ReadDouble();

字节序控制

var reader = new SpanReader(data);

// 使用大端字节序
reader.IsLittleEndian = false;
int bigEndianValue = reader.ReadInt32();

// 切换回小端字节序
reader.IsLittleEndian = true;
int littleEndianValue = reader.ReadInt32();

字符串读取

var reader = new SpanReader(data);

// 读取方式:
// length = -1: 读取剩余全部字节
// length = 0:  先读取7位压缩长度前缀,再读取相应字节
// length > 0:  读取固定长度

// 读取剩余全部数据为字符串
string allText = reader.ReadString(-1);

// 读取带长度前缀的字符串(7位压缩格式)
string prefixedText = reader.ReadString(0);

// 读取固定长度字符串
string fixedText = reader.ReadString(10);

// 指定编码
string gbkText = reader.ReadString(10, Encoding.GetEncoding("GBK"));

字节数组读取

var reader = new SpanReader(data);

// 读取指定长度的字节片段
ReadOnlySpan<byte> bytes = reader.ReadBytes(5);

// 读取到目标缓冲区
Span<byte> buffer = stackalloc byte[10];
int bytesRead = reader.Read(buffer);

结构体读取

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct MyHeader
{
    public int Magic;
    public short Version;
    public byte Flags;
}

var reader = new SpanReader(data);
MyHeader header = reader.Read<MyHeader>();

高级功能

7位压缩整数

var reader = new SpanReader(data);

// 读取7位压缩格式的32位整数(兼容 BinaryReader)
int compressedValue = reader.ReadEncodedInt();

数据包切片

// 从IPacket构造的reader支持零拷贝切片
var reader = new SpanReader(packet);

// 切片10字节的数据包(不会复制数据)
IPacket slice = reader.ReadPacket(10);

错误处理

所有读取操作在数据不足时会抛出异常:

var reader = new SpanReader(smallData);

try
{
    int value = reader.ReadInt32(); // 如果数据不足4字节会抛出异常
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"数据不足: {ex.Message}");
}

使用场景

网络协议解析

public ParsedMessage ParseProtocolFrame(ReadOnlySpan<byte> frameData)
{
    var reader = new SpanReader(frameData);
    
    var header = reader.Read<ProtocolHeader>();
    var bodyLength = reader.ReadEncodedInt();
    var body = reader.ReadBytes(bodyLength);
    
    return new ParsedMessage(header, body);
}

流式数据解析

public async Task<List<Record>> ParseStreamAsync(Stream stream)
{
    var records = new List<Record>();
    var reader = new SpanReader(stream, bufferSize: 4096);
    reader.MaxCapacity = 1024 * 1024; // 限制1MB
    
    while (reader.FreeCapacity > 0)
    {
        try
        {
            var recordType = reader.ReadByte();
            var recordLength = reader.ReadEncodedInt();
            var recordData = reader.ReadBytes(recordLength);
            
            records.Add(new Record(recordType, recordData));
        }
        catch (InvalidOperationException)
        {
            break; // 数据读取完毕
        }
    }
    
    return records;
}

性能说明

  • SpanReaderref struct,只能在栈上分配,无 GC 压力
  • 所有数值读取都是直接内存访问,性能接近不安全代码
  • 支持从流扩展时,会使用 OwnerPacket 管理缓冲区生命周期
  • 大端字节序读取会有轻微性能开销

限制与注意事项

  1. ref struct 限制:不能存储在堆上,不能作为字段,不能在异步方法中跨await使用
  1. 生命周期:依赖底层数据的生命周期,使用期间底层数据不能被释放
  1. 线程安全:非线程安全,不能跨线程使用
  1. 流扩展模式:与数据包切片模式不兼容,需要选择合适的构造方式

相关类型

  • SpanWriter - 对应的写入器
  • SpanHelper - Span相关的辅助方法
  • PooledByteBufferWriter - 池化的动态缓冲区写入器