Bootstrap

基于ModbusTcp协议的Java Socket通信 报文编码格式与数据采集过程详解【上】


1 简介
modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的modbus协议:modbusTCP
Modbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。
标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。

2 ModbusTCP数据帧
ModbusTCP的数据帧可分为两部分:MBAP+PDU。
在这里插入图片描述

2.1 报文头MBAP
MBAP为报文头,长度为7字节,组成如下:
事务处理标识 协议标识 长度 单元标识符
2字节 2字节 2字节 1字节

事务处理标识 :可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文。
协议标识符 :00 00表示ModbusTCP协议。
长度 :表示接下来的数据长度,单位为字节。
单元标识符 :设备地址。
在这里插入图片描述

2.2 帧结构PDU
PDU由功能码+数据组成。功能码为1字节,数据长度不定,由具体功能决定。

2.2.1 功能码
modbus的操作对象有四种:线圈、离散输入、输入寄存器、保持寄存器。
线圈:PLC的输出位,开关量,在MODBUS中可读可写
离散量:PLC的输入位,开关量,在MODBUS中只读
输入寄存器:PLC中只能从模拟量输入端改变的寄存器,在MODBUS中只读
保持寄存器RW:PLC中用于输出模拟量信号的寄存器,可读可写

根据对象的不同,modbus的功能码有:
0x01:读线圈
0x05:写单个线圈
0x0F:写多个线圈
0x02:读离散量输入
0x04:读输入寄存器
0x03:读保持寄存器
0x06:写单个保持寄存器
0x10:写多个保持寄存器

Wireshark第三方抓包netdebugtool(client)和MThings(ModusTCP):

ip过滤

   ip.src ==192.168.1.104 显示源地址为192.168.1.104的数据包列表
   ip.dst==192.168.1.104, 显示目标地址为192.168.1.104的数据包列表
   ip.addr == 192.168.1.104 显示源IP地址或目标IP地址为192.168.1.104的数据包列表

端口过滤

  tcp.port ==80,  显示源主机或者目的主机端口为80的数据包列表。
  tcp.srcport == 80,  只显示TCP协议的源主机端口为80的数据包列表。
  tcp.dstport == 80,只显示TCP协议的目的主机端口为80的数据包列表。

Http模式过滤

http.request.method=="GET",   只显示HTTP GET方法的。

逻辑运算符为 and/or/not

过滤多个条件组合时,使用and/or,比如获取IP地址为192.168.1.104的ICMP数据包表达式为ip.addr == 192.168.1.104 and icmp

编码与进制转换:

平时看到的数据都是十进制,在tcp/socket发出的消息将以十六进制编码,且有两种显示方式“0x00,0x01”和”00 01”(WireShark捕获的是后者)。记得要先把显示十六进制的选择项选中,在数据发送窗口即处于HEX输入模式了, 在里面直接输入HEX格式内容。
连接成功后,从WireShark的捕获可以看到通信中使用的是ascii码。

字节流转换为十六进制的函数

public static String printHexString(byte[] b) {
        String res = "";
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            res += hex;
        }

        return res;
    }

socket通信/数据采集程序
假设现在需要读取保持寄存器的数值,已知设备的地址、寄存器地址和数量,对应上述的tcp报文格式进行询问报文编码。

//创建Socket对象
Socket socket=new Socket("127.0.0.1",502);

//根据输入输出流和服务端连接
OutputStream outputStream=socket.getOutputStream();//获取一个输出流,向服务端发送信息
byte[] sendInfo = new byte[] { 0x00,0x00,0x00,0x00,0x00,0x06,0x01,0x03,0x00, 0x64, 0x00, 0x01 };  //发送给Modbus软件的消息
outputStream.write(sendInfo);
outputStream.flush();

InputStream inputStream=socket.getInputStream();//获取一个输入流,接收服务端的信息
byte[] bs = new byte[32];
inputStream.read(bs);
String res1=printHexString(bs);  //输出十六进制,调用刚刚的转换函数
System.out.println(res1);

WireShark对应的详情:(十六进制与ascii码)
主站询问=client发出的tcp报文
在这里插入图片描述

从站应答=client收到的报文
在这里插入图片描述

在IntelliJ中的逐步调试情况
在这里插入图片描述

其中,要注意的是字节的存放顺序
在这里插入图片描述

大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中(正序);小端模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中(逆序)。
所以,上图中捕获的59139e7 03实际为十进制999和其十六进制03e7的小端存储结果。到此,已成功取出想要的RW数值。


最后记得关闭对应的资源和异常处理(万能的`Exception e`):
//关闭相对应的资源
socket.shutdownOutput();//关闭输出流
inputStream.close();
outputStream.close();
socket.close();
;