Helvar灯TCP协议模拟器代码,netty自定义协议拆包沾包解析

helvar国外的一家灯设备公司,协议是tcp,格式是他们自己定义的

#发送的消息,格式是>开头,#结尾。命令大概意思是C14修改灯光亮度L亮度100F过度时间1秒@后面是设备编号
>V:2,C:14,L:100,F:100,@0.50.1.63.1#
#返回消息 ?号正常、!号异常,
?V:2,C:14,L:100,F:100,@0.50.1.63.1#
!V:2,C:14,L:100,F:100,@0.50.1.63.1#

服务端客服端沾包拆包有些类似都是以*V开头#结尾进行的

看代码,这是需要的pom

        <!--工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>5.0.0.Alpha2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            <!--            <version>1.18.28</version>-->
        </dependency>

模拟器tcpserver

package com.example.haverserver;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author lanyanhua
 * @date 2023/5/25 10:14 AM
 * @description
 */
@Slf4j
@Component
public class TcpServer implements ApplicationRunner {

    public static void main(String[] args) throws InterruptedException {
        new TcpServer().start();
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        new Thread(() -> {
            try {
                start();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }

    /**
     * 启动tcp服务
     */
    public void start() throws InterruptedException {
        // 创建两个事件循环组,一个用于接收客户端的连接,另一个用于处理I/O操作
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 添加处理器
                            ch.pipeline()
                                    .addLast(new StringEncoder()
                                            , new MyStringDecoder()
                                            , new NettyServerHandler());
//                                    .addLast(new LineBasedFrameDecoder(1024, true, true))
//                                    .addLast(new NettyServerHandler());
                        }
                    });

            // 监听并启动服务器
            int port = 8888;
            ChannelFuture future = bootstrap.bind(port).sync();
            // 阻塞等待服务器关闭
            future.channel().closeFuture().sync();
        } finally {
            // 关闭事件循环组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

@Slf4j
class NettyServerHandler extends SimpleChannelInboundHandler<String> {
    public static Map<String, Data> dataMap = new ConcurrentHashMap<>(100);

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("Connected to server: " + ctx.channel().remoteAddress());
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.out.println("Failed to handle request: " + cause.getMessage());
        ctx.close();
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println("拆包后Message received: " + msg);
        //推送mq执行宏
        try {
            if (msg.contains("C:100@")) {
                ctx.writeAndFlush(msg.replace(">V", "?V").replace("#", "=1,2,3,4,5#"));
                return;
            }
            String json = msg.replace(">", "{").replace("#", "}").replace("@", "DC:");
            JSONObject obj = JSONUtil.parseObj(json);
            String v = "";
            //亮度、色温、颜色
            String l = obj.getStr("L");
            String m = obj.getStr("K");
            String cx = obj.getStr("CX");
            String cy = obj.getStr("CY");
            //设备编号
            String deviceCode = obj.getStr("DC");
            Data data;
            switch (obj.getInt("C")) {
                //调试场景
                case 11:
                case 12:
                    //块、场景
                    Integer b = obj.getInt("B");
                    Integer s = obj.getInt("S");
                    data = dataMap.get(b + "_" + s);
                    if (data == null) {
                        dataMap.put(deviceCode, new Data(l,
                                (!StringUtils.hasLength(m) && !StringUtils.hasText(cx) ? "2700" : m)
                                , cx, cy));
                        return;
                    }
                    data.set(l, m, cx, cy);
                    return;
                //控制亮度、色温、颜色
                case 14:
                    data = dataMap.get(deviceCode);
                    if (data == null) {
                        dataMap.put(deviceCode, new Data(l,
                                (!StringUtils.hasLength(m) && !StringUtils.hasText(cx) ? "2700" : m)
                                , cx, cy));
                        return;
                    }
                    data.set(l, m, cx, cy);
                    return;
                //状态
                case 110:
                    v = "0";
                    break;
                //获取亮度
                case 152:
                    data = dataMap.get(deviceCode);
                    if (data == null) {
                        dataMap.put(deviceCode, new Data("0", "2700", null, null));
                        v = "0";
                    } else {
                        v = data.l == null ? "0" : data.l;
                    }
                    break;
                //获取色温、颜色
                case 157:
                    //?V:2,C:157,@0.213.1.1=M:270#
                    //?V:2,C:157,@0.213.1.2=CX:0.209991,CY:0.679993#
                    data = dataMap.get(deviceCode);
                    System.out.println(deviceCode + ":" + data);
                    if (data == null) {
                        data = new Data("0", "2700", null, null);
                        dataMap.put(deviceCode, data);
                    }
                    System.out.println(deviceCode + ":" + data);
                    v = data.m == null ? ("CX:" + data.cx + ",CY:" + data.cy) : "M:" + data.m;
                    break;
                //修改场景
                case 202:
                default:
                    return;
            }
            String send = msg.replace(">V", "?V").replace("#", "=" + v + "#");
            System.out.println("回复:" + send);
            ctx.writeAndFlush(send);

        } catch (Exception e) {
            log.error("解析失败:" + e);
        }
    }
}

@ToString
class Data {
    public String l;
    public String m;
    public String cx;
    public String cy;

    public Data(String l, String m, String cx, String cy) {
        this.l = l;
        setM(m);
        this.cx = cx;
        this.cy = cy;
    }

    public void set(String l, String m, String cx, String cy) {
        if (l != null) {
            this.l = l;
        }
        if (m != null) {
            setM(m);
            this.cx = null;
            this.cy = null;
        }

        if (cx != null) {
            this.cx = cx;
            this.m = null;
        }
        if (cy != null) {
            this.cy = cy;
            this.m = null;
        }
    }

    public void setM(String m) {
        if (m != null) {
            this.m = String.valueOf(1000000 / Integer.parseInt(m));
        }
    }
}


class MyStringDecoder extends StringDecoder {
    private final Charset charset = StandardCharsets.UTF_8;
    // 用来临时保留没有处理过的请求报文
    ByteBuf tempMsg = Unpooled.buffer();

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("Connected to server: " + ctx.channel().remoteAddress());
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.out.println("Failed to handle request: " + cause.getMessage());
        ctx.close();
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf message, List<Object> out) {
        System.out.println("decode  Message received: " + message.toString(charset));
        //推送mq执行宏

//        ctx.writeAndFlush("test");

        // 合并报文
        ByteBuf in;
        int tmpMsgSize = tempMsg.readableBytes();
        // 如果暂存有上一次余下的请求报文,则合并
        if (tmpMsgSize > 0) {
            in = Unpooled.buffer();
            in.writeBytes(tempMsg);
            in.writeBytes(message);
            System.out.println("合并:上一数据包余下的长度为:" + tmpMsgSize + ",合并后长度为:" + in.readableBytes());
        } else {
            in = message;
        }

        try {
            int i = in.readableBytes();
            String string = in.toString(this.charset);
            System.out.println("开始处理沾包拆包:::" + string);
            if (i < 4) {
                return;
            }
            while (true) {
                System.out.println(1);
                int length = ">V".length();
                //判断是否有开始的命令
                int startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
                        Unpooled.wrappedBuffer(">V'".getBytes(charset)).readByte());
                if (startIdx < 0 && (startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
                        Unpooled.wrappedBuffer("!V'".getBytes(charset)).readByte())) < 0) {
                    return;
                }
                //没有结尾消息值 结束
                if (in.indexOf(startIdx + length, in.writerIndex(),
                        Unpooled.wrappedBuffer("#".getBytes(charset)).readByte()) < 0) {
                    //设置为下一条消息的开始位置
                    // 多余的报文存起来
                    // 第一个报文: i+  暂存
                    // 第二个报文: 1 与第一次
                    int size = in.readableBytes();
                    if (size != 0) {
                        System.out.println("多余的数据长度:" + size);
                        // 剩下来的数据放到tempMsg暂存
                        tempMsg.clear();
                        tempMsg.writeBytes(in.readBytes(size));
                    }
                    return;
                }

                //将下一个开始的命令作为截取的索引。
                int endIdx = in.indexOf(startIdx + length, in.writerIndex(),
                        Unpooled.wrappedBuffer(">V".getBytes(charset)).readByte());
                if (endIdx == -1 && (endIdx = in.indexOf(startIdx + length, in.writerIndex(),
                        Unpooled.wrappedBuffer("!V".getBytes(charset)).readByte())) == -1) {
                    //没有时可以直接解码当前所有
                    super.decode(ctx, in, out);
                    return;
                }
                in.readerIndex(startIdx);
                byte[] bytes = new byte[endIdx - startIdx];
                in.readBytes(bytes);
                //设置为下一条消息的开始位置
                in.skipBytes(endIdx - in.readerIndex());
                //解码字符串添加到了 out 列表中
                String s = new String(bytes, charset);
                out.add(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

下面是客户端调用

package com.example.haverserver;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

/**
 * @author lanyanhua
 * @date 2023/5/18 10:12 AM
 * @description
 */
public class HelvarNetProtocol {

    private static final Map<String, Consumer<String>> messageMap = new ConcurrentHashMap<>();
    private final Channel channel;
    private final EventLoopGroup group;
    public boolean singleton = false;

    public HelvarNetProtocol(String addr, int port, int timeout) throws Exception {
        try {
            group = new NioEventLoopGroup();
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_TIMEOUT, timeout)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
//                        ByteBuf[] delimiters = {Unpooled.wrappedBuffer("#".getBytes())
//                                , Unpooled.wrappedBuffer("!".getBytes())
//                                , Unpooled.wrappedBuffer("?".getBytes())
////                                , Unpooled.wrappedBuffer("#!".getBytes())
////                                , Unpooled.wrappedBuffer("#?".getBytes())
//                        };
                            ch.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8),
//                                new StringDecoder(),
                                    new MyStringDecoder1(StandardCharsets.UTF_8),
                                    new ClientHandler())
                            ;
//                                .addLast("frameDecoder", new DelimiterBasedFrameDecoder(8192
//                                        , delimiters));

                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(addr, port).sync();
            channel = channelFuture.channel();

        } catch (Exception e) {
            singleton = false;
            close1();
            throw new Exception("连接创建失败:" + e.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        HelvarNetProtocol localhost = new HelvarNetProtocol("192.168.0.181", 8888, 500);
//        HelvarNetProtocol localhost = new HelvarNetProtocol("localhost", 8888, 50000);
        localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
        localhost.sendCommand(">V:1,C:110#");
        localhost.sendCommand(">V:1,C:104#");
        Scanner scanner = new Scanner(System.in);
        new Thread(() -> {
            localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
            for (int i = 0; i < 100; i++) {

                localhost.sendCommand(">V:1,ID:1704410508190887938,C:152,@0.181.1.1#");
            }
            localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
        }).start();
        for (int i = 0; i < 100; i++) {

            localhost.sendCommand(">V:1,ID:1704410508190887938,C:152,@0.181.1.1#");
        }
        localhost.sendCommand(">V:2,C:14,L:100,F:100,@0.50.1.63.1#");
        while (scanner.hasNext()) {
            String ipt = scanner.next();

            localhost.sendCommand(ipt);

        }
    }

    public void sendCommand(String command) {
        System.out.println("Message sent: " + command);
        // 使用发送请求
        channel.writeAndFlush(command);
    }


    public void close1() {
        if (singleton) {
            return;
        }
        //不用关闭
        if (channel != null) {
            channel.close().syncUninterruptibly(); // 关闭Channel对象
        }
        if (group != null) {
            group.shutdownGracefully(); // 关闭EventLoopGroup对象
        }
    }

    public String name() {
        return "tcp";
    }

    public static class ClientHandler extends SimpleChannelInboundHandler<String> {


        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            System.out.println("Connected to server: " + ctx.channel().remoteAddress());
        }


        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            System.out.println("Failed to handle request: " + cause.getMessage());
            ctx.close();
        }

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("Message received: " + msg);

        }
    }

}


class MyStringDecoder1 extends StringDecoder {

    private final Charset charset;
    // 用来临时保留没有处理过的请求报文
    ByteBuf tempMsg = Unpooled.buffer();

    public MyStringDecoder1(Charset charset) {
        this.charset = charset;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf message, List<Object> out) {
        // 合并报文
        ByteBuf in;
        int tmpMsgSize = tempMsg.readableBytes();
        // 如果暂存有上一次余下的请求报文,则合并
        if (tmpMsgSize > 0) {
            in = Unpooled.buffer();
            in.writeBytes(tempMsg);
            in.writeBytes(message);
            System.out.println("合并:上一数据包余下的长度为:" + tmpMsgSize + ",合并后长度为:" + in.readableBytes());
        } else {
            in = message;
        }

        try {
            int i = in.readableBytes();
            String string = in.toString(this.charset);
            System.out.println("开始处理沾包拆包:::" + string);
            if (i < 4) {
                return;
            }
            while (true) {
                int length = "?V".length();
                //判断是否有开始的命令
                int startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
                        Unpooled.wrappedBuffer("?V'".getBytes(charset)).readByte());
                if (startIdx < 0 && (startIdx = in.indexOf(in.readerIndex(), in.writerIndex(),
                        Unpooled.wrappedBuffer("!V'".getBytes(charset)).readByte())) < 0) {
                    return;
                }
                //没有结尾消息值 结束
                if (in.indexOf(startIdx + length, in.writerIndex(),
                        Unpooled.wrappedBuffer("#".getBytes(charset)).readByte()) < 0) {
                    //设置为下一条消息的开始位置
                    // 多余的报文存起来
                    // 第一个报文: i+  暂存
                    // 第二个报文: 1 与第一次
                    int size = in.readableBytes();
                    if (size != 0) {
                        System.out.println("多余的数据长度:" + size);
                        // 剩下来的数据放到tempMsg暂存
                        tempMsg.clear();
                        tempMsg.writeBytes(in.readBytes(size));
                    }
                    return;
                }

                //将下一个开始的命令作为截取的索引。
                int endIdx = in.indexOf(startIdx + length, in.writerIndex(),
                        Unpooled.wrappedBuffer("?V".getBytes(charset)).readByte());
                if (endIdx == -1 && (endIdx = in.indexOf(startIdx + length, in.writerIndex(),
                        Unpooled.wrappedBuffer("!V".getBytes(charset)).readByte())) == -1) {
                    //没有时可以直接解码当前所有
                    super.decode(ctx, in, out);
                    return;
                }
                in.readerIndex(startIdx);
                byte[] bytes = new byte[endIdx - startIdx];
                in.readBytes(bytes);
                //设置为下一条消息的开始位置
                in.skipBytes(endIdx - in.readerIndex());
                //解码字符串添加到了 out 列表中
                String s = new String(bytes, charset);
                out.add(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

踩过的坑:

1.线程未关闭导致内存异常

客户端HelvarNetProtocol在创建时netty是在构造函数中进行创建的由于连接失败异常进入catch模块,但是EventLoopGroup group对象是已经初始化过了的没有关闭group线程,实现AutoCloseable接口的close方法是在外面try(HelvarNetProtocol protocol = new HelvarNetProtocol())的方式实现,实实在在漏掉了异常的关闭线程情况。

2.客户端解析返回的消息代码抛出异常导致当前客户端对象卡死

ClientHandler.messageReceived是服务端返回的消息中,通常我们都需要在这里进行下一步操作,解析参数下发指令等等,当这里出现异常时这个客户端就会卡死,这里估计是我代码有问题还没研究透。估计是exceptionCaught方法中ctx.close();关闭了通道。最好时再messageReceived中做好异常处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值