解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
|
# 会话关闭与在途帧交付
> 适用范围:`NewLife.Net`(`SessionBase` 关闭流程、`TcpSession` 消息泵)、`NewLife.Messaging`(帧泵)、`NewLife.Data`(管道读侧取消)。
> 定位:**取消只终止“等待”,不丢弃已到达的数据**——关闭流程必须先把“已到达并写入管道”的帧交付给上层,再推进关闭。
> 配套专题:《消息协议栈》《网络库同步异步分工》《网络库架构》。
## 1. 现象
对端“发完即关”(发出最后一帧后立即关闭连接,不等对端关闭)时,服务端可能整帧丢弃这条最后消息。
最典型的受害场景是 MQTT:第三方客户端发出 DISCONNECT 后立即断开,服务端因没看到 DISCONNECT,把正常断开判定为**异常断开**并误发遗嘱消息。
裸 TCP 复现(本地回环,客户端发完 DISCONNECT 立即关闭):
| 版本 | 误发遗嘱 |
|---|---|
| 修复前(12.0.2026.1009-beta0916) | 199/200、200/200(多次复现;延迟 300ms 关闭的对照组恒为 0) |
| 修复后 | 0/200(连续 7 轮共 1400 轮全部为 0;含 30 秒线程池压力轮次) |
对照组恒为 0 说明服务端**能收到也能正确识别** DISCONNECT,问题只出在关闭时序上。
## 2. 机制
协议模式(`SessionBase.Protocol` 非空)下,入站数据通路是非对称的两段:
| 环节 | 执行者 | 时序 |
|---|---|---|
| 接收环读到字节 → `AppendToPipe` 写入数据管道 | 接收完成回调线程 | 同步,先发生 |
| 帧定界与交付(`MessagePump` → `OnMessageAsync` → 事件链/处理器) | 消息泵任务 | 异步,续体排队等线程池 |
FIN 的 0 字节完成读在接收环上**同步**触发 `Close` → `CloseAsync` → `StopReceiveFlow` 停泵(`cts.Cancel()`)→ 关闭处理器链与会话 → 触发上层断线判定。
关闭在同一线程上同步走完,而此刻泵的交付续体可能还排在线程池队列里,于是出现两处丢帧:
1. **交付晚于判定**:泵的续体在会话已被判定为“异常断开”之后才执行——上层已按“没收到正常断开报文”做完善后(如发布遗嘱),迟到的交付无法撤回该判定。
2. **取消吞掉已缓冲帧**:泵循环原以 `while (!cancellationToken.IsCancellationRequested)` 为退出条件,取消后直接在循环头退出,**管道内已到达的完整帧再也不读**;数据虽在管道里,也不会再被交付。
## 3. 修复
三处最小改动,统一为“取消只终止等待,不丢已到达数据”:
| 位置 | 改动 |
|---|---|
| `SessionBase.CloseAsync` | 关闭流程开头改为 `await StopReceiveFlowAsync()`;基类默认实现转发同步 `StopReceiveFlow()`,既有子类重写不受影响 |
| `TcpSession.Pump` | 新增 `PumpDrainTimeout`(默认 1000ms):停泵后等待泵退出再推进关闭,泵退出前会把已读出的帧交付完;泵内关闭(处理器内主动关闭、泵内报错)以异步流标记识别,不自等;泵循环不再按取消令牌提前退出,改为“无数据可读时的取消异常”才退出 |
| `PipeReader.ReadAsync` | 取消已触发且无数据可读时直接以取消结束,不再登记挂起回调(关闭流程对取消源是 Cancel 后立即 Dispose,无需依赖“释放后仍可登记”的运行时行为) |
两处易踩的坑(修复过程中实测踩到,记录备查):
- **异步流标记不能在同步前缀赋值**:`AsyncLocal` 若在方法同步前缀置位,会随执行上下文泄漏到启动会话的调用方,之后同一线程上的关闭会被误判为“泵内关闭”而跳过等待,丢帧照旧。故泵内标记在 `await Task.Yield()` 之后再置位。
- **关闭期间的读取失效不报错**:关闭流程释放入站管道后,泵的收尾读取会命中已结束的读取器;此时取消已在下达,按“随关闭一并收尾”静默退出,否则会刷出无意义的 `MessagePumpError` 并触发多余的二次关闭。
## 4. 验证
| 项 | 结果 |
|---|---|
| 裸 TCP 第三方客户端发完即关(200 轮 × 7,含 30s 线程池压力) | 0 次误发遗嘱 |
| 本地回环对照(延迟 300ms 关闭) | 0 次(与修复前一致,无损) |
| Core 全量单元测试(`Tools/Scripts/run-tests.ps1 -Only "Core-*"`,5 个宿主) | 全部通过(3305 用例/宿主,0 失败) |
| 下游 MQTT 全量测试(本地修复版 Core) | 720 通过 / 0 失败 / 2 跳过 |
回归用例:`XUnitTest.Core/Net/MessageSessionTests.PeerSendThenClose_FrameDeliveredBeforeClose`
用处理器卡住首帧制造“次帧已在管道、泵仍在交付首帧”的窗口,断言次帧交付早于底层会话销毁事件;未修复代码上该用例超时失败(负向验证已做)。
配套用例:`GarbageData_ExceedsMaxCache_ClosesConnection`(关闭进行中的读取失效不误报)。
## 5. 边界
- 等待有上限(`PumpDrainTimeout`,默认 1000ms):处理器长时间不返回时不再等待,按剩余流程关闭;置 0 可退回“不等待”的旧行为。
- 并行派发模式(`SessionBase.MaxConcurrency > 1`)下消息本就无序交付,排空只覆盖泵循环自身,不覆盖已并发派发出去的消息。
- 本契约只覆盖**读侧**;写侧(发送队列残余)仍按“不排空”处理,理由见《网络库同步异步分工》。
## 6. 相关文档
- 《消息协议栈》:帧泵与交付契约
- 《网络库同步异步分工》:读写路径的同步/异步划分与关闭纪律
- 《数据管道Pipe》:读侧取消与挂起登记语义
|