手把手教你用 C# 搭建一个 Modbus TCP 从站模拟器
你有没有遇到过这样的场景:上位机软件已经写好了,但现场的 PLC 还没到货?或者想测试主站对异常响应的处理能力,却找不到能“故意出错”的硬件设备?
别急——今天我们不靠任何真实仪表,只用一段 C# 代码,就能让你的电脑变身一台“虚拟 PLC”,对外提供标准 Modbus TCP 接口。整个过程不到 200 行代码,核心依赖只有一个开源库:nmodbus4。
这不仅是个玩具项目,更是工业自动化开发中极具实战价值的调试利器。接下来我会带你一步步拆解它的设计逻辑,从零开始构建一个可读、可写、还能动态刷新数据的完整从站模拟器。
为什么选 nmodbus4?它到底解决了什么问题?
在工业通信领域,Modbus 协议就像空气一样无处不在。尤其是Modbus TCP,凭借其基于以太网的高带宽和易部署特性,早已成为 SCADA、HMI 和智能仪表之间的主流通信方式。
但如果你尝试从头实现一个 Modbus 服务端,很快就会被这些问题缠住:
- 报文格式怎么组织?MBAP 头要不要手动拼?
- 功能码 FC3(读保持寄存器)和 FC16(写多个寄存器)该怎么解析?
- 字节序如何处理?大小端转换会不会搞错?
- 多客户端并发访问时数据安全怎么保证?
这时候,nmodbus4就派上了大用场。它是专为 .NET 平台打造的开源 Modbus 协议栈,支持 TCP、RTU、ASCII 多种模式,封装了所有底层细节,让你只需关注业务逻辑。
更重要的是,它完全基于 .NET Standard 2.0 构建,意味着你的模拟器不仅能跑在 Windows 上,也能轻松移植到 Linux 或 macOS 环境,甚至部署成 Docker 容器。
先跑通最简版本:三步启动一个 TCP 从站
我们先抛开复杂功能,写出第一个能工作的原型。目标很明确:让这台机器监听 502 端口,响应主站的读写请求。
第一步:安装依赖
通过 NuGet 安装核心库:
dotnet add package NModbus4第二步:初始化从站与数据区
using Modbus.Device; using System; using System.Net; using System.Net.Sockets; using System.Threading; namespace ModbusSlaveSimulator { class Program { private static ModbusIpSlave slave; private static IDataStore dataStore; static void Main(string[] args) { byte slaveId = 1; // 标准从站地址 int port = 502; // 默认 Modbus TCP 端口 IPAddress address = IPAddress.Any; // 监听所有网卡 var tcpListener = new TcpListener(address, port); dataStore = DataStoreFactory.CreateDefaultDataStore(); // 预设初始值:HR[0]=100, HR[1]=200 dataStore.HoldingRegisters[0] = 100; dataStore.HoldingRegisters[1] = 200; slave = ModbusIpSlave.CreateTcp(slaveId, tcpListener); slave.DataStore = dataStore; Thread listenThread = new Thread(ListenLoop); listenThread.Start(); Console.WriteLine($"✅ Modbus TCP Slave 已启动,监听端口 {port}"); Console.WriteLine("按任意键停止服务..."); Console.ReadKey(); slave.Dispose(); } static void ListenLoop() { try { slave.Listen(); // 阻塞式监听 } catch (Exception ex) { Console.WriteLine("❌ 监听异常:" + ex.Message); } } } }就这么简单,一个基本可用的 Modbus 从站就完成了!
它是怎么工作的?
当主站发送如下请求报文时:
00 01 00 00 00 06 01 03 00 00 00 02nmodbus4 会自动完成以下动作:
1. 解析 MBAP 头部,确认是发给自己的请求;
2. 提取功能码0x03,识别为“读保持寄存器”;
3. 查找起始地址 0,数量 2 的数据;
4. 从dataStore.HoldingRegisters中取出[100, 200];
5. 自动组包并返回响应:
00 01 00 00 00 05 01 03 04 00 64 00 C8整个过程无需你写一行协议解析代码。
💡 提示:这里的
00 64是十进制 100 的十六进制表示,00 C8是 200,符合 Modbus 双字节高位在前规则。
让数据“活起来”:加入动态更新机制
静态数据只能应付简单测试。真正的工业设备,比如温度传感器、电机状态,都是随时间变化的。那我们能不能让某个寄存器每秒自动更新一次?
当然可以!关键是自定义IDataStore实现。
创建可动态刷新的数据存储
public class DynamicDataStore : DataStore { private Timer _timer; private Random _rand = new Random(); public DynamicDataStore() : base() { // 每隔 1 秒触发一次更新 _timer = new Timer(UpdateSensorValue, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); } private void UpdateSensorValue(object state) { ushort value = (ushort)_rand.Next(0, 65535); // 模拟 AD 采样值 lock (this.SyncRoot) // 必须加锁,确保线程安全 { this.HoldingRegisters[2] = value; } Console.WriteLine($"🔄 [动态更新] HR[2] = {value}"); } }然后替换主程序中的默认数据区:
dataStore = new DynamicDataStore(); slave.DataStore = dataStore;现在,只要你读取 HR 地址 2 的值,每次都会不一样——就像真的接了一个不断波动的传感器。
⚠️ 注意:所有对
HoldingRegisters的修改都必须用lock(SyncRoot)包裹。这是 nmodbus4 提供的同步对象,防止多线程下出现数据竞争。
更进一步:模拟异常与错误场景
一个好的测试工具不仅要“正常工作”,还要能“故意出错”。
假设你想验证主站是否具备良好的容错能力,能否正确处理非法地址或写保护区域的写入尝试。这时就可以拦截请求,手动抛出异常。
虽然 nmodbus4 没有直接暴露中间件钩子,但我们可以通过继承DataStore并重写OnValidate方法来实现控制。
例如,禁止写入 HR[100] 到 HR[199] 这段地址:
protected override void OnValidate(IPointSource source, Point point, ValidateMode mode) { if (source == this.HoldingRegisters && point.StartAddress >= 100 && point.StartAddress <= 199 && mode == ValidateMode.Write) { throw new SlaveExceptionResponse(ExceptionCode.IllegalDataAddress); } base.OnValidate(source, point, mode); }这样,一旦主站尝试向该区间写数据,就会收到标准的“非法数据地址”异常(0x02),从而触发其错误处理流程。
这类能力对于提升系统鲁棒性至关重要。
实战应用:一机模拟多台设备
在大型系统中,往往需要连接多个从站设备。难道要为每个 Slave ID 单独运行一个进程?
不需要。nmodbus4 支持在一个TcpListener上注册多个从站实例,共享同一个 502 端口。
var network = new ModbusTcpSlaveNetwork(tcpListener); // 添加从站 ID=1 var slave1 = new ModbusIpSlave(1, network.Transport); slave1.DataStore = CreateInitialDataStore(1); network.AddSlave(slave1); // 添加从站 ID=2 var slave2 = new ModbusIpSlave(2, network.Transport); slave2.DataStore = CreateInitialDataStore(2); network.AddSlave(slave2); // 启动监听 network.ListenAsync();这样一来,主站只要连接同一 IP 不同 Unit ID,就能访问不同设备的数据区,完美模拟分布式现场环境。
工程最佳实践建议
当你准备将这个模拟器投入实际项目使用时,请记住以下几点:
✅ 使用独立 DataStore 实例
每个从站应使用独立的数据存储,避免共用导致状态污染。
✅ 控制定时任务粒度
不要在Timer回调里执行数据库查询或网络请求等耗时操作,会影响响应实时性。建议采用缓存+异步更新策略。
✅ 开启日志追踪
虽然 nmodbus4 不内置日志框架,但你可以包装Transport层或利用 AOP 注入日志,便于后期排查问题。
✅ 生产环境做访问控制
虽然是模拟器,在开放网络中仍需防范未授权访问。可通过防火墙限制源 IP,或关闭非必要功能码。
✅ 结合 Wireshark 调试
抓包分析是最直观的调试手段。你会发现 nmodbus4 生成的报文完全符合规范,连事务 ID 都能自动匹配请求与响应。
它能解决哪些真实痛点?
| 场景 | 传统做法 | 使用本方案 |
|---|---|---|
| 设备未到货 | 开发停滞等待 | 上位机提前联调 |
| 边界条件测试 | 难以复现超限值 | 主动注入异常数据 |
| 协议兼容性验证 | 依赖多种硬件 | 软件模拟全功能码 |
| 教学演示 | 缺少实物设备 | 学生动手调试代码 |
特别是在 CI/CD 流水线中,这种纯软件的 Modbus 节点可以作为自动化测试的一部分,显著提升交付质量。
写在最后
今天我们用不到 200 行 C# 代码,搭建了一个功能完整的 Modbus TCP 从站模拟器。它不仅能响应常规读写请求,还支持动态数据更新、异常模拟、多设备仿真等高级特性。
而这一切的背后,正是nmodbus4这个强大又轻量的类库在支撑。它把复杂的协议细节封装得严丝合缝,只留下简洁清晰的 API 接口,极大提升了开发效率。
未来,随着数字孪生、边缘计算的发展,这类“软PLC”式的仿真工具会越来越重要。你可以在此基础上集成 MQTT、OPC UA、REST API,把它变成一个多协议桥接平台。
如果你正在做工业通信相关的开发,不妨试试把这个小工具加入你的调试箱。也许下一次紧急联调时,它就能救你一命。
欢迎在评论区分享你的使用经验,或者提出改进想法。代码已托管 GitHub,欢迎 Fork & Star!