简介:一份基于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 Task<WriteResult> 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是站号,通常填1;startAddress是寄存器地址,注意协议层地址从0开始,人机界面显示时一般要加1偏移。2.3 从站的数据存储与响应分派从站侧的核心是一个ConcurrentDictionary<ushort, 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); } }异常码的返回是很多初学者容易忽略的地方。比如收到一个不支持的功本文还有配套的精品资源,点击获取
简介:一份基于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 Task<WriteResult> 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是站号,通常填1;startAddress是寄存器地址,注意协议层地址从0开始,人机界面显示时一般要加1偏移。2.3 从站的数据存储与响应分派从站侧的核心是一个ConcurrentDictionary<ushort, 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); } }异常码的返回是很多初学者容易忽略的地方。比如收到一个不支持的功本文还有配套的精品资源,点击获取