Java は Netty を使用して Modbus-RTU 通信プロトコルを実装します

Modbus

Modbus はシリアル通信プロトコルです。Modbus は、業界で一般的に使用されている通信プロトコルであり、通信規約です。Modbus プロトコルには、RTU、ASCII、TCP が含まれます。その中でもMODBUS-RTUは最もよく使われており、比較的シンプルでシングルチップマイコンへの実装も容易です。

Modbus-RTUメッセージの簡易分析

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(16進数) 37:スレーブ局アドレス、10:ファンクションコード、00 14:MODBUSの先頭
アドレスは 40021 で、20、14: 書き込まれたデータ バイト数、20、00 a0: crc チェック コードに対応します。残りは送信されたデータです。

37 10 00 14 00 0a 14 … 00 a0、中央のデータは関数データであり、上記のメッセージは、float の基本データ型に対応する 2 バイトに従って実数に結合されます。 00 00 00 00 は実数で、その後に が続きます。類推すると、上記のメッセージには 5 つの実数が含まれています。解析された値は 0.0、0.0、0.0、1.0、1.0 です。

4G DTU(ZHC4013)

ZHC4012 は、2G/3G/4G 信号の透過的な伝送をサポートする完全な Netcom 7 モード 4G DTU です。産業用 RS232/485 およびその他のインターフェイスをサポートし、送信用に機器に直接接続します。このハードウェアは私のプロジェクトで実践されており、デバイスは 4G オペレータ ネットワークを通じてリモート サーバーとデータを通信できます。具体的な操作については、公式サイトのカスタマーサービスまでお問い合わせください。デバイスの公式 Web サイト 4G DTU (ZHC4013)

このプロジェクトは、複数の 4G DTU デバイスのデータ アップロードをサポートし、指定された 4G DTU デバイスの制御をサポートします。

Nettyの利点

Netty は、epoll の NIO スレッド モデルに基づいています。NIO の呼び出しプロセス全体は、Java がオペレーティング システムのカーネル関数を呼び出してソケットを作成し、ソケットのファイル記述子を取得し、セレクター オブジェクトを作成し、オペレーティング システムの Epoll 記述子に対応して、次のイベントを取得します。 Socket接続のファイル記述子セレクタに対応するEpollファイル記述子をバインドしてイベントの非同期通知を行うことで、1スレッドの使用を実現し、多くの無効なトラバーサルを必要とせずにイベント処理を任せますオペレーティング システムのカーネル (オペレーティング システムの割り込みプログラムによって実装) に渡され、効率が大幅に向上します。

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

データ解析関連のコード 以下で共有されているブログでは、Netty 関連のコードが技術的な実装です。

ソケット実装コード: Java はソケット通信を使用して Modbus-RTU 通信プロトコルを実装します。

依存関係をインポートする

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

Nettyサーバーコード

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;
    }

おすすめ

転載: blog.csdn.net/qq_40042416/article/details/128457288