基于Socket编程,实现ModbusTCP通讯
2026/6/3 2:20:56 网站建设 项目流程

目录

一、准备工作

二、手动封装ModbusTCP报文

1、了解Socket

socket是什么?

socket能用来做什么?

2、了解ModbusTCP报文结构

3、开启Modbus Slave

4、创建Socket连接

5、手动封装读取寄存器报文

6、手动封装写入寄存器报文


一、准备工作

1、硬件:两台电脑、一根网线

2、软件:Visual Studio 2026 、Modbus Slave 、WireShark

3、封装协议:ModbusTCP

二、手动封装ModbusTCP报文

1、了解Socket

  • socket是什么?

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

  • socket能用来做什么?

在同一台主机上,会运行多个软件,每个软件都是一个进程,在本地运行的多个进程之间如何通讯?

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(Solaris门和Sun RPC)

在本地运行,可以通过进程PID来唯一标识一个进程

那在不同主机运行的进程,通过网络连接,如何唯一标识一个进程?进程间又如何通讯?

Socket就是用来解决这个问题

网络间进程唯一标识方式:IP地址(网络层)+ 传输层的“协议 + 端口”

这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

2、了解ModbusTCP报文结构

ModbusTCP协议报文结构 = MBAP报文头 + PDU数据

MBAP报文头:固定长度,7个字节

  • Transaction Identifier(2字节) :事务 ID。用于匹配请求和响应,确保收到的回复是给这个请求的。
  • Protocol Identifier (2字节):协议 ID。0 代表 Modbus 协议。
  • Length(2字节) :长度。表示后面还有 几个字节的数据
  • Unit Identifier(1字节) :单元 ID(也叫从站地址)。用于在串行链路或网关中区分不同的从站设备。

PDU 协议数据单元:不固定长度

  • Function Code (3):功能码。表示“读保持寄存器”。
  • Reference Number (0):起始地址。表示从地址 0 开始读。
  • Word Count (1):数量。表示读 1 个寄存器。

3、开启Modbus Slave

4、创建Socket连接

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net.Sockets; using System.Net; namespace SocketClient { internal class Program { private static Socket ClientSocket = null; //创建Socket字段 private static byte[] m_DataBuffer = new byte[1024]; private static ushort transactionId = 1;//事务ID static void Main(string[] args) { ClientSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);//实例化Socket对象 IPAddress ServerIP = new IPAddress(new byte[] {192,168,1,100});//远程主机IP地址 IPEndPoint ServerPoint = new IPEndPoint(ServerIP,502); //远程主机IP+端口 try { ClientSocket.Connect(ServerPoint); //连接Modbus Slave Console.WriteLine("连接服务器成功"); } catch (Exception ex) { Console.WriteLine("连接服务器失败,按回车键退出"); Console.WriteLine(ex.Message); return; } } } }

程序运行结果

WireShark抓包结果

三次握手成功

5、手动封装读取寄存器报文

  • 最简版
byte[] hexData = new byte[12]; //modbusTCP报文 //============================读取一个寄存器============================ //MBAP头部,固定7个字节 hexData[0] = 0x00; hexData[1] = 0x01;//事务ID hexData[2] = 0x00; hexData[3] = 0x00;//协议标识符 0 = modbus协议 hexData[4] = 0x00; hexData[5] = 0x06;//数据字节数 hexData[6] = 0x01; //从站ID //PDU数据部分,不固定长度 hexData[7] = 0x03; //功能码 hexData[8] = 0x00; hexData[9] = 0x00;//起始地址 hexData[10] = 0x00; hexData[11] = 0x01;//寄存器数目 ClientSocket.Send(hexData); Console.WriteLine("发送完毕,按回车键退出"); // 1. 接收数据,并获取实际接收到的字节数 int receiveLength = ClientSocket.Receive(m_DataBuffer); Console.WriteLine($"成功接收到 {receiveLength} 个字节的数据:"); // 2. 仅遍历“实际接收到的长度”,而不是整个数组长度 StringBuilder sb = new StringBuilder(); // 使用 StringBuilder 拼接字符串效率更高 for (int i = 0; i < receiveLength; i++) { // "X2" 表示将数字转换为大写十六进制,不足两位补零 (例如 A -> 0A) sb.Append(m_DataBuffer[i].ToString("X2") + " "); } // 3. 一次性打印结果 Console.WriteLine(sb.ToString());

程序运行结果

WireShark抓包结果

  • 升级版:封装成读取寄存器的函数
//读取一个PLC寄存器 static void Modbus_Read_Register(Socket ModbusSocket, ushort addr, out ushort dest) { byte[] SendHexData = new byte[12]; //modbusTCP报文 byte[] ReceivehexData = new byte[64]; //modbusTCP报文 byte[] bytes = new byte[2]; //============================读取一个寄存器============================ //MBAP头部,固定7个字节 UShortToBytes(transactionId, out SendHexData[0], out SendHexData[1]);//事务ID SendHexData[2] = 0x00; SendHexData[3] = 0x00; //协议标识符 0 = modbus协议 SendHexData[4] = 0x00; SendHexData[5] = 0x06; //数据字节数 SendHexData[6] = 0x01; //从站ID //PDU数据部分,不固定长度 SendHexData[7] = 0x03; //功能码 UShortToBytes(addr, out SendHexData[8], out SendHexData[9]); //起始地址 SendHexData[10] = 0x00; SendHexData[11] = 0x01; //寄存器数目 //发送给服务端报文 ModbusSocket.Send(SendHexData); //客户端接收报文 transactionId++; int receiveLength = ModbusSocket.Receive(ReceivehexData); //比对事务ID是否一致? for (int i = 0; i < 2; i++) { bool flag = ReceivehexData[i] == SendHexData[i]; if (flag == false) { dest = 0; return; } } // dest = (ushort)((ReceivehexData[9] << 8) | ReceivehexData[10]); } //读取多个PLC寄存器 static void Modbus_Read_Registers(Socket ModbusSocket, ushort addr,ushort nb, out ushort[] dest) { byte[] SendHexData = new byte[12]; //modbusTCP报文 byte[] ReceivehexData = new byte[64]; //modbusTCP报文 byte[] bytes = new byte[2*nb]; //============================读取一个寄存器============================ //MBAP头部,固定7个字节 UShortToBytes(transactionId, out SendHexData[0], out SendHexData[1]);//事务ID SendHexData[2] = 0x00; SendHexData[3] = 0x00; //协议标识符 0 = modbus协议 SendHexData[4] = 0x00; SendHexData[5] = 0x06; //数据字节数 SendHexData[6] = 0x01; //从站ID //PDU数据部分,不固定长度 SendHexData[7] = 0x03; //功能码 UShortToBytes(addr, out SendHexData[8], out SendHexData[9]); //起始地址 UShortToBytes(nb, out SendHexData[10], out SendHexData[11]); //寄存器数目 //发送给服务端报文 ModbusSocket.Send(SendHexData); //客户端接收报文 int receiveLength = ModbusSocket.Receive(ReceivehexData); transactionId++; //比对事务ID是否一致? for (int i = 0; i < 2; i++) { bool flag = ReceivehexData[i] == SendHexData[i]; if (flag == false) { dest = null; return; } } //事务ID比对一致 // 1. 计算有多少个寄存器数据 (Modbus 每个寄存器 2 字节) // 注意:这里要减去前面的报头长度(根据你的截图,看起来是从索引 9 开始的数据) int dataLength = receiveLength - 9; int registerCount = dataLength / 2; // 【修复点 1】:先初始化 out 参数,防止报错 dest = new ushort[registerCount]; for (int i = 9; i < receiveLength; i++) { bytes[i-9] = ReceivehexData[i]; } // 2. 循环解析 for (int i = 0; i < registerCount; i++) { // 【修复点 2】:动态计算当前要读取的字节位置 // 起始位置是 9,每个寄存器占 2 字节,所以偏移量是 i * 2 int currentIndex = 9 + (i * 2); // 提取当前的两个字节 byte highByte = ReceivehexData[currentIndex]; // 高位在前 (Modbus Big-Endian) byte lowByte = ReceivehexData[currentIndex + 1]; // 低位在后 // 组合成 ushort (手动移位比 BitConverter 更快且可控字节序) dest[i] = (ushort)((highByte << 8) | lowByte); } }

调用读取寄存器函数

for (int i = 0; i < 1; i++) { ushort value = 0; Modbus_Read_Register(ClientSocket, 0, out value); Console.WriteLine(value.ToString("X4")); Modbus_Read_Register(ClientSocket, 1, out value); Console.WriteLine(value.ToString("X4")); Modbus_Read_Register(ClientSocket, 2, out value); Console.WriteLine(value.ToString("X4")); Console.WriteLine("================================"); }

程序运行结果:

WireShark抓包结果:

6、手动封装写入寄存器报文

待更新

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询