Bootstrap

【Netty实现udp协议通讯】

Netty中的udp

使用Netty去实现udp通讯,在netty中支持udp的广播,组播,单播三中模式,本文主要是和硬件进行udp通讯,需要实现一发一收机制,所以使用单播模式进行通讯。

一. 实现服务端

  1. 该服务类交由Spring进行管理
  2. PortDefinition为配置文件,进行读取yml配置参数的
  3. UdpInitHandler为处理器
@Slf4j
@Service
public class UdpListeningServer implements Runnable {

    @Autowired
    private PortDefinition portDefinition;
    @Autowired
    private UdpInitHandler udpInitHandler;
    private EventLoopGroup bossGroup;
    private List<Channel> ioChannels = new ArrayList<Channel>();

    @PostConstruct
    public void init() {
        new Thread(this).start();
    }

    /**
     * 服务监听启动
     */
    public void run() {
        bossGroup = new NioEventLoopGroup();
        try {
            Bootstrap bs = new Bootstrap();
            bs.group(bossGroup);
            bs.channel(NioDatagramChannel.class);
            bs.option(ChannelOption.SO_BROADCAST, false);// 不广播
            bs.handler(udpInitHandler);

            int udpPort = portDefinition.getUdpPort();
            // 绑定端口,开始接收进来的连接
            Channel ch = bs.bind(udpPort).sync().channel();
            ioChannels.add(ch);
            log.info("... UDP listening on port:{}", udpPort);
        } catch (Exception e) {
         throw new BizException("udp服务监听失败:{}", e);
        }
    }
        /**
     * 注销、关闭
     */
    @PreDestroy
    public void destroy() {
        //
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }
        //
        for (Channel ch : ioChannels) {
            ch.closeFuture().syncUninterruptibly();
        }
    }
}

二. 实现Udp处理器

  1. MantunsciMsgHandler 位消息处理器,由于整个服务端都交由Spring进行管理,所以必须使用注入的方式,不能通过new进行创建
/**
 * UDP的处理器初始化
 *
 * @author xxxx
 *
 */
@Component
public class UdpInitHandler extends ChannelInitializer<DatagramChannel> {

	@Autowired
	private MsMsgHandler msMsgHandler;
	@Override
	protected void initChannel(DatagramChannel ch) throws Exception {
		ChannelPipeline pipeline = ch.pipeline();

		pipeline.addLast(new ChannelInboundHandlerAdapter());
		// 消息UDP解析器
		pipeline.addLast(new MsMsgDecoder());
		// 消息UDP编码器
		pipeline.addLast(new MsMsgEncoder())
		// 消息UDP处理器
		pipeline.addLast(msMsgHandler);
	}
}

三. 实现UDP解析器

  1. 具体解析内容,按需解析
/**
 * UDP消息解析器
 *
 * @author xxxx
 */
@Slf4j
public class MsMsgDecoder extends MessageToMessageDecoder<DatagramPacket> {


    @Override
    protected void decode(ChannelHandlerContext ctx, DatagramPacket dataPkg, List<Object> out) throws Exception {
        // 获取UDP消息数据包字节流
        ByteBuf buf = dataPkg.content();

        // 转换协议将协议转换为十六进制,方便排查问题
        String msg = Decoder.bytesToString(buf);

        // 解析字节流并转换为消息对象
        ProtocolMessageDto msgObj = new ProtocolMessageDto
        ........
        }
}

四. 实现UDP编码器

  1. 具体编码内容,按需编码
/**
 * UDP消息编码器
 *
 * @author xxxx
 */
@Slf4j
public class MsMsgEncoder extends MessageToMessageEncoder<ProtocolMessageDto> {

    @Override
    protected void encode(ChannelHandlerContext ctx, ProtocolMessageDto msg, List<Object> out) throws Exception {
        //起始域
        StringBuffer sb = new StringBuffer(msg.getMsf());
        //消息ID
        sb.append(msg.getCmd());
        //消息参数1
        sb.append(msg.getCmdVer());
        //消息参数2
       ........
       }
}

五. 实现UDP协议消息处理器

  1. 该处理器主要对解码后的消息进行业务处理
/**
 * UDP协议消息处理器
 *
 * @author xxxx
 */
@Slf4j
@Component
public class MsMsgHandler extends SimpleChannelInboundHandler<ProtocolMessageDto> {

    public static Map<String, Channel> getMTS_MAP() {
        return MTS_MAP;
    }

    public static Map<String, String> getMTS_IP_MAP() {
        return MTS_IP_MAP;
    }

    /**
     * 用来保存对应的设备MAC-channel
     */
    private static final ConcurrentHashMap<String, Channel> MTS_MAP = new ConcurrentHashMap<>();
        /**
     * 用来保存对应的设备MAC-ip:port
     */
    private static final ConcurrentHashMap<String, String> MTS_IP_MAP = new ConcurrentHashMap<>();
    /**
     * 用来记录网关-服务器间通讯时间
     */
    public static final ConcurrentHashMap<String, LocalDateTime> MTS_HEARTBEAT_MAP = new ConcurrentHashMap<>();
    
    public static void closeChannel(String gwMac) {
        MTS_MAP.remove(gwMac);
        MTS_IP_MAP.remove(gwMac);
        MTS_HEARTBEAT_MAP.remove(gwMac);
    }
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessageDto msgObj) throws Exception {
        // 处理从消息解码器传递下来的消息对象,先对消息队形进行判空防止NPE
        if (msgObj == null) {
            return;
        }
        MTS_HEARTBEAT_MAP.put(msgObj.getMac(), LocalDateTime.now());
        // CMD 命令,为服务器下发命令,服务器再次收到时释放同步锁。无需回复
        if (MsCMDConstant.COMMAND.equals(msgObj.getCmd())) {
            String key = msgObj.getCmd() + msgObj.getCmdVer();
            mtsCorrespondent.confirmSuccess(msgObj, key);
            return;
        }
        MTS_MAP.put(msgObj.getMac(), ctx.channel());
        MTS_IP_MAP.put(msgObj.getMac(), msgObj.getIp() + MsConstant.COLON + msgObj.getPort());
         // 获取消息对象的DATA属性,
        String data = msgObj.getData();
        // 根据消息类型区分,参照协议对应消息体约定格式解析具体数据,并转换成数据对象,进行不同的业务逻辑处理
        // CMDxxxx配置消息
        if (MsCMDConstant.HANDSHAKE_MSG.equals(msgObj.getCmd()) || MsCMDConstant.HEARTBEAT_MSG.equals(msgObj.getCmd())) {

        }
        // CMD=xxxx消息
        else if (MsCMDConstant.PUSH_GATEWAY_CONFIG_MSG.equals(msgObj.getCmd())) {
            this.netConfig(data);
        }
        //回复时都没有回复数据体
        msgObj.setData("");
        ctx.writeAndFlush(msgObj);
    }
   
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

六. 实现发送逻辑

/**
 * @author z26146
 * @description: 通信者,负责与硬件通信
 * @date 2022-5-12 14:02
 */
@Slf4j
@Component
public class MtsCorrespondent {

    /**
     * 暂存发送的包 - 用于重复逻辑
     */
    private final Map<String, ProtocolMessageDto> receConfirm = new ConcurrentHashMap<>();
    /**
     * 发送报文数据
     *
     * @param message  数据信息
     * @param isResend 是否重发
     */
    public void sendMessage(ProtocolMessageDto message, boolean isResend) {
        if (redisClient.exists(RedisKeyConstants.OK_IOT_GATEWAY + message.getMac())) {
            // 将对象存入缓存中
            String key;
            key = message.getCmd() + message.getCmdVer();
            // 将key转为大写
            key = key.toUpperCase();
            // 初始化暂定为 未响应
            message.setAck(RespondCodeUtil.RESPON
            int count = CommonUtils.NumberUtil.NUM_ZERO;
            //开启同步  当命令存在时,进行等待,如果6s后上个命令还没执行完返回设备忙碌
            while (receConfirm.containsKey(key)) {
                if (count == CommonUtils.NumberUtil.NUM_THREE) {
                    break;
                }
                try {
                    Thread.sleep(CommonUtils.NumberUtil.NUM_2000);
                } catch (InterruptedException e) {
                    log.error("{}", e);
                }
                count++;
                if (count == CommonUtils.NumberUtil.NUM_THREE) {
                message.setAck(RespondCodeUtil.DEVICE_BUSY.getCode());
                return;
            }
            int num = isResend ? CommonUtils.NumberUtil.NUM_THREE : CommonUtils.NumberUtil.NUM_ONE;
            while (num > CommonUtils.NumberUtil.NUM_ZERO) {
                receConfirm.put(key, message);
                if (message.getAck() == RespondCodeUtil.RESPONSE_TIMEOUT.getCode()) {
                synchronized (message) {
                        try {
                            send(message);
                            //等待网关4秒消息响应
                            message.wait(CommonUtils.NumberUtil.NUM_4000);
                        } catch (InterruptedException e) {
                            log.error("{}", e);
                        }
                    }
                }
                if (message.getAck() == RespondCodeUtil.RESPONSE_TIMEOUT.getCode()) {
                    //未响应的才需要释放 key value 已响应的key value已被释放
                    if (message == receConfirm.get(key)) {
                        receConfirm.remove(key);
                    }
                    num--;
                } else {
                    num = CommonUtils.NumberUtil.NUM_ZERO;
                }
                }
        }
        log.info("<------ 响应数据结果 :{}", message);
    }
        /**
     * 释放线程
     *
     * @param message 消息
     * @param key     键
     */
    public void confirmSuccess(ProtocolMessageDto message, String key) {
        ProtocolMessageDto sendMessage;
        sendMessage = receConfirm.remove(key.toUpperCase());
        // 网关响应成功后,唤醒发送对象
        if (sendMessage != null) {
            sendMessage.setAck(message.getAck());
            sendMessage.setData(message.getData());
            synchronized (sendMessage) {
                sendMessage.notify();
            }
        }
    }
        /**
     * 发送报文
     *
     * @param message 发送报文
     */
    protected void send(ProtocolMessageDto message) {
        String[] ipp = MsMsgHandler.getMTS_IP_MAP().get(message.getMac()).split(MsConstant.COLON);
        message.setIp(ipp[0]);
        message.setPort(Integer.parseInt(ipp[1]));
        Channel channel = MsMsgHandler.getMTS_MAP().get(message.getMac());
        try {
            channel.writeAndFlush(message);
        } catch (NullPointerException e) {
            log.error("发送报文失败:{}", message);
        }
    }
 }
            
   
;