Netty+SpringBoot实战用例

前言

公司需要与硬件对接,需要接收硬件传过来的消息数据。根据这个前提,技术选型为Netty。软件作为服务端,硬件作为客户端,双方进行通讯。

代码

导包

<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.68.Final</version>
</dependency>
在pom文件中加上Netty的依赖

搭建Netty服务端

  1. 创建服务端初始化类

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;


/**
 * @program: lxy
 * @author: lxy
 * @create: 2024-03-22 15:04
 * @description: 服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
 **/

public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {

        //自定义的进出站编码器
        channel.pipeline().addLast("decoder",new TimeDecoder());
        channel.pipeline().addLast("encoder",new TimeDecoder());
        //自带的进出站String编码器
        channel.pipeline().addLast("decoder",new StringDecoder());
        channel.pipeline().addLast("encoder",new StringEncoder());
        channel.pipeline().addLast(new NettyServerHandler());
    }
}

 2. 自定义进出站编码器(因为此项目接受的16进制数据,所以需要自定义进出站编码器;如果没有特殊要求,可以直接使用自带的String编码器)下面代码中的RFIDTO类是根据业务自定义的

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        // 定义一个缓冲区用于存储数据

        ByteBuf buffer = ctx.alloc().buffer();

        try {
            // 定义每次读取的最大字节数
            int maxChunkSize = 1024; // 假设每次最多读取1024字节

            while (in.isReadable()) {
                // 计算本次读取的字节数
                int readableBytes = Math.min(in.readableBytes(), maxChunkSize);

                // 从输入缓冲区中读取数据到临时缓冲区中
                ByteBuf tempBuffer = in.readSlice(readableBytes).retain();

                try {
                    // 将临时缓冲区中的数据写入到总缓冲区中
                    buffer.writeBytes(tempBuffer);
                } finally {
                    // 释放临时缓冲区
                    tempBuffer.release();
                }
            }

            // 进行数据处理
            RFIDTO rfidto = processReadData(buffer, out);
            out.add(rfidto);
        } finally {
            // 释放总缓冲区
            buffer.release();
        }
//        // 将ByteBuf转换为字节数组
//        System.out.println(in.readableBytes());



    }


    // 将十六进制字节数组转换为字符串
    public static String hexBytesToString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X", b));
        }
        return sb.toString();
    }

    public static String hexBytesToString(String hexString) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < hexString.length(); i += 2) {
            String hex = hexString.substring(i, i + 2);
            sb.append((char)Integer.parseInt(hex, 16));
        }
        return sb.toString();
    }

    public static RFIDTO processReadData (ByteBuf in,List<Object> out){
        int i = in.readableBytes();

        System.out.println("接收的字节数:"+i);
        byte[] byteArray = new byte[in.readableBytes()];
        in.readBytes(byteArray);
        int len=byteArray[0] & 0XFF;
        System.out.println("len:"+len);
        String adr=String.format("%02X",byteArray[1]);
        System.out.println("adr:"+adr);
        String reCmd=String.format("%02X",byteArray[2]);
        System.out.println("reCmd:"+reCmd);
        String status=String.format("%02X",byteArray[3]);
        System.out.println("status:"+status);
        String ant=String.format("%02X",byteArray[4]);
        System.out.println("Data-ant:"+ant);
        int dateLen=byteArray[5] & 0XFF;
        System.out.println("Data-len:"+dateLen);
        int startIndex = 6; // 起始下标
        int length = len-8; // 要拷贝的元素数量

        byte[] newArray = new byte[length]; // 新数组

        // 使用 System.arraycopy() 方法进行拷贝
        System.arraycopy(byteArray, startIndex, newArray, 0, length);


        String dataString = hexBytesToString(newArray);
        String string = hexBytesToString(dataString);
        System.out.println(string);
        System.out.println("Data-data:"+dataString);



        String rssi=String.format("%02X",byteArray[len-2]);
        System.out.println("Data-rssi:"+rssi);




        String lsb=String.format("%02X",byteArray[len-1]);
        System.out.println("LSB:"+lsb);

        String msb=String.format("%02X",byteArray[len]);
        System.out.println("MSB:"+msb);

        RFIDTO rfidto = new RFIDTO();
        rfidto.setLen(len);
        rfidto.setAdr(adr);
        rfidto.setReCmd(reCmd);
        rfidto.setStatus(status);
        rfidto.setAnt(ant);
        rfidto.setDateLen(dateLen);
        rfidto.setDataString(dataString);
        rfidto.setRssi(rssi);
        rfidto.setLsb(lsb);
        rfidto.setMsb(msb);

        return rfidto;
    }

3. 创建Netty服务端启动类

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

/**
 * @program: ksf
 * @author: lxy
 * @create: 2024-03-22 15:04
 * @description: netty服务启动类
 **/
 
@Slf4j
@Component
public class NettyServer {

    public void start(InetSocketAddress address) {
        //配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)  // 绑定线程池
                    .channel(NioServerSocketChannel.class)
                    .localAddress(address)
                    .childHandler(new NettyServerChannelInitializer())//编码解码
                    .option(ChannelOption.SO_BACKLOG, 128)  //服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
                    .childOption(ChannelOption.SO_KEEPALIVE, true);  //保持长连接,2小时无数据激活心跳机制

            // 绑定端口,开始接收进来的连接
            ChannelFuture future = bootstrap.bind(address).sync();
            log.info("netty服务器开始监听端口:" + address.getPort());
            //关闭channel和块,直到它被关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

 4.创建服务端处理类

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @program: ksf
 * @author: lxy
 * @create: 2024-03-22 15:04
 * @description: netty服务端处理类
 **/

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 管理一个全局map,保存连接进服务端的通道数量
     */
    private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();

    /**
     * @param ctx
     * @author lxy
     * @DESCRIPTION: 有客户端连接服务器会触发此函数
     * @return: void
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {

        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();
        int clientPort = insocket.getPort();

        //获取连接通道唯一标识
        ChannelId channelId = ctx.channel().id();

        System.out.println();
        //如果map中不包含此连接,就保存连接
        if (CHANNEL_MAP.containsKey(channelId)) {
            log.info("客户端【" + channelId + "】是连接状态,连接通道数量: " + CHANNEL_MAP.size());
        } else {
            //保存连接
            CHANNEL_MAP.put(channelId, ctx);

            log.info("客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
    }

    /**
     * @param ctx
     * @author lxy
     * @DESCRIPTION: 有客户端终止连接服务器会触发此函数
     * @return: void
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {

        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();

        ChannelId channelId = ctx.channel().id();

        //包含此客户端才去删除
        if (CHANNEL_MAP.containsKey(channelId)) {
            //删除连接
            CHANNEL_MAP.remove(channelId);

            System.out.println();
            log.info("客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
    }

    /**
     * @param ctx
     * @author lxy
     * @DESCRIPTION: 有客户端发消息会触发此函数
     * @return: void
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        log.info("加载客户端报文......");

        if (msg==null){
            log.info("无效信息");
            return;
        }



        RFIDTO rfidto=(RFIDTO) msg;

        log.info("【" + ctx.channel().id() + "】" + " :" + rfidto.toString());

        /**
         *  下面可以解析数据,保存数据,生成返回报文,将需要返回报文写入write函数
         *
         */



        //响应客户端

        this.channelWrite(ctx.channel().id(), "Hello Netty Client!");
    }

    /**
     * @param msg        需要发送的消息内容
     * @param channelId 连接通道唯一id
     * @author lxy
     * @DESCRIPTION: 服务端给客户端发送消息
     * @return: void
     */
    public void channelWrite(ChannelId channelId, Object msg) throws Exception {

        ChannelHandlerContext ctx = CHANNEL_MAP.get(channelId);

        if (ctx == null) {
            log.info("通道【" + channelId + "】不存在");
            return;
        }

        if (msg == null || msg == "") {
            log.info("服务端响应空的消息");
            return;
        }

        //将客户端的信息直接返回写入ctx
        ctx.write(msg);
        //刷新缓存区
        ctx.flush();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        String socketString = ctx.channel().remoteAddress().toString();

        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("Client: " + socketString + " READER_IDLE 读超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("Client: " + socketString + " WRITER_IDLE 写超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Client: " + socketString + " ALL_IDLE 总超时");
                ctx.disconnect();
            }
        }
    }

    /**
     * @param ctx
     * @author lxy
     * @DESCRIPTION: 发生异常会触发此函数
     * @return: void
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        System.out.println();
        ctx.close();
        log.info(ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + CHANNEL_MAP.size());
        cause.printStackTrace();
    }
}

 以上就是服务端的整个代码,但是我们通常需要服务端与整个项目一起启动,所以需要写监听类

5.创建Spring监听类,实现Netty服务与项目同时启动(代码中的port是指Netty服务端的端口号,我这里是配置在nacos的,也可以直接写死)

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;

/**
 * @Author Li XuAn Yi
 * @Date 2024/3/19 15:30
 * @Version 1.3.0
 * @Describe
 */
@Slf4j
@Component
public class NettyServerApplicationListener implements ApplicationListener<ApplicationEvent> {

    @Resource
    private NettyServer nettyServer;

    @Value("${netty.server.port}")
    private int port;

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationReadyEvent){
            String localIpAddress = this.getLocalIpAddress();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(localIpAddress,port);
            nettyServer.start(inetSocketAddress);

        }

    }

    private String getLocalIpAddress(){
        log.info("********开始获取IP地址********");
        String ipAddress="127.0.0.1";
        try {
            InetAddress localhost = InetAddress.getLocalHost();
            ipAddress = localhost.getHostAddress();
            log.info("******成功获取本地ID地址:{}*******",ipAddress);
            return ipAddress;
        } catch (UnknownHostException e) {
            log.info("******获取本地IP地址失败*******");
            e.printStackTrace();
        }
        return ipAddress;
    }
}

 如果加上监听类依然不能启动Netty服务端,检查一下项目的启动类是否启动了监听类;如下图的项目启动类

import com.frank.netty.NettyServerApplicationListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author 80540
 */
@SpringBootApplication
public class WebSocketTestApplication {

	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication(WebSocketTestApplication.class);
		springApplication.addListeners(new NettyServerApplicationListener());
		springApplication.run(args);
	}

}

6.Netty服务端注意事项

处理类NettyServerHandler中,如果需要依赖注入其它Service类,需要静态初始化

第一步,将Handler交给spring 管理 添加@Component

@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

 第二步,静态初始化

@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    public static NettyServerHandler nettyServerHandler;

    @Resource
    private IConeRecordService coneRecordService;

    @PostConstruct
    public void init() {
        nettyServerHandler = this;
        nettyServerHandler.coneRecordService = this.coneRecordService;
        // 初使化时将已静态化的testService实例化
    }

 第三步,引用,注意引用需要加前缀,注意注意!!!

/**
     * 客户端发消息会触发
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf msgBuf = (ByteBuf) msg;
        byte[] bytes = new byte[msgBuf.readableBytes()];
        msgBuf.readBytes(bytes);

        // 16进制转码后的内容
        String hexStr = HexUtil.encodeHexStr(bytes);
        log.info("收到服务器 {} 初始消息: {}",getClientIp(ctx),hexStr);

        // 16进制转 字符串
        String decodeHexStr = HexUtil.decodeHexStr(hexStr);

        log.info("服务器收到 {} 解码消息: {}",getClientIp(ctx), decodeHexStr);

		// 这里需要加前缀,避免为空
        nettyServerHandler.coneRecordService.addRecord(decodeHexStr);

        ctx.flush();
    }

 为了测试我们可以再简单搭建一个客户端

1.客户端初始化类,与服务端一样需要设置出入站编码器,如果不设置那就是字节数据,我这里没有设置,因为需要硬件的原始字节数据。一般也是设置为String编码器

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;


/**
 * @program: ksf
 * @author: lxy
 * @create: 2024-03-22 15:04
 * @description: 客户端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器,客户端服务端编解码要一致
 **/

public class NettyClientChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {


        channel.pipeline().addLast(new NettyServerHandler());
    }
}

2.创建客户端启动类,绑定的端口号要与服务端对应

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * @program: ksf
 * @author: lxy
 * @create: 2024-03-22 15:04
 * @description: 客户端
 **/

@Slf4j
@Data
public class NettyClient implements Runnable {

    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8080"));
    static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));

    private String content;

    public NettyClient(String content) {
        this.content = content;
    }

    @Override
    public void run() {
        // Configure the client.
        EventLoopGroup group = new NioEventLoopGroup();
        try {

            int num = 0;
            boolean boo =true;

            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new NettyClientChannelInitializer() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new NettyClientHandler());
                        }
                    });

            ChannelFuture future = b.connect(HOST, PORT).sync();

            while (boo) {

                num++;

                future.channel().writeAndFlush(content + "--" + new Date());

                try { //休眠一段时间
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //每一条线程向服务端发送的次数
                if (num == 1) {
                    boo = false;
                }
            }

            log.info(content + "-----------------------------" + num);
            //future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

}

3.客户端处理类 channelActive方法是发送消息的具体方法,代码中是本人项目中硬件的模拟消息,如果配上我的服务端,可以直接发送成功

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @program: ksf
 * @author: lxy
 * @create: 2024-03-22 15:04
 * @description: 客户端处理类
 **/

@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 计算有多少客户端接入,第一个string为客户端ip
     */
    private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CLIENT_MAP = new ConcurrentHashMap<>();

    @Override
    public void channelActive(ChannelHandlerContext ctx) {

        CLIENT_MAP.put(ctx.channel().id(), ctx);

        // 创建一个10字节的ByteBuf
        ByteBuf byteBuf = Unpooled.buffer(60);

        // 要放入的内容
        byte[] content = {(byte)0x14,(byte) 0x01,(byte) 0x02,(byte) 0x00,(byte) 0x08,(byte) 0x03,
                (byte)0x41,(byte) 0x30,(byte) 0x38,(byte) 0x30,(byte) 0x31,(byte) 0x30,(byte) 0x30,(byte) 0x30, (byte)0x30,(byte) 0x30,(byte) 0x30,(byte) 0x31,
                (byte) 0xBE,(byte) 0x09,(byte) 0x09};

        // 将内容写入ByteBuf
        byteBuf.writeBytes(content);

        int i = byteBuf.readableBytes();
        System.out.println("发送的字节数量:"+i);


        ctx.writeAndFlush(byteBuf);
    }

    /**
     * @param ctx
     * @author lxy on 2024-03-22 15:04
     * @DESCRIPTION: 有服务端端终止连接服务器会触发此函数
     * @return: void
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {

        ctx.close();
        log.info("服务端终止了服务");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {

        log.info("回写数据:" + msg);
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {

        //cause.printStackTrace();
        log.info("服务端发生异常【" + cause.getMessage() + "】");
        ctx.close();
    }

    /**
     * @param msg       需要发送的消息内容
     * @param channelId 连接通道唯一id
     * @author lxy
     * @DESCRIPTION: 客户端给服务端发送消息
     * @return: void
     */
    public void channelWrite(ChannelId channelId, String msg) {

        ChannelHandlerContext ctx = CLIENT_MAP.get(channelId);

        if (ctx == null) {
            log.info("通道【" + channelId + "】不存在");
            return;
        }

        //将客户端的信息直接返回写入ctx
        ctx.write(msg + " 时间:" + new Date());
        //刷新缓存区
        ctx.flush();
    }



    }

 4.测试类

**
 * @program: ksf
 * @author: lxy
 * @create: 2024-03-22 16:06
 * @description: 模拟多客户端发送报文
 **/
public class TestNettyClient {

    public static void main(String[] args) {

        //开启10条线程,每条线程就相当于一个客户端
        for (int i = 1; i <= 1; i++) {

            new Thread(new NettyClient("Hello Netty Server!" + i)).start();
        }
    }
}

 ok,启动项目然后运行测试类没可以看到Netty的运行日志了

 

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值