Skip to content

🐛 修复 MC 协议残帧误判导致 TCP 流错位与半开连接无法自愈的缺陷 - #3

Merged
keeleycenc merged 1 commit into
mainfrom
fix/mc-half-open-connection
Jul 15, 2026
Merged

keeleycenc merged 1 commit into
mainfrom
fix/mc-half-open-connection

Conversation

@keeleycenc

Copy link
Copy Markdown
Collaborator

Pull Request 描述

提交前检查

  • 本地合并了最新的代码,解决了合并冲突
  • 已经检查了代码,并在必要的位置添加了相应的注释
  • 遵循了此项目的代码风格及规范
  • 代码构建干净,没有任何错误和警告
  • 已经添加单元测试(非必须)
  • 更新了相应的文档(非必须)

变更类型

  • Bug修复
  • 紧急修复

相关 Issue

  • 解决 Issue 1:
  • 解决 Issue 2:

目的

问题描述:

上层压力采集服务轮询时偶发报错,且一旦发生,重新启动会持续报同一个错,只有先执行 ProbeConnectionAsync(测试连接)后才能恢复。核心异常:

MAS.Communication.ReadErrorException: Error reading words from PLC. Device: D, StartAddress: 8500, Length: 2
 ---> MAS.Communication.ReadErrorException: Not enough data read from PLC. Expected at least 4 bytes, but got 0 bytes.

深挖 McProtocolClient 收发/解析链路后,确认这主要不是掉线,而是几处缺陷叠加导致 TCP 流错位(desync) 且坏连接不失效:

  1. ExecuteAsync 读循环用「单次读到 < 缓冲区长度就 break」判断消息边界。TCP 是字节流,一帧 MC 响应可能被拆成多个分段到达,于是拿到残帧。
  2. MitsubishiHelper.IsIncorrectResponse 把「长度不足 minLength」误判为「正确」(return false)放行,残帧被当作有效响应,不再重试。
  3. McCommand.SetResponse 在帧短于帧头时直接 break,静默产出空 Response,最终在 ReadWordsAsync 变成 “got 0 bytes”。
  4. 读到 0 字节(对端关闭/EOF)或传输异常时未失效内部连接,CheckConnection() 仍返回 true,OpenAsync/TryReconnectAsync 开头的 if (CheckConnection()) return; 短路而不重连,复用坏 socket。

残帧只读走半截,socket 缓冲区残留的脏字节让之后每次读取都错位,故障持续;只有 ProbeConnectionAsync 因 finally 里 Disconnect() 销毁并重建 socket 才恢复——这正是「一旦发生、重启一直报,只有测试连接后才恢复」的原因。

本次修复:

  1. 新增 McFrameReader:按协议帧结构读满整帧——先按定长读满帧头(MC1E=2 / MC3E=11 / MC4E=15),再由帧头「响应数据长度」字段算出剩余字节并用 ReadExactly 读够整帧;MC1E 响应头无长度字段,按请求推算数据长度。读到 0 字节即判定对端关闭(EOF)→ 抛 ConnectionException;取消/超时抛 OperationCanceledException,二者严格区分,取消不当断线。ExecuteAsync 改用该读帧逻辑,彻底消除分段导致的残帧与流错位。
  2. 修正 IsIncorrectResponse:长度不足帧头判为「不正确」,不再把残帧当正确响应放行。
  3. SetResponse:帧长不足帧头时抛出明确的 ReadErrorException,不再静默返回空数据。
  4. 事务出错即失效连接:ExecuteAsync 在 EOF/传输异常时 InvalidateConnection(),使 CheckConnection() 随后返回 false,下次操作重连,避免脏字节错位蔓延;并为 IsSocketAlive 的 Poll + Available 半开检测补充说明性注释。

预期影响:

  • 断线/半开/分段场景下能自愈:坏连接被正确失效,ConnectAsync/TryReconnectAsync 会先断开再重建,无需再手动 ProbeConnectionAsync。
  • 三种帧(MC1E/MC3E/MC4E)均覆盖读满整帧逻辑。
  • 对外异常语义保持不变(读写路径仍以 ReadErrorException/WriteErrorException/ConnectionException 呈现)。
  • 版本号升至 1.1.1(修订版)。

评估

性能影响:

  • 读帧改为「先读帧头再按长度读满」,正常路径下的系统调用次数与原先相当,无明显开销;不再一次性 Rent(256) 缓冲区,改为按帧精确分配。
  • 消除了残帧误判后的无谓重试与错位,异常路径反而更快收敛。

兼容性问题:

  • 公共 API 无变更(改动集中在内部收发/解析)。对外异常类型保持不变,向后兼容。

额外信息

  • 新增 tests/McProtocol/McFrameReaderTest.cs:验证 MC3E/MC4E/MC1E 响应在逐字节等极端 TCP 分段下仍能正确拼接为完整帧、数据段中途 EOF 判为断线、取消不误判为断线。
  • 测试结果:40 通过 / 28 跳过(无实机 PLC)/ 0 失败;dotnet build 四个目标框架(net10.0 / net9.0 / net8.0 / net481)0 警告 0 错误。
  • 上层 mas-pressure-collector 的 PressureAcquisitionService 侧已配套做了自动重连(且重连前先强制 Disconnect() 再 TryReconnectAsync);本仓库这几处是从底层根治。

图片/截图

无(本次无 UI 变更)


Copyright (c) MAS(厦门威光) Corporation. All rights reserved.

## 根因
现场日志 “ReadErrorException: Not enough data read from PLC.
Expected at least 4 bytes, but got 0 bytes” 主要不是掉线,而是收发/解析
链路的几处缺陷叠加,导致 TCP 流错位(desync)且坏连接不失效:

1. ExecuteAsync 的读循环用「单次读到 < 缓冲区长度就 break」判断消息边界。
   TCP 是字节流,一帧 MC 响应可能被拆成多个分段到达,于是会拿到残帧。
2. MitsubishiHelper.IsIncorrectResponse 把「长度不足 minLength」误判为
   「正确」(return false)放行,残帧因此被当作有效响应,不再重试。
3. McCommand.SetResponse 在帧短于帧头时直接 break,静默产出空 Response,
   最终在 ReadWordsAsync 变成 “got 0 bytes”。
4. 读到 0 字节(对端关闭/EOF)或传输异常时未失效内部连接,CheckConnection()
   仍返回 true,OpenAsync/TryReconnectAsync 开头的 `if (CheckConnection()) return;`
   短路而不重连,复用坏 socket。

残帧只读走半截,socket 缓冲区残留的脏字节让之后每次读取都错位,故障持续;
只有 ProbeConnectionAsync 因 finally 里 Disconnect() 销毁并重建 socket 才恢复。
这正是「一旦发生、重启一直报,只有测试连接后才恢复」的原因。

## 修复
1. 新增 McFrameReader:按协议帧结构读满整帧——先按定长读满帧头
   (MC1E=2 / MC3E=11 / MC4E=15),再由帧头「响应数据长度」字段算出剩余字节
   并用 ReadExactly 读够整帧;MC1E 响应头无长度字段,按请求推算数据长度。
   读到 0 字节即判定对端关闭(EOF)→ 抛 ConnectionException;取消/超时抛
   OperationCanceledException,二者严格区分,取消不当断线。
   ExecuteAsync 改用该读帧逻辑,彻底消除分段导致的残帧与流错位。
2. 修正 IsIncorrectResponse:长度不足帧头判为「不正确」(return true),
   不再把残帧当正确响应放行。
3. SetResponse:帧长不足帧头时抛出明确的 ReadErrorException,不再静默返回空数据。
4. 事务出错即失效连接:ExecuteAsync 在 EOF/传输异常时 InvalidateConnection(),
   使 CheckConnection() 随后返回 false,下次操作重连,避免脏字节错位蔓延。
   并为 IsSocketAlive 的 Poll + Available 半开检测补充说明性注释。

三种帧(MC1E/MC3E/MC4E)均覆盖读满整帧逻辑。对外异常语义保持不变
(读写路径仍以 ReadErrorException/WriteErrorException/ConnectionException 呈现)。
版本号升至 1.1.1(修订版)。

## 测试
新增 McFrameReaderTest:验证 MC3E/MC4E/MC1E 响应在逐字节等极端 TCP 分段下
仍能正确拼接为完整帧、数据段中途 EOF 判为断线、取消不误判为断线。
40 通过 / 28 跳过(无实机)/ 0 失败;dotnet build 四目标框架 0 警告 0 错误。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@keeleycenc
keeleycenc merged commit fd70e74 into main Jul 15, 2026
1 check passed
@keeleycenc
keeleycenc deleted the fix/mc-half-open-connection branch July 15, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant