解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
14.10 KiB
X
# 高级二进制序列化 面向高级工程师和 AI 的零分配协议编解码架构指南。 核心目标:协议消息在 `IPacket` 数据包、`NetworkStream` 网络流、`FileStream` 文件流以及加密/压缩包装流中编解码时,全程零内存分配。 > 不计入的 GC:消息实例本身的 `new`、`String` 解码产生的字符串对象、内存池首次内部扩容。 --- ## 体系结构 ``` ┌─────────────────────────────────────────────────────┐ │ SpanSerializer (static) │ │ 快捷方法: ToPacket / FromPacket / Serialize / ... │ ├─────────────────────────────────────────────────────┤ │ ISpanSerializable │ │ 消息契约: Write(ref SpanWriter) / Read(ref SpanReader)│ ├──────────────────────┬──────────────────────────────┤ │ SpanWriter │ SpanReader │ │ ref struct 写入器 │ ref struct 读取器 │ │ Span / Span+Stream │ Span / IPacket / Stream │ └──────────────────────┴──────────────────────────────┘ ``` `ISpanSerializable` 定义消息体字段布局,由 `SpanWriter`/`SpanReader` 执行底层读写。`SpanSerializer` 在此基础上封装池化缓冲区管理和头部预留。 ### SpanReader 构造速查 | 构造函数 | 数据来源 | 零拷贝 | 自动补齐 | 典型场景 | |---------|---------|:------:|:------:|---------| | `SpanReader(ReadOnlySpan<Byte>)` | 栈/堆内存 | ✓ | ✗ | 已知完整数据的解析 | | `SpanReader(IPacket)` | 数据包 | ✓(单段) | ✗ | NewLife 网络库收包后解码 | | `SpanReader(Stream, IPacket?, bufferSize)` | 任意流 | ✗ | ✓ | NetworkStream / FileStream | 行为差异: - **单段 `IPacket`**:直接在底层 Span 上读取,`ReadPacket(len)` 返回零拷贝切片 - **链式 `IPacket`**(`Next != null`):构造时即转为流模式(`data.GetStream(false)`),后续与流构造行为一致 - **流构造**:内部维护 `OwnerPacket` 缓冲区,剩余字节不足时从流拉取新数据并重组;`MaxCapacity` 控制单次解析的总字节上限 ### SpanWriter 构造速查 | 构造函数 | 输出目标 | 溢出处理 | 典型场景 | |---------|---------|---------|---------| | `SpanWriter(Span<Byte>)` | 固定缓冲区 | 抛异常 | 已知长度上限 | | `SpanWriter(IPacket)` | 数据包 Span | 抛异常 | 写入现有包头 | | `SpanWriter(Span<Byte>, Stream)` | 缓冲区 + 后备流 | 自动 Flush | NetworkStream / FileStream | 行为差异: - **纯 Span 模式**:空间不足直接抛 `InvalidOperationException` - **流模式**:空间不足时先 Flush 已写入数据到流再重置 `_index`;`Write(ReadOnlySpan<Byte>)` 支持超过缓冲区的大块数据分块刷流 - **流模式约束**:单次原子写入(如 `Write(Int32)`)不能超过缓冲区容量 - `TotalWritten` 属性反映含已 Flush 部分的总长度;`Dispose()` 自动调用 `Flush()` --- ## ISpanSerializable 消息体 协议消息实现 `ISpanSerializable`,手写字段布局,零反射: ```csharp public sealed class DeviceReport : ISpanSerializable { public UInt16 DeviceId { get; set; } public Int64 Timestamp { get; set; } public Double Temperature { get; set; } public String? Location { get; set; } public void Write(ref SpanWriter writer) { writer.Write(DeviceId); writer.Write(Timestamp); writer.Write(Temperature); writer.Write(Location, 0); // 7 位压缩长度前缀 + UTF-8 } public void Read(ref SpanReader reader) { DeviceId = reader.ReadUInt16(); Timestamp = reader.ReadInt64(); Temperature = reader.ReadDouble(); Location = reader.ReadString(); // length=0 → 7 位压缩长度 } } ``` 规则: - 字段顺序即协议格式,`Read`/`Write` 必须严格对称,发布后不可调整 - 字符串:`Write(value, 0)` + `ReadString(0)` = 7 位压缩长度前缀 - 大块 payload:先写长度再 `Write(ReadOnlySpan<Byte>)`,不要先复制到 `Byte[]` - 字节序:通过 `writer.IsLittleEndian` / `reader.IsLittleEndian` 控制,默认小端 - 与 `Binary` 类线格式完全兼容(`IsLittleEndian` 匹配时),可双向互读 --- ## IPacket 数据包模式 适用 NewLife 网络库。`SessionBase.ProcessReceive` 收到的 `IPacket` 是本次缓冲区数据,经管道(`LengthFieldCodec` / `StandardCodec`)成帧后为完整消息体。 ### 编码 ```csharp // 消息 → 数据包(池化缓冲区,小数据零拷贝切片返回) using var pk = report.ToPacket(); session.Send(pk); ``` `ToPacket(bufferSize, reserve)` 内部机制: 1. 从 `ArrayPool` 租借 `bufferSize` 缓冲区,从 `Pool.MemoryStream` 租借后备流 2. 缓冲区前方跳过 `reserve`(默认 32)字节,消息体从该偏移后开始写入 3. **小数据路径**:消息全部落入缓冲区 → `pk.Slice(reserve, count)` 零拷贝切片返回 `OwnerPacket`,后备流归还池 4. **大数据路径**:缓冲区满时自动 Flush 到后备流 → 从流包装新 `OwnerPacket` 返回 #### 带帧头发送(利用预留区) 协议格式 `[4 字节长度][消息体]`: ```csharp // ToFrame 内部完成 ToPacket(reserve) + OwnerPacket 扩展,一步到位 using var frame = report.ToFrame(4); // 帧头区域 = GetSpan()[..4],正文长度 = Length - 4 var hw = new SpanWriter(frame.GetSpan()[..4]); hw.Write(frame.Length - 4); session.Send(frame); ``` `ToFrame(headerSize)` 等价于 `ToPacket(reserve: headerSize)` + `new OwnerPacket(owner, headerSize)`。`OwnerPacket(owner, expandSize)` 把 `Offset` 前移 `expandSize` 字节并转移所有权 —— 这是预留区设计的核心价值。 ### 解码 ```csharp // 管道已成帧 → 直接读 var report = new DeviceReport(); report.FromPacket(e.Packet); // 原始协议包 → 拆帧后读 var reader = new SpanReader(e.Packet); var bodyLen = reader.ReadInt32(); using var body = reader.ReadPacket(bodyLen); // 单段包零拷贝切片 var report2 = new DeviceReport(); report2.FromPacket(body); ``` ### 零拷贝分析 | 操作 | 单段包 | 链式包 | |------|:------:|:------:| | `ReadPacket(len)` | 底层切片,零拷贝 | 跨段切片,零拷贝 | | `ReadInt32()` 等定长读取 | 直接 Span 访问 | 跨段时退化为流补齐(分配内部缓冲区) | | `FromPacket(pk)` 扩展方法 | 单段直接取 Span | 构造时转流 | 最佳实践:成帧层保证完整消息落在一个连续段上,或先 `ReadPacket` 取正文子包再交消息层。 ### 携带负载的消息(IPacket Payload) 协议消息内嵌可变长度负载时,将 `Payload` 设计为 `IPacket` 类型,解码时 `Slice` 切片零拷贝获取,同时完成所有权转移: ```csharp public sealed class DataMessage : ISpanSerializable, IDisposable { public Byte Command { get; set; } public UInt16 Sequence { get; set; } /// <summary>负载数据,持有切片的缓冲区所有权</summary> public IPacket? Payload { get; set; } public void Write(ref SpanWriter writer) { writer.Write(Command); writer.Write(Sequence); var body = Payload; var len = body?.Total ?? 0; writer.Write(len); if (body != null) writer.Write(body.GetSpan()); } public void Read(ref SpanReader reader) { Command = reader.ReadByte(); Sequence = reader.ReadUInt16(); var len = reader.ReadInt32(); // ReadPacket 对 OwnerPacket 底层包执行 Slice,所有权转移给 Payload // 原始包后续 Dispose 不会重复归还该段缓冲区 if (len > 0) Payload = reader.ReadPacket(len); } /// <summary>释放负载数据,归还池化缓冲区</summary> public void Dispose() => Payload.TryDispose(); } ``` 所有权流转: ``` 网络收包 pk (OwnerPacket, 持有缓冲区) ↓ Slice(offset, len) — 所有权转移 msg.Payload (OwnerPacket 切片, 持有缓冲区) ↓ msg.Dispose() / Payload.TryDispose() 缓冲区归还 ArrayPool ``` 关键规则: - `Slice` 默认 `transferOwner: true`,切片接管缓冲区所有权,原始包失去管理权 - 消息生命周期结束时通过 `Payload.TryDispose()` 归还池化缓冲区 - 若需跨线程/异步延迟使用 Payload,必须先深拷贝(`Payload.ReadBytes()` 或 `ToArray()`) - 编码时用 `writer.Write(body.GetSpan())` 直接写入,不需要先转 `Byte[]` 参考实现:`DefaultMessage.Read` 中 `Payload = pk.Slice(size, len, true)` 即此模式。 --- ## Stream 流模式 适用 `TcpClient.GetStream()`、`FileStream`、`SslStream`、`GZipStream`、`CryptoStream` 等任何 `Stream`。不使用 NewLife 网络库时的标准路径。 ### 编码:消息 → 流 ```csharp // stackalloc 缓冲区,适合小消息 Span<Byte> buf = stackalloc Byte[256]; using var writer = new SpanWriter(buf, stream); report.Write(ref writer); // Dispose 自动 Flush 剩余 ``` ```csharp // 池化缓冲区,适合较大消息 var buffer = Pool.Shared.Rent(4096); try { using var writer = new SpanWriter(buffer, stream); report.Write(ref writer); } finally { Pool.Shared.Return(buffer); } ``` #### 带帧头写流(利用预留区) 与 IPacket 模式的帧头写法一致,使用 `ToFrame` 一步完成序列化和头部扩展: ```csharp using var frame = report.ToFrame(4); // 填充帧头 var hw = new SpanWriter(frame.GetSpan()[..4]); hw.Write(frame.Length - 4); // 整帧一次写入流 frame.CopyTo(stream); ``` #### 带帧头写流(仅限可寻址流,如 FileStream) ```csharp // 记录帧头位置,先写占位 var headerPos = stream.Position; Span<Byte> placeholder = stackalloc Byte[4]; stream.Write(placeholder); // 写消息体 var buffer = Pool.Shared.Rent(4096); try { using var writer = new SpanWriter(buffer, stream); report.Write(ref writer); } finally { Pool.Shared.Return(buffer); } // 回填帧头 var bodyLen = (Int32)(stream.Position - headerPos - 4); stream.Position = headerPos; var hw = new SpanWriter(placeholder); hw.Write(bodyLen); stream.Write(placeholder); stream.Seek(0, SeekOrigin.End); ``` > `NetworkStream` 不可寻址,禁止使用回填方案。 ### 解码:流 → 消息 ```csharp var reader = new SpanReader(stream, bufferSize: 1024) { MaxCapacity = 64 * 1024 // 单帧上限,必须设置 }; var report = new DeviceReport(); report.Read(ref reader); ``` #### 带帧头解码 ```csharp var reader = new SpanReader(stream, bufferSize: 1024) { MaxCapacity = 64 * 1024 }; var bodyLen = reader.ReadInt32(); var start = reader.Position; var report = new DeviceReport(); report.Read(ref reader); if (reader.Position - start != bodyLen) throw new InvalidDataException("帧长度不匹配"); ``` #### 混合模式:初始包 + 流 已收到部分头部数据,后续正文需从流继续拉取: ```csharp var reader = new SpanReader(stream, headerPacket, bufferSize: 8192); report.Read(ref reader); ``` ### 各类 Stream 注意事项 | 流类型 | 特征 | 注意 | |--------|------|------| | `NetworkStream` | 不可寻址、不可回退 | 禁止回填长度 | | `SslStream` | 同 NetworkStream | 禁止回填长度 | | `FileStream` | 可寻址 | 支持 Flush 后回填帧头 | | `GZipStream` / `DeflateStream` | 不可寻址、压缩后长度不可预知 | 适合纯流顺序写 | | `CryptoStream` | 不可寻址 | 同压缩流 | --- ## SpanSerializer 快捷方法 | 方法 | 作用 | 零反射 | 备注 | |------|------|:------:|------| | `msg.ToPacket(bufferSize, reserve)` | `ISpanSerializable` → `IOwnerPacket` | ✓ | 池化双路径 | | `msg.ToFrame(headerSize, bufferSize)` | 序列化 + 扩展帧头区域 | ✓ | 帧头需调用方填充 | | `msg.FromPacket(packet)` | `IPacket` → 填充已有实例 | ✓ | 返回消费字节数 | | `msg.FromSpan(data)` | `ReadOnlySpan<Byte>` → 填充已有实例 | ✓ | 返回消费字节数 | | `SpanSerializer.Serialize(obj)` | 任意对象 → `IOwnerPacket` | ✗(编译缓存) | `HeaderReserve` 预留 | | `SpanSerializer.Serialize(obj, span)` | 任意对象 → 写入指定 Span | ✗(编译缓存) | 返回写入字节数 | | `SpanSerializer.Deserialize<T>(data)` | `ReadOnlySpan<Byte>` → 新实例 | ✗(编译缓存) | — | | `SpanSerializer.Deserialize<T>(packet)` | `IPacket` → 新实例 | ✗(编译缓存) | 单段直接取 Span | 普通对象路径首次反射后编译为 `Expression` 委托缓存,后续调用无反射开销。不支持 `List`、`Dictionary` 等集合的自动序列化 —— 协议含集合时须在 `ISpanSerializable` 中手写。 --- ## AI 架构准则 用这套体系重构目标项目时遵循以下原则: 1. 协议消息统一实现 `ISpanSerializable`,禁止业务层手动拼接 `Byte[]` 或 `new MemoryStream` 2. 帧头(长度、魔数、序列号、CRC)与消息体分离:`ISpanSerializable` 只管正文,帧编解码独立 3. NewLife.Net 管道已完成成帧时,业务层只处理完整消息体的 `IPacket` 4. 非可寻址流(`NetworkStream`、`SslStream`、压缩/加密流)的协议设计顺序写出,不依赖回填 5. 大 payload 用长度前缀 + `Write(ReadOnlySpan<Byte>)` / `ReadBytes(len)` 直写直读 6. 链式 `IPacket` 在成帧层切出正文子包再交消息层,避免跨段读取基础类型 7. 每个流入口设置 `MaxCapacity` 上限,防御恶意或错误数据 8. 极端吞吐场景延迟 `String` 物化,保留原始字节段操作 --- ## 约束 - `SpanReader`/`SpanWriter` 是 `ref struct`:不能跨 `await`、不能存为字段、不能放入闭包 - `ISpanSerializable.Read`/`Write` 必须严格对称,字段顺序和类型不一致会导致数据错位 - `SpanSerializer` 自动序列化不支持集合/字典类型 - `String` 解码必然产生堆对象(GC),无法避免 - 流模式零业务分配 ≠ 零拷贝;真正零拷贝需要单段 `IPacket` 切片路径