概述
SpanWriter
是一个高性能的字节流写入器,提供零分配的二进制数据写入能力。它是一个 ref struct
,专门设计用于构建网络协议帧、序列化二进制数据或生成自定义格式的字节流。
核心特性
- 零分配写入:基于
Span<byte>
的直接内存写入,无 GC 压力
- 多数据类型支持:内置基础类型(整数、浮点、字符串)的写入方法
- 7位压缩整数:兼容 .NET 标准的压缩格式写入
- 结构体直接序列化:利用内存布局直接写入结构体
- 字符串灵活写入:支持定长、变长和长度前缀多种模式
- 字节序控制:支持大端和小端字节序
- 边界检查:自动进行缓冲区溢出检查
构造方式
// 从 Span<byte> 构造
Span<byte> buffer = stackalloc byte[1024];
var writer = new SpanWriter(buffer);
// 从 IPacket 构造
IPacket packet = new OwnerPacket(1024);
var writer = new SpanWriter(packet);
// 从字节数组构造
var buffer = new byte[1024];
var writer = new SpanWriter(buffer);
主要属性
属性 | 类型 | 说明 |
|
| 目标缓冲区 |
|
| 已写入字节数 |
|
| 总容量 |
|
| 剩余可写字节数 |
|
| 已写入的数据片段 |
|
| 已写入数据长度 |
|
| 是否小端字节序(默认 true) |
基础操作
位置控制
var writer = new SpanWriter(buffer);
// 前进指定字节数(标记已写入)
writer.Advance(4);
// 获取可写入的缓冲区
Span<byte> writeBuffer = writer.GetSpan();
Span<byte> writeBufferWithHint = writer.GetSpan(sizeHint: 100);
// 获取已写入的数据
ReadOnlySpan<byte> written = writer.WrittenSpan;
int writtenLength = writer.WrittenCount;
数据类型写入
基础数值类型
var writer = new SpanWriter(buffer);
// 写入基础类型
writer.Write((byte)0x01);
writer.WriteByte(0x02); // 等价于 Write((byte)0x02)
writer.Write((short)1000);
writer.Write((ushort)2000);
writer.Write(12345);
writer.Write(67890U);
writer.Write(123456789L);
writer.Write(987654321UL);
writer.Write(3.14f);
writer.Write(2.718281828);
// 所有Write方法都返回写入的字节数
int bytesWritten = writer.Write(42); // 返回 4
字节序控制
var writer = new SpanWriter(buffer);
// 使用大端字节序
writer.IsLittleEndian = false;
writer.Write(0x12345678); // 写入为 12 34 56 78
// 切换回小端字节序
writer.IsLittleEndian = true;
writer.Write(0x12345678); // 写入为 78 56 34 12
字符串写入
var writer = new SpanWriter(buffer);
// 写入模式:
// length = -1: 写入全部字符串内容,不含长度信息
// length = 0: 先写入7位压缩长度前缀,再写入字符串内容
// length > 0: 写入固定长度(不足填0,超长截断)
// 写入全部内容
writer.Write("Hello World", -1);
// 写入带长度前缀的字符串
writer.Write("Hello World", 0);
// 写入固定长度字符串(20字节,不足补0)
writer.Write("Hello", 20);
// 指定编码
writer.Write("你好世界", 0, Encoding.UTF8);
writer.Write("Hello", 10, Encoding.ASCII);
字节数组和 Span 写入
var writer = new SpanWriter(buffer);
// 写入字节数组
byte[] data = { 1, 2, 3, 4, 5 };
writer.Write(data);
// 写入 ReadOnlySpan<byte>
ReadOnlySpan<byte> span = stackalloc byte[] { 6, 7, 8 };
writer.Write(span);
// 写入 Span<byte>
Span<byte> spanData = stackalloc byte[] { 9, 10 };
writer.Write(spanData);
结构体写入
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct MyHeader
{
public int Magic;
public short Version;
public byte Flags;
}
var writer = new SpanWriter(buffer);
var header = new MyHeader
{
Magic = 0x12345678,
Version = 1,
Flags = 0xFF
};
int bytesWritten = writer.Write(header); // 返回结构体大小
高级功能
7位压缩整数
var writer = new SpanWriter(buffer);
// 写入7位压缩格式的32位整数(兼容 BinaryWriter)
int bytesWritten = writer.WriteEncodedInt(12345);
// 负数也支持(会占用5字节)
writer.WriteEncodedInt(-1);
批量操作示例
public byte[] BuildProtocolFrame(int messageId, string content)
{
Span<byte> buffer = stackalloc byte[1024];
var writer = new SpanWriter(buffer);
// 写入协议头
writer.Write((byte)0x01); // 版本
writer.WriteEncodedInt(messageId); // 消息ID(压缩格式)
writer.Write(content, 0); // 内容(带长度前缀)
// 返回实际数据
return writer.WrittenSpan.ToArray();
}
错误处理
所有写入操作在空间不足时会抛出异常:
Span<byte> smallBuffer = stackalloc byte[2];
var writer = new SpanWriter(smallBuffer);
try
{
writer.Write(12345); // 需要4字节,但缓冲区只有2字节
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"缓冲区空间不足: {ex.Message}");
}
使用场景
网络协议构建
public ReadOnlySpan<byte> BuildHttpResponse(int statusCode, string body)
{
Span<byte> buffer = stackalloc byte[4096];
var writer = new SpanWriter(buffer);
// HTTP状态行
writer.Write($"HTTP/1.1 {statusCode} OK\r\n", -1, Encoding.ASCII);
// 头部
writer.Write("Content-Type: text/plain\r\n", -1, Encoding.ASCII);
writer.Write($"Content-Length: {Encoding.UTF8.GetByteCount(body)}\r\n", -1, Encoding.ASCII);
writer.Write("\r\n", -1, Encoding.ASCII);
// 正文
writer.Write(body, -1, Encoding.UTF8);
return writer.WrittenSpan;
}
二进制数据序列化
public void SerializeBinaryData(Span<byte> output, MyDataStructure data)
{
var writer = new SpanWriter(output);
// 魔数和版本
writer.Write(0x12345678);
writer.Write((short)1);
// 数据项数量
writer.WriteEncodedInt(data.Items.Count);
// 序列化每个数据项
foreach (var item in data.Items)
{
writer.Write(item.Id);
writer.Write(item.Name, 0); // 带长度前缀
writer.Write(item.Timestamp.ToBinary());
}
}
性能关键场景
public unsafe void HighPerformanceWrite(Span<byte> buffer, int[] values)
{
var writer = new SpanWriter(buffer);
// 写入数组长度
writer.WriteEncodedInt(values.Length);
// 批量写入整数(避免循环开销)
foreach (int value in values)
{
writer.Write(value);
}
}
字符串写入详解
定长字符串处理
var writer = new SpanWriter(buffer);
// 固定20字节,不足补0
writer.Write("Hello", 20); // "Hello" + 15个0字节
// 超长截断
writer.Write("This is a very long string", 10); // 只写入前10字节
编码处理
var writer = new SpanWriter(buffer);
// UTF8编码(默认)
writer.Write("中文测试", 0);
// GBK编码
var gbk = Encoding.GetEncoding("GBK");
writer.Write("中文测试", 0, gbk);
// ASCII编码(非ASCII字符会被替换)
writer.Write("Hello World", 0, Encoding.ASCII);
性能说明
SpanWriter
是ref struct
,只能在栈上分配,无 GC 压力
- 所有数值写入都是直接内存访问,性能接近不安全代码
- 字符串写入会涉及编码转换,建议复用编码器实例
- 大端字节序写入会有轻微性能开销
- 7位压缩整数写入针对小数值优化
限制与注意事项
- ref struct 限制:不能存储在堆上,不能作为字段,不能在异步方法中跨await使用
- 生命周期:依赖底层缓冲区的生命周期,使用期间缓冲区不能被释放
- 边界检查:会自动进行溢出检查,但不会自动扩容
- 线程安全:非线程安全,不能跨线程使用
- 缓冲区大小:需要预先分配足够的缓冲区,写入超出容量会抛出异常
最佳实践
- 合理估算缓冲区大小:预留足够空间避免溢出异常
- 复用编码器:避免重复创建
Encoding
实例
- 使用 stackalloc:小缓冲区优先使用栈分配
- 批量写入:合并多个小写入操作提高性能
- 错误处理:妥善处理缓冲区溢出异常
相关类型
SpanReader
- 对应的读取器
SpanHelper
- Span相关的辅助方法
PooledByteBufferWriter
- 池化的动态缓冲区写入器