【Netty客户端-实现模拟硬件设备在线】

通过Netty服务端达到和硬件设备进行通信,现需要对服务端连接进行压测,来测试出当前服务器上能够达到的最大连接数。通过思考和验证使用了2种方式可进行Socket压测。

  1. 通过netty客户端实现 (需要写大量代码)
  2. 通过jmeter来实现 (需要写通信维持心跳数据生成脚本)

通过Netty客户端实现硬件设备在线模拟

1. 通过netty客户端实现

1.1 实现Netty客户端

@Slf4j
public class NettyClient {
    
    

    private final String inetHost;
    private final Integer inetPort;

    public NettyClient(String inetHost, Integer inetPort) {
    
    
        this.inetHost = inetHost;
        this.inetPort = inetPort;
    }

    public void createClient() {
    
    
        //客户端只需要实现一个线程组即可
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
    
    
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
    
    
                        ChannelPipeline pipeline = ch.pipeline();
                        //添加编码器
                        pipeline.addLast(new NettyClientEncoder());
                        //添加客户端处理器
                        pipeline.addLast(new NettyClientHandler());
                    }
                });
        ChannelFuture channelFuture;
        try {
    
    
            channelFuture = bootstrap.connect(inetHost, inetPort).sync();
            channelFuture.addListener((ChannelFutureListener) channelFuture1 -> {
    
    
                if (channelFuture1.isSuccess()) {
    
    
                    log.info("Successfully connect to remote server, remote peer = " + inetHost + ":" + inetPort);
                } else {
    
    
                    log.error("Can not connect to remote server, remote peer = " + inetHost + ":" + inetPort);
                }
            });
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            group.shutdownGracefully();
        }
    }
}

1.2 实现客户端处理器

public class NettyClientHandler extends SimpleChannelInboundHandler {
    
    

    public NettyClientHandler() {
    
    
    }

    private static final ConcurrentHashMap<String, Channel> client = new ConcurrentHashMap<>();
    /**
     * 模拟硬件网关前缀
     */
    private static final String gwCodePre = "11AA12";

    /**
     * 硬件网关后缀 - 压测时需要生成几万个设备该参数用来递增
     */
    private static Integer gwCodeSuf = 100000;

    public static void setGwCodeSuf(Integer gwCodeSuf) {
    
    
        NettyClientHandler.gwCodeSuf = gwCodeSuf + 1;
    }

    public static ConcurrentHashMap<String, Channel> getClient() {
    
    
        return client;
    }

    /**
     * 连接成功后触发 - 模拟硬件心跳发送给服务端
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        //拼接出硬件设备唯一Code
        String gwCode = gwCodePre + gwCodeSuf;
        MessageEntity message = new MessageEntity(gwCode, "01", "01");
        //24-58-18-00-01-01-11-AA-0B-F2-2F-0B-01-01-01-10-00-00-01-00-00-00-C1-57
        //24-58-30-00-01-01-11-AA-12-10-00-00-01-01-01-10-00-00-01-00-00-00-C4-57
        message.setMsgLength("1800");
        message.setMsgData("0110000001000000");
        channel.writeAndFlush(message);
        client.put(gwCode, channel);
        setGwCodeSuf(gwCodeSuf);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
        super.exceptionCaught(ctx, cause);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        System.out.println("收到服务端消息:" + msg.toString());
    }
}

1.3 实现客户端编码器

public class NettyClientEncoder extends MessageToByteEncoder {
    
    

    @Override
    protected void encode(ChannelHandlerContext ctx, Object o, ByteBuf out) throws Exception {
    
    
        MessageEntity message = (MessageEntity) o;
        // 起始域
        StringBuilder msg = new StringBuilder("2458");
        // 长度域
        if (message.getMsgData() == null) {
    
    
            message.setMsgData("");
        }
        msg.append(message.getMsgLength());
        // 信息域
        msg.append(message.getMsgInformation());
        // 控制域
        msg.append(message.getMsgControl());
        // MAC地址
        msg.append(message.getMsgGwMac());
        // 主功能码
        msg.append(message.getMsgMainCode());
        // 辅功能码
        msg.append(message.getMsgAuxiliaryCode());
        // 数据域
        msg.append(message.getMsgData());
        // 校验 - 服务端用来验证该条数据的正确性
        msg.append(Objects.requireNonNull(DataTypeConvert.getChecksum(msg.substring(0))).toUpperCase());
        // 结束符
        msg.append(LimsProtocolConstant.END);

        log.info("----> 初始数据【{}】 : {}", message.getMsgGwMac(), Decoder.strSplit(msg.toString()));
        //将字符串转换成byte
        byte[] bytes = DataTypeConvert.hexStringToBytes(msg.toString());
        //写入发送
        out.writeBytes(bytes);
    }
}

2. 启动客户端(多线程)

一个客户端代表一个线程,如果需要达到压测目的就需要通过多线程来模拟出多个客户端。

2.1 实现启动线程

public class StartClientThread implements Runnable{
    
    
   /**
    *  netty服务端主机ip
    */
    private String inetHost;
   /**
    *  netty服务端监听的端口
    */ 
    private Integer inetPort;

    public StartClientThread(String inetHost, Integer inetPort) {
    
    
        this.inetHost = inetHost;
        this.inetPort = inetPort;
    }

    @Override
    public void run() {
    
    
        NettyClient client = new NettyClient(inetHost, inetPort);
        client.createClient();
    }
}

2.2 创建线程池,提供对外启动接口

@RestController
@EnableScheduling
@SpringBootApplication
public class ClientApplication {
    
    

    private static final ThreadFactory NAME_THREAD_FACTORY = new ThreadFactoryBuilder().setNameFormat("netty-client-pool-%d").build();

    private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
            0,
            Integer.MAX_VALUE,
            60L,
            TimeUnit.SECONDS,
            new SynchronousQueue<>(),
            NAME_THREAD_FACTORY,
            new ThreadPoolExecutor.AbortPolicy()
    );

    public static ExecutorService getExecutor() {
    
    
        return EXECUTOR;
    }

    public static void newTask(Runnable r) {
    
    
        EXECUTOR.execute(r);
    }

    @GetMapping("/start")
    public Integer startClient(@RequestParam("inetHost") String inetHost, @RequestParam("inetPort") Integer inetPort) {
    
    
        newTask(new StartClientThread(inetHost, inetPort));
        return 1;
    }

    public static void main(String[] args) {
    
    
        SpringApplication.run(ClientApplication.class, args);
    }
}

2.3 实现心跳定时任务

@Slf4j
@Configuration
public class HeartbeatTask {
    
    

    /**
     *  心跳定时任务,用来定时发送心跳给服务端。从而达到在线状态
     */
    @Scheduled(cron = "0/30 * * * * ? ")
    public void task() {
    
    
        ConcurrentHashMap<String, Channel> client = NettyClientHandler.getClient();
        Set<Map.Entry<String, Channel>> entrySet = client.entrySet();
        for (Map.Entry<String, Channel> entry : entrySet) {
    
    
            String gwCode = entry.getKey();
            Channel channel = entry.getValue();
            log.info("网关:{} 发送心跳", gwCode);
            MessageEntity message = new MessageEntity(gwCode, "01", "01");
            message.setMsgLength("1800");
            message.setMsgData("0110000001000000");
            channel.writeAndFlush(message);
        }
    }
}

3. 验证

3.1 启动netty服务端
在这里插入图片描述
3.2 启动Netty客户端-使用Postman进行测试
在这里插入图片描述
3.3 查看服务端是否收到心跳
在这里插入图片描述
3.4 查看netty客户端定时心跳是否发送
在这里插入图片描述

4. 总结

通过netty客户端来简单模拟硬件设备心跳,要想实现几万甚至十几万设备的模拟,那么就会开启相对应的线程数,对测试的机器有一定的硬件条件。而且无法生成像jmeter一样的聚合报告。且需要人工查看和计算出服务器压测指标。

使用jmeter对socket进行压测

通过jmeterc测试工具来进行socket压测,相比较上述方法会非常简洁。只需要实现socket要发送的文件即可。

1. 下载jmeter测试工具

下载Jmeter,官网地址:https://jmeter.apache.org/download_jmeter.cgi
具体安装步骤和使用方法这里就不太多阐述。

1.1 进入安装包bin目录,对jmeter.properties进行配置文件修改
在这里插入图片描述
1.2 修改配置文件后保存退出
在这里插入图片描述

2. 编写心跳数据生成脚本

public class Script {
    
    

    public static void main(String[] args) {
    
    
        Scanner scanner = new Scanner(System.in);
        int count = scanner.nextInt();
        int fixe = 100000;
        FileOutputStream fos = null;
        try {
    
    
            fos = new FileOutputStream(new File("D:/data.txt"));
            for (int i = 1; i <= count; i++) {
    
    
                // 起始域
                StringBuilder msg = new StringBuilder("485918000101");
                //网关code
                msg.append("11AA0B");
                msg.append(fixe + i);
                //心跳常量
                msg.append("01010110000001000000");
                // 校验
                msg.append(Objects.requireNonNull(getChecksum(msg.toString().toUpperCase())).toUpperCase());
                // 结束符
                msg.append("57");
                byte[] bytes = msg.toString().getBytes(StandardCharsets.UTF_8);
                fos.write(bytes);
                fos.write("\r\n".getBytes());
                fos.flush();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                assert fos != null;
                fos.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }

    }

    public static String getChecksum(String data) {
    
    
        int len = data.length();
        if ((len % 2) != 0) {
    
    
            return null;
        }
        len = len / 2;
        int checksum = 0;
        for (int iLen = 0; iLen < len; iLen++) {
    
    
            String dataTmp = data.substring(iLen * 2, (iLen + 1) * 2);
            checksum += Integer.parseInt(dataTmp, 16);
            checksum = checksum % 256;
        }
        return int2Hex(checksum, 1);
    }

    public static String int2Hex(int intData, int byteSize) {
    
    
        String hexString = Integer.toHexString(intData);
        String rs = "";
        int pos;
        for (pos = hexString.length(); pos >= 2; pos -= 2) {
    
    
            rs += hexString.substring(pos - 2, pos);
        }
        if (pos == 1) {
    
    
            rs += "0";
            rs += hexString.substring(0, pos);
        }
        int dataSize = rs.length();
        byteSize *= 2;
        while (dataSize < byteSize) {
    
    
            rs += "00";
            dataSize += 2;
        }
        return rs.toUpperCase();
    }
}

执行脚本后,会在D盘生成data.txt测试数据包
在这里插入图片描述

3. 启动jmeter进行socket压测

3.1 进入jmeter测试工具后,添加线程组
在这里插入图片描述
3.2 第二步,添加TCP取样器
在这里插入图片描述
3.3 第三步,添加CSV数据文件设置

这里是用来读取我们上面脚本生成的心跳数据。从而实现动态数据加载

在这里插入图片描述
3.4 第四步,编辑CSV
在这里插入图片描述
3.5 第五步,编辑TCP取样器

这里${heartbeat} 就是我们上面配置的CSV中的变量名称。

在这里插入图片描述

4. 验证

启动后通过结果数可以看到10个线程(10个客户端)压测成功。
在这里插入图片描述
服务端也正常接收到心跳数据。
在这里插入图片描述

4. 总结

相比第一种方法,使用jmeter来进行压测要简单很多,但使用jmeter来压测socket无法维持心跳,jmeter线程只能不断循环发送这种方式来维持。

猜你喜欢

转载自blog.csdn.net/GBK_8/article/details/128011300