恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C#实现Modbus TCP主从站完整源码解析:无需手写协议栈
首页
资讯中心
/
C#实现Modbus TCP主从站完整源码解析:无需手写协议栈
C#实现Modbus TCP主从站完整源码解析:无需手写协议栈
发布时间:2026/10/21 4:48:05
简介一份基于TCP协议的Modbus通信C#完整源码面向工业自动化领域的C#开发者亦适合需要实现PLC、现场设备网络通信的工程人员。代码围绕Modbus_TCP_class_demo展开覆盖连接管理、请求报文构造、NetworkStream数据收发、报文解析、异常码处理等核心环节部分场景还引入async/await异步与重试机制对项目落地有直接参考价值。压缩包共46个文件以cs源文件为核心另含sln/csproj工程配置、xml说明文档、dll依赖库及exe可执行程序整体仅148KB目录结构清晰便于快速研读与调试。目前已有1073人学习下载。对于希望掌握Modbus TCP协议报文格式、功能码运用及C#网络编程细节的开发者这份代码可作为从理论到实践的有效范例。1. 一个不用写协议栈的TCP Modbus上位机C#源码里藏着的主从站完整实现做工业上位机开发的十有八九都遇到过同一个需求设备支持Modbus TCP协议但厂家给的通信Demo要么是半成品要么只写了读不写写真要集成到自己的系统里还得自己补一堆异常处理和重连逻辑。这份基于TCP的Modbus C#源码把主站和从站两条链路都做完了而且报文解析、超时重试、断线重连这些最容易翻车的细节都有现成实现。它解决的核心问题是你不用再对着Modbus协议规范逐字节抠报文结构也不用在Socket层自己拼半包粘包处理逻辑。适合设备调试工程师、MES系统开发者和准备做工业网关的嵌入式转上位机开发者直接拿去做二开。整套代码结构清爽主站能批量读写保持寄存器和线圈从站能模拟真实设备响应跑起来也就一个控制台的事。2. 整体架构拆解从Socket连接到寄存器映射的分层设计2.1 项目目录与命名空间组织拿到源码先看目录结构。代码按功能边界分了四个核心命名空间ModbusTcp.Core放协议解析核心类ModbusTcp.Master是主站客户端实现ModbusTcp.Slave是从站服务端实现ModbusTcp.Utils放着日志和字节序转换等公共工具类。这种分层方式的好处是主站和从站共用一套报文解析逻辑改动协议层时不用动上层业务代码。ModbusTcp.sln ├─ ModbusTcp.Core/ │ ├─ ModbusMessage.cs // 报文构造与解析 │ ├─ FunctionCode.cs // 功能码枚举 │ └─ ModbusRegister.cs // 寄存器模型 ├─ ModbusTcp.Master/ │ ├─ ModbusMaster.cs // 主站客户端 │ ├─ MasterConfig.cs // 连接配置类 │ └─ WriteResult.cs // 写入结果封装 ├─ ModbusTcp.Slave/ │ ├─ ModbusSlave.cs // 从站服务端 │ ├─ SlaveDataStore.cs // 寄存器数据存储 │ └─ SlaveSession.cs // 会话管理 └─ ModbusTcp.Utils/ └─ ByteConverter.cs // 字节序工具每个类都保持单一职责。比如ModbusRegister只描述一个寄存器的地址、值和读写权限不掺入任何Socket通信逻辑这样在单元测试时可以独立验证数据映射是否正确。2.2 主站与从站的通信链路设计主站的处理流程是一个典型的请求-响应循环ModbusMaster.SendRequest()构造报文后通过TCP发送然后阻塞等待从站响应并校验功能码和事务标识符。这里的关键设计是用了ManualResetEventSlim做同步等待配合内部维护的_transactionId自增计数器来匹配请求和响应避免并发请求时出现错乱。public async TaskWriteResult WriteRegisterAsync(byte slaveId, ushort startAddress, ushort value) { var message ModbusMessage.CreateWriteSingleRegister(slaveId, startAddress, value); var request message.Serialize(); var response await SendAndReceiveAsync(request); if (response null) return WriteResult.Fail(响应超时或连接已断开); var parsed ModbusMessage.Parse(response); if (parsed.FunctionCode ! message.FunctionCode) return WriteResult.Fail($功能码不匹配: 期望{message.FunctionCode}, 实际{parsed.FunctionCode}); // 校验写入的地址和值是否与请求一致 if (parsed.StartAddress ! startAddress || parsed.Value ! value) return WriteResult.Fail(从站返回的数据与请求不一致); return WriteResult.Ok(); }这段写单个寄存器的代码把完整流程串起来了构造报文 → 发送 → 解析响应 → 一致性校验。为什么在写完寄存器后还要对比响应里的值和请求时的值因为Modbus规范里写单寄存器的响应必须原样回显请求内容如果从站返回的数据和请求对不上说明从站内部逻辑有问题或地址越界被截断了。参数说明slaveId是站号通常填1startAddress是寄存器地址注意协议层地址从0开始人机界面显示时一般要加1偏移。2.3 从站的数据存储与响应分派从站侧的核心是一个ConcurrentDictionaryushort, ModbusRegister作为寄存器存储收到请求后先根据功能码做权限判断再查字典取数据并拼接响应报文。从站同时处理多个TCP客户端连接时每个连接对应一个SlaveSession实例数据存储是共享的这样多个主站读取的是同一份寄存器数据。public byte[] HandleRequest(byte[] request) { var message ModbusMessage.Parse(request); switch (message.FunctionCode) { case FunctionCode.ReadHoldingRegisters: var data _dataStore.ReadHoldingRegisters(message.StartAddress, message.Quantity); return ModbusMessage.CreateReadResponse(message.TransactionId, data); case FunctionCode.WriteSingleRegister: _dataStore.WriteRegister(message.StartAddress, message.Value); return request; // 写单寄存器回显原报文 case FunctionCode.WriteMultipleRegisters: var success _dataStore.WriteRegisters(message.StartAddress, message.Values); return ModbusMessage.CreateWriteMultipleResponse(message.TransactionId, message.StartAddress, message.Quantity, success); default: return ModbusMessage.CreateExceptionResponse(message.TransactionId, FunctionCode.ErrorIllegalFunction); } }异常码的返回是很多初学者容易忽略的地方。比如收到一个不支持的功本文还有配套的精品资源点击获取