Modbus TCP 主站实现

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 维护 PortNameBaudRatePollIntervalMs,避免硬编码。联调阶段打开 Trace 级 日志,验收后降为 Warning,防止磁盘占满。

性能与内存建议

场景 建议
100ms 轮询 50 台设备 线程池 + 异步,避免 50 个 Thread
大帧图像数据 不要用 List 反复 RemoveRange,用 RingBuffer 或 ArrayPool
UI 刷新 合并 200ms 内多条数据一次绑定,降低 Dispatcher 压力

延伸阅读

  • 第三篇粘包与 RingBuffer 配合本篇传输层使用;
  • 第四篇 CRC 是第五、六篇的共同基础;
  • 第七篇工厂模式可扩展到 OPC UA Gateway 的多协议配置。