Java uses Netty to implement Modbus-RTU communication protocol

Modbus

Modbus is a serial communication protocol. Modbus is a commonly used communication protocol and a communication convention in the industry. Modbus protocol includes RTU, ASCII, TCP. Among them, MODBUS-RTU is the most commonly used, relatively simple, and easy to implement on a single-chip microcomputer.

Simple analysis of Modbus-RTU messages

37 10 00 14 00 0a 14 00 00 00 00 00 00 00 00 00 00 00 00 3f 80 00 00 3f 80 00 00 00 a0 (hexadecimal) 37: slave address, 10: function code, 00 14:
MODBUS The starting address is 40021, corresponding to 20, 14: the number of written data bytes, 20, 00 a0: crc check code. The rest is the transmitted data.

37 10 00 14 00 0a 14 ... 00 a0, the data in the middle is functional data, and the above message is spliced ​​into a real number according to two bytes, which is corresponding to the basic data type of float, among which 00 00 00 00 is a real number, followed by By analogy, the above message has 5 real numbers. Parsed out is 0.0, 0.0, 0.0, 1.0, 1.0.

4G DTU(ZHC4013)

ZHC4012 is a full Netcom seven-mode 4G DTU that supports transparent transmission of 2G/3G/4G signals. Support industrial RS232/485 and other interfaces, directly connected to equipment for transmission. This hardware is practiced in my project, and the device can communicate data with a remote server through the 4G operator network. For specific operations, please contact customer service on the official website. Device official website 4G DTU (ZHC4013)

The project supports data upload of multiple 4G DTU devices, and supports control of specified 4G DTU devices.

Advantages of Netty

Netty is based on epoll's NIO threading model. The entire call process of NIO is that Java calls the kernel function of the operating system to create a Socket, obtains the file descriptor of the Socket, creates a Selector object, corresponds to the Epoll descriptor of the operating system, and obtains the event of the file descriptor of the Socket connection. Bind the Epoll file descriptor corresponding to the Selector to perform asynchronous notification of events, so that the use of one thread is realized, and there is no need for many invalid traversals, and the event processing is handed over to the operating system kernel (implemented by the operating system interrupt program) , greatly improving the efficiency.

2021年分享过用原生Socket技术实现的服务端代码,其中服务端还是存在一些问题。当时项目运行在生产上,经过几个月的运行,发现服务端监听端口线程不稳定,运行一段时间后,新的客户端就连接不上服务端了。

Data parsing related code In the blog shared below, Netty related code is the technical implementation.

Socket implementation code: Java uses socket communication to implement Modbus-RTU communication protocol

import dependencies

<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.72.Final</version>
</dependency>

Netty server code

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

/**
 * TODO
 *
 * @author linfeng
 * @date 2022/12/26 15:57
 */
public class DtuServer implements Runnable {
    
    

    public static void main(String[] args) {
    
    
        new Thread(new DtuServer()).start();
    }

    @Override
    public void run() {
    
    
        EventLoopGroup bossGroup = new NioEventLoopGroup(2);
        EventLoopGroup workGroup = new NioEventLoopGroup(10);
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup,workGroup).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
    
    

                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
    
    
                        ch.pipeline().addLast(new IdleStateHandler(15, 0, 0, TimeUnit.MINUTES));
                        ch.pipeline().addLast(new DtuServiceHandler());
                    }

                });
        try {
    
    
            ChannelFuture channelFuture = serverBootstrap.bind(9005).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

import com.ruoyi.socket.ModBusUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * TODO
 *
 * @author linfeng
 * @date 2022/12/26 15:58
 */
public class DtuServiceHandler extends ChannelInboundHandlerAdapter {
    
    

	// 保存
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

	// 保存客户端channelId与注册包信息
    private static Map<String,String> channelIdMap = new ConcurrentHashMap<>();

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        Channel channel = ctx.channel();
        ChannelId channelId = channel.id();
        ByteBuf byteBuf = (ByteBuf) msg;
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String str = ModBusUtils.bytes2HexString(bytes);
        System.out.println(bytes.length);
        if(bytes.length == 16){
    
    
        	// 注册包长度为16(我这边用到4G DTU(ZHC4013)注册包长度为16,如果有变化需要修改)
        	// 注册包为设备编号,设备编号是唯一的。
            String registerPackage = "";
            for(int i=0;i<bytes.length;i++) {
    
    
                registerPackage=registerPackage+ModBusUtils.byteToASCLL(bytes[i]);
            }
            // channelId.asLongText()获取的是客户端全局唯一ID,根据channelId获取注册包信息,可以判断出数据是哪个节点上传的
            channelIdMap.put(channelId.asLongText(),registerPackage);
            System.out.println("注册包:"+registerPackage);
        }else {
    
    
        	// 从channelIdMap获取注册包(设备编号)
        	String registerPackage = channelIdMap.get(channelId.asLongText());
        	// 获取到客户端注册包,可以根据注册包(设备编号)查询数据库唯一的设备信息。
        	// 数据就不做解析了,数据解析相关代码在上面分享的博客地址中。
        	// 后面就是业务逻辑和数据解析了。
        	/** 如果要服务端向客户端发送信息,可以利用客户端心跳机制发送数据(也可以写发送数据的线程),其中具体业务逻辑就需要利用数据库了。
        		具体思想:首先数据库保存发送状态和发送数据,利用channelIdMap查询注册包,可以通过注册包在数据库中查询相关数据,
        		根据发送状态判断是否需要发送数据。
        		Netty发送数据需要转ByteBuf,具体代码:channel.writeAndFlush(Unpooled.copiedBuffer(new byte[]{1, 2, 3})),
			*/ 
		}
        
        System.out.println("收到的数据:"+str);
        System.out.println("链接数:"+channelGroup.size());
        System.out.println("注册包数:"+channelIdMap.size());
        super.channelRead(ctx, msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        ChannelId channelId = channel.id();
        channelIdMap.remove(channelId.asLongText());
        System.out.println(channelId.asLongText()+" channelInactive客户端关闭连接");
        super.channelInactive(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    
    
        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    
    
        // 连接建立
        Channel channel = ctx.channel();
        channelGroup.add(channel);
        super.handlerAdded(ctx);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        ChannelId channelId = channel.id();
        channelIdMap.remove(channelId.asLongText());
        System.out.println(channelId.asLongText()+" ChannelHandlerContext客户端关闭连接");
        super.handlerRemoved(ctx);
    }
}
package com.ruoyi.socket;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * TODO
 *
 * @author linfeng
 * @date 2022/12/8 16:04
 */
public class ModBusUtils {
    
    

    public static char byteToASCLL(byte b){
    
    
        return (char) b;
    }


    /*
     * 字节数组转16进制字符串
     */
    public static String bytes2HexString(byte[] b) {
    
    
        String r = "";
        for (int i = 0; i < b.length; i++) {
    
    
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
    
    
                hex = '0' + hex;
            }
            r += hex.toUpperCase()+" ";
        }
        return r;
    }

Guess you like

Origin blog.csdn.net/qq_40042416/article/details/128457288