C# 上位机开发 - Modbus TCP 主站实现(一篇搞定 建议收藏)
专栏导航 :上一篇:ModbusRTU主站实现 | 下一篇:TCP Socket通信
前言
Modbus TCP 把 Modbus PDU 封装在 TCP 里,走以太网,无需 CRC(TCP 已保证顺序与校验)。本文解析 MBAP 头 、实现 ModbusTcpMaster,演示 NModbus4 TCP ,并用 工厂模式 统一 RTU/TCP 调用,便于产线混用串口网关与 PLC 网口。
MBAP 头结构(7 字节)
| 字段 | 长度 | 说明 |
|---|---|---|
| Transaction Id | 2 | 事务标识,主站递增 |
| Protocol Id | 2 | 固定 0x0000 |
| Length | 2 | 后续字节数(UnitId + PDU) |
| Unit Id | 1 | 从站地址,直连 PLC 常为 1 |
其后为 PDU (功能码 + 数据),与 RTU 的 PDU 相同 ,但 不含 CRC。
示例:读保持寄存器 slave=1,起始0,数量2
00 01 00 00 00 06 01 03 00 00 00 02
| MBAP (6) |Uid| PDU ...
ModbusTcpMaster 手动实现
csharp
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace UpperComputer.Communication.Modbus
{
public class ModbusTcpMaster : IDisposable
{
private readonly TcpClient _client;
private readonly NetworkStream _stream;
private ushort _transactionId;
public ModbusTcpMaster(string host, int port = 502)
{
_client = new TcpClient();
_client.Connect(host, port);
_stream = _client.GetStream();
_stream.ReadTimeout = 3000;
_stream.WriteTimeout = 3000;
}
public async Task<ushort[]> ReadHoldingRegistersAsync(byte unitId, ushort start, ushort count)
{
ushort tid = ++_transactionId;
var pdu = new byte[5];
pdu[0] = 0x03;
pdu[1] = (byte)(start >> 8);
pdu[2] = (byte)(start & 0xFF);
pdu[3] = (byte)(count >> 8);
pdu[4] = (byte)(count & 0xFF);
var mbap = BuildMbap(tid, unitId, (ushort)(pdu.Length + 1));
await _stream.WriteAsync(mbap, 0, mbap.Length);
await _stream.WriteAsync(pdu, 0, pdu.Length);
var header = await ReadExactAsync(7);
int len = (header[4] << 8) | header[5];
var tail = await ReadExactAsync(len);
if (tail[1] != 0x03) throw new InvalidOperationException("功能码不匹配");
int byteCount = tail[2];
var result = new ushort[count];
for (int i = 0; i < count; i++)
result[i] = (ushort)((tail[3 + i * 2] << 8) | tail[4 + i * 2]);
return result;
}
private byte[] BuildMbap(ushort transactionId, byte unitId, ushort length)
{
return new byte[]
{
(byte)(transactionId >> 8), (byte)(transactionId & 0xFF),
0, 0,
(byte)(length >> 8), (byte)(length & 0xFF),
unitId
};
}
private async Task<byte[]> ReadExactAsync(int n)
{
var buf = new byte[n];
int offset = 0;
while (offset < n)
{
int read = await _stream.ReadAsync(buf, offset, n - offset);
if (read == 0) throw new IOException("连接关闭");
offset += read;
}
return buf;
}
public void Dispose()
{
_stream?.Dispose();
_client?.Dispose();
}
}
}
TCP 必须按长度读满,不能假设一次 Read 等于一帧(粘包见第八篇)。
NModbus4 TCP
csharp
using Modbus.Device;
using System.Net.Sockets;
public static IModbusMaster CreateTcpMaster(string ip, int port = 502)
{
var client = new TcpClient(ip, port);
return ModbusIpMaster.CreateIp(client);
}
// 用法与 RTU 相同
ushort[] values = master.ReadHoldingRegisters(1, 0, 10);
工厂模式统一 RTU / TCP
csharp
public enum ModbusTransport { Rtu, Tcp }
public interface IModbusSession : IDisposable
{
ushort[] ReadHoldingRegisters(byte slaveId, ushort start, ushort count);
}
public class ModbusSessionFactory
{
public static IModbusSession Create(ModbusTransport transport, string connection)
{
return transport switch
{
ModbusTransport.Rtu => new RtuSession(connection), // COM3 或 COM3:9600
ModbusTransport.Tcp => new TcpSession(connection), // 192.168.1.10:502
_ => throw new ArgumentOutOfRangeException(nameof(transport))
};
}
}
// RtuSession / TcpSession 内部分别包装 NModbus CreateRtu / CreateIp
业务层只依赖 IModbusSession,配置切换 Transport 即可,适合 MES 多设备模板。
RTU vs TCP 对比
| 项目 | RTU | TCP |
|---|---|---|
| 物理 | RS-485 | Ethernet |
| 校验 | CRC16 | 无(靠 TCP) |
| 并发 | 总线半双工 | 多连接看设备 |
| 端口 | COM | 502(默认) |
| 网关 | 串口服务器 | 直接 PLC |
FAQ
Q1:502 连接被拒绝?
PLC Modbus TCP 未启用、防火墙、或端口被占。
Q2:Transaction Id 必须递增吗?
规范建议;重复可能导致部分设备混淆,保持递增最稳。
Q3:Unit Id 填什么?
直连设备常为 1;经 串口转以太网 网关时 Unit Id 对应 RTU 从站地址。
Q4:与 Modbus RTU over TCP 区别?
有的网关把 整帧 RTU(含 CRC) 透传 TCP,需 Raw 解析,不是标准 Modbus TCP。
小结
记住 MBAP + PDU ,异步 ReadExact 读满。工厂模式让 RTU/TCP 共用业务代码。下一篇讲通用 TCP Socket 与重连。
专栏导航 :上一篇:ModbusRTU主站实现 | 下一篇:TCP Socket通信
实战扩展:与日志、配置结合
工业现场调试串口/TCP 时,建议把 原始十六进制帧 与 解析结果 分开落日志(Serilog/NLog),并带上时间戳与 COM/IP。配置层用 appsettings.json 维护 PortName、BaudRate、PollIntervalMs,避免硬编码。联调阶段打开 Trace 级 日志,验收后降为 Warning,防止磁盘占满。
性能与内存建议
| 场景 | 建议 |
|---|---|
| 100ms 轮询 50 台设备 | 线程池 + 异步,避免 50 个 Thread |
| 大帧图像数据 | 不要用 List 反复 RemoveRange,用 RingBuffer 或 ArrayPool |
| UI 刷新 | 合并 200ms 内多条数据一次绑定,降低 Dispatcher 压力 |
延伸阅读
- 第三篇粘包与 RingBuffer 配合本篇传输层使用;
- 第四篇 CRC 是第五、六篇的共同基础;
- 第七篇工厂模式可扩展到 OPC UA Gateway 的多协议配置。