Modbus TCP 是工業自動化領域最通用的協議之一。本文詳細拆解 **MBAP Header、功能碼、大小端轉換、事務標識匹配、緩衝區殘留處理**,並提供可直接落地的 C# 封裝庫代碼結構與核心邏輯。
[MBAP Header (7 bytes)] [Function Code (1 byte)] [Data (N bytes)]
| 字段 | 長度 | 說明 |
|---|---|---|
| Transaction Identifier | 2 bytes | 事務 ID,請求/響應配對,防止串包 |
| Protocol Identifier | 2 bytes | 固定 0x0000 (Modbus 協議) |
| Length | 2 bytes | 後續字節數 (Unit ID + Function Code + Data) |
| Unit Identifier | 1 byte | 從站地址 (單機場景通常 0x00/0xFF) |

Modbus 規定 **Big-Endian (網絡序)**。x86/x64 為 Little-Endian,必須轉換。
bool isLittleEndian = BitConverter.IsLittleEndian; // .NET 內置屬性
// 或手動檢測
unsafe { int t = 1; bool le = (*(byte*)&t) == 1; }
public abstract class ValueHelper {
public static readonly ValueHelper Instance = BitConverter.IsLittleEndian
? new LittleEndianValueHelper() : new BigEndianValueHelper();
public abstract byte[] GetBytes(int value);
public abstract int GetInt32(byte[] data);
}
internal class LittleEndianValueHelper : ValueHelper {
public override byte[] GetBytes(int v) => BitConverter.GetBytes(v).Reverse().ToArray();
public override int GetInt32(byte[] d) => BitConverter.ToInt32(d.Reverse().ToArray(), 0);
}
private byte _tid = 0;
protected byte NextTid() => ++_tid; // 發送時賦值
// 接收響應時驗證
if (responseTid != _currentTid) DiscardPacket(); // 丟棄舊/亂序包
痛點:TCP 流無邊界,異常斷開/重發導致緩衝區殘留數據污染下次讀取。
方案對比:
| 策略 | 優點 | 缺點 |
|---|---|---|
| 每次讀超大緩衝 (如 256B) 忽略多餘 | 簡單,無需關閉 Socket | 浪費帶寬,高併發下內存壓力 |
| 讀完精確長度,錯誤時關閉重連 Socket | 徹底清理狀態 | 重連開銷大,可能丟失進行中的事務 |
| 推薦:長度前綴 + 狀態機解析 + 異常重置 | 健壯、高效、符合 TCP 流特性 | 實現復雜度較高 |
文中採用「讀 256B 忽略多餘」作為暫時方案,並建議後續升級為狀態機解析器。
public override void Send(byte[] data) {
// 1. 構建請求幀
var frame = new List<byte>(255);
frame.AddRange(ValueHelper.Instance.GetBytes(NextTid())); // TID
frame.AddRange(new byte[] { 0, 0 }); // PID
frame.AddRange(ValueHelper.Instance.GetBytes((ushort)(data.Length + 7))); // Length
frame.Add(0); // UID
frame.Add((byte)FunctionCode.Write); // FC=16
frame.AddRange(ValueHelper.Instance.GetBytes(StartingAddress)); // Addr
frame.AddRange(ValueHelper.Instance.GetBytes((ushort)(data.Length / 2))); // Qty
frame.Add((byte)data.Length); // Byte Count
frame.AddRange(data); // Data
// 2. 發送
_socketWrapper.Write(frame.ToArray());
Application.DoEvents(); // 簡易讓出 UI 線程
// 3. 讀取響應 (固定 12 字節)
byte[] resp = _socketWrapper.Read(12);
// TODO: 校驗響應 TID、FC、錯誤碼
}
public override byte[] Receive() {
var frame = new List<byte>(255);
frame.AddRange(ValueHelper.Instance.GetBytes(NextTid()));
frame.AddRange(new byte[] { 0, 0 });
frame.AddRange(ValueHelper.Instance.GetBytes((short)6)); // Length=6 (UID+FC+Addr+Qty)
frame.Add(0);
frame.Add((byte)FunctionCode.Read); // FC=3
frame.AddRange(ValueHelper.Instance.GetBytes(StartingAddress));
frame.AddRange(ValueHelper.Instance.GetBytes((short)30)); // Qty=30
_socketWrapper.Write(frame.ToArray());
Application.DoEvents();
// 讀取響應頭 (8 字節) + 數據
byte[] header = _socketWrapper.Read(8); // 實際應動態解析 Length
byte[] all = _socketWrapper.Read(256); // 讀大緩衝防殘留
// 校驗 TID
ushort tid = (ushort)((all[0] << 8) | all[1]);
if (tid != _currentTid) return Array.Empty<byte>();
byte byteCount = all[8];
byte[] result = new byte[byteCount];
Array.Copy(all, 9, result, 0, byteCount);
return result;
}
IModbusWrapper 介面定義 Send/Receive/Connect。ModbusTcpWrapper 實現 TCP 通訊邏輯。ModbusRtuWrapper 預留串口實現 (文章未完成)。ValueHelper 策略類隔離大小端邏輯。源碼下載:CSharpModBusExample
Length 字段動態讀取,而非固定長度。SemaphoreSlim 串行化請求/響應,避免多線程串包。