核心是用只读零分配切片视图替代新建对象,优先 Span 或 ArraySegment 避免字节数组拷贝,解析器接收 ReadOnlySpan 返回值类型结果,字段提取用无分配 API,迭代器仅推进游标不缓存数据,并配合缓冲区池与严格生命周期管理。
核心思路是:用只读、零分配、生命周期可控的切片视图替代每次解析都新建对象的惯用做法。关键不在“做迭代器”,而在“不创建对象”。
用 Span 或 ArraySegment 替代 byte[] 子数组拷贝
网络包解析最常见浪费发生在从原始字节流中提取字段时——比如取前 4 字节作为长度、中间 16 字节作为 ID。传统写法常调用 Array.Copy 或 new byte[n],导致高频 GC 压力。
- C# 中优先用
Span<byte></byte>:它可直接指向堆上缓冲区某一段,span.Slice(offset, length)不分配内存,且支持栈上暂存(如栈分配的stackalloc byte[256]) - 兼容旧框架时用
ArraySegment<byte></byte>:它仅含Array、Offset、Count三个字段,结构体无 GC 压力;但注意它不防越界,也不阻止原数组被回收或修改 - 避免隐式转换:不要把
Span转成byte[]或Memory<byte></byte>再传参,那会触发装箱或堆分配
设计只读、无状态的解析器类型
解析逻辑本身不该持有数据,而应接收切片并返回结构化结果(值类型优先)。
- 定义轻量解析方法,参数为
ReadOnlySpan<byte></byte>,返回PacketHeader这类struct,而非class PacketHeader - 字段提取用
BinaryPrimitives.ReadInt32BigEndian()等无分配 API,避免BitConverter(它内部可能新建数组) - 若需字符串字段(如协议名),用
Encoding.UTF8.GetString(span)——.NET 5+ 已优化为只读视图,不拷贝;旧版本可用Encoding.UTF8.GetChars()配合栈分配字符数组
迭代器本身不缓存数据,只推进读取位置
所谓“基于数组切片的迭代器”,本质是一个带状态的游标,每次返回下一个逻辑包的切片视图,而不是封装一堆对象。
- 实现
IEnumerator<readonlyspan>></readonlyspan>时,Current属性直接返回当前切片(buffer.Slice(start, length)),不 new、不 copy - 内部维护
int _readPos,每次MoveNext()仅解析包头获取长度,然后更新偏移量;整个过程无堆分配 - 配合
yield return时要小心:C# 的迭代器块会生成状态机类,若只需单次线性解析,更推荐手动实现struct迭代器(无 GC)
配套生命周期管理策略
零拷贝的前提是底层字节数组在解析全程有效且不被覆盖。
- 使用对象池(
ArrayPool<byte>.Shared</byte>)复用缓冲区,解析完立即Return(),避免频繁申请释放 - 若用
SocketAsyncEventArgs接收数据,其Buffer是复用的,解析器必须在回调内完成,不可异步延迟访问 - 禁止跨线程共享同一
Span或ArraySegment,尤其在异步上下文中;必要时转为Memory<byte></byte>并确保持有者明确负责生命周期










