Netty 物联网自定协议(含粘包、拆包及半包处理)(多协议)

本文介绍了一种使用Netty处理物联网自定义协议的方法,包括粘包、拆包和半包的处理。通过责任链模式,定义了协议解析器接口,实现了统一的拆包粘包处理。此外,还展示了如何处理多个设备协议,并构建了一个协议解析器链,确保不同协议报文的正确解析。最后,文章提到了设备空闲检查和下游业务处理的实现。
摘要由CSDN通过智能技术生成

Netty 物联网自定协议(含粘包、拆包及半包处理)(多协议)

一、先看架构:

按步骤:罗列出具体代码实现思路

注意:只需要启动一个netty服务端口,就能处理不同的协议报文!

在这里插入图片描述

(1) 统一的拆包粘包处理

1. 使用责任链,定义解析器接口

public interface ProtocolRecognizer {

    void handler(ChannelHandlerContext ctx, ByteBuf in, List<Object> out);

    void setNext(ProtocolRecognizer recognizer);
}

2.由于每个协议的报文格式不一致因此需要定义通用属性

/**
 * 自定义协议属性
 */
public class ProtocolAgreement {
    protected final List<AProcessPair<ByteProcessor, Integer>> begins; //用于匹配 头文件开始报文
    protected final List<ByteProcessor> ends;
    protected final int headOffset; // 头文件(头部+报文长度)

    public ProtocolAgreement(List<AProcessPair<ByteProcessor, Integer>> begins, List<ByteProcessor> ends, int headOffset) {
        this.begins = begins;
        this.ends = ends;
        this.headOffset = headOffset;
    }
}

3.核心(抽象的拆包粘包处理器)

@Slf4j
public abstract class UnpacRecognizer extends ProtocolAgreement implements ProtocolRecognizer {
    protected int currentPackLength;
    private ProtocolRecognizer next;

    public UnpackRecognizer(List<AProcessPair<ByteProcessor, Integer>> begins, List<ByteProcessor> ends, int headOffset) {
        super(begins, ends, headOffset);
    }

    // 一帧一帧的读取报文
    protected abstract boolean startOffsetReadHead(ByteBuf buf);

    // 设置当前报文长度
    protected abstract void setCurrentPackLength(int packLength);
	
    // 解析报文
    protected abstract void analysis(ByteBuf in, List<Object> out);

    @Override
    public void handler(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        final AtomicInteger startCounter = this.getStartCounter(ctx);

        if (SkipHelper.isMatchHead(begins, startCounter.get(), in)) {
            try {
                while (in.isReadable()) {
                    in.markReaderIndex(); // 记录起始位
                    int readableBytes = in.readableBytes();
                    if (readableBytes < headOffset) {
                        // 4 是头文件部分
                        // 半包,且报文不完整
                        return;
                    }
                    //  读到头文件
                    if (startOffsetReadHead(in)) {
                        // 半包问题
                        // 协议包头数据还未到齐,回到协议开始的位置,等待数据到齐
                        if (readableBytes < currentPackLength) {
                            in.resetReaderIndex();
                            return;
                        }

                        // 正确报文
                        if (readableBytes == currentPackLength) {
                            in.resetReaderIndex(); // 重置读指针
                            analysis(in, out);
                            this.resetStartCounter(startCounter);
                            return;
                        }

                        // 粘包问题
                        in.resetReaderIndex(); // 重置读指针 到mark的位置
                        if (SkipHelper.isMatchEnd(ends, in.readerIndex(), Math.min(readableBytes, currentPackLength), in)) {
                            analysis(in, out);
                            startCounter.set(in.readerIndex()); // 设置粘包匹配起始位

                            log.info("--- 当前: start: {}, readable: {} ,剩余报文:{}", startCounter.get(), in.readableBytes(), ByteBufUtil.hexDump(in));
                            return;
                        }
                        // 通过读取,来丢弃无效报文,直接读到包尾或读完
                        readDiscard(in, startCounter);
                    }
                }
            } catch (Exception e) {
                log.error("协议解析失败", e);
                SkipHelper.skip(in);
            }
            return;
        }

        int start = startCounter.get();
		
        if (start > 0 && next == null ) { // 没有可处理的解析器,直接丢掉
            log.warn("start:{}  丢弃无效报文:{}  剩余可读:{}", start, ByteBufUtil.hexDump(in), in.readableBytes());
            SkipHelper.skip(in);
            this.resetStartCounter(startCounter);
            return;
        }

        if (next != null) {
            
            // 当前协议处理不了,交给下一个 handler处理
            this.resetStartCounter(startCounter);
            next.handler(ctx, in, out);
            return;
        }

        SkipHelper.skipToNext(in, out);
    }

    /**
     * 读取且丢弃 报文
     *
     * @param in
     * @return
     * @throws Exception
     */
    private void readDiscard(ByteBuf in, AtomicInteger startCounter) throws Exception {
        boolean b = false;
        log.info("脏包 丢弃报文:{}", ByteBufUtil.hexDump(in));

        while (in.isReadable()) {
            for (ByteProcessor processor : ends) {
                if (in.isReadable()) {
                    b = !processor.process(in.readByte());
                }
            }

            if (b) {
                startCounter.set(in.isReadable() ? in.readerIndex() : 0); // 重置起始位,粘包和读完情况
                // 读到包尾
                log.info("剩余报文: {} , start:{}", ByteBufUtil.hexDump(in), startCounter.get());
                return;
            }
        }
    }

    /**
     * 单独记录起始位,防止上层读取buf污染readIndex
     *
     * @param ctx
     * @return
     */
    private AtomicInteger getStartCounter(ChannelHandlerContext ctx) {
        AtomicInteger startCounter;
        synchronized (ctx.channel()) {
            Attribute<AtomicInteger> attr = ctx.channel().attr(IotChannelConstant.STICKING_COUNTER); // 这里是直接放入channel中
            startCounter = attr.get();
            if (startCounter == null) {
                startCounter = new AtomicInteger(0);
                attr.set(startCounter);
            }
        }
        return startCounter;
    }

    private void resetStartCounter(AtomicInteger startCounter) {
        startCounter.set(0);
    }

    @Override
    public void setNext(ProtocolRecognizer recognizer) {
        this.next = recognizer;
    }
}
SkipHelper 是否跳过当前解析器的相关辅助代码
@Slf4j
public class SkipHelper {

    /**
     * 是否匹配头文件
     * —— 该方法只是宏观上匹配,具体是不是头文件,还需要一帧一帧的去识别 {@link UnpackRecognizer#startOffsetReadHead(ByteBuf)}
     *
     * @param begins
     * @param start
     * @param in
     * @return
     */
    @SuppressWarnings("all")
    public static boolean isMatchHead(List<AProcessPair<ByteProcessor, Integer>> begins, int start, ByteBuf in) {
        AtomicBoolean isHead = new AtomicBoolean(false);
        try {
            begins.forEach(t -> {
                if (t.getOffset() > in.readableBytes()) {
                    return;
                }
                 isHead.compareAndSet(false, in.forEachByte(start, t.getOffset(), t.getProcess()) > -1);
            });
        } catch (IndexOutOfBoundsException e) {
            log.warn("isMatchHead 数组越界异常: start: {} , 异常:{}", start, e);
        }
        return isHead.get();
    }


    /**
     * 是否匹配包尾
     * @param ends
     * @param start
     * @param endOffset
     * @param in
     * @return
     */
    public static boolean isMatchEnd(List<ByteProcessor> ends, int start, int endOffset, ByteBuf in) {
        AtomicBoolean isEnd = new AtomicBoolean(false);
        try {
            ends.forEach(t -> {
                isEnd.compareAndSet(false, in.forEachByte(start, endOffset, t) > -1);
            });
        } catch (IndexOutOfBoundsException e) {
            log.warn("isMatchEnd 数组越界异常: start: {} , 异常:{}", start, e);
        }
        return isEnd.get();
    }

    /**
     * 跳过交给下层  decoder处理
     */
    public static void skipToNext(ByteBuf buf, List<Object> out) {
        ByteBuf duplicate = buf.retainedDuplicate();
        buf.skipBytes(buf.readableBytes());
        out.add(duplicate); // 下一层处理
    }


    /**
     * 直接跳过 不处理
     *
     * @param buf
     */
    public static void skip(ByteBuf buf) {
        buf.skipBytes(buf.readableBytes());
    }
}

(2) 多个设备协议->继承抽象的拆包器

/**
 * A 协议
 */
@Recognizer(order = 1) // 注解 扫描使用
@Slf4j
public class AProtocol extends UnpackRecognizer {
    private static final ByteProcessor HEAD_PROCESSOR = new ByteProcessor.IndexOfProcessor((byte) 0xAA);
    private static final ByteProcessor HEAD2_PROCESSOR = new ByteProcessor.IndexOfProcessor((byte) 0xBB);
    private static final ByteProcessor END_PROCESSOR = new ByteProcessor.IndexOfProcessor((byte) 0xDD);
    private static final ByteProcessor END2_PROCESSOR = new ByteProcessor.IndexOfProcessor((byte) 0xEE);
    private final static byte FIXED_LEN = 7;

    private static final List<AProcessPair<ByteProcessor, Integer>> BEGINS = Arrays.asList(new AProcessPair<>(HEAD_PROCESSOR, 1), new AProcessPair<>(HEAD2_PROCESSOR, 2));
    private static final List<ByteProcessor> ENDS = Arrays.asList(END_PROCESSOR, END2_PROCESSOR);

    public AProtocol() {
        super(BEGINS, ENDS, 4);
    }

    @Override
    protected boolean startOffsetReadHead(ByteBuf buf) {
        // 一帧一帧读取
        byte head = buf.readByte();
        byte head2 = buf.readByte();
        byte userIdLen = buf.readByte();
        byte len = buf.readByte();
        try {
            if (!(HEAD_PROCESSOR.process(head) && HEAD2_PROCESSOR.process(head2))) {
                // 文件头匹配
                this.setCurrentPackLength(FIXED_LEN + userIdLen + len); // 计算出当前包大小
                return true;
            }
        } catch (Exception e) {
            log.error("解析文件头失败,头文件不完整,请检查 headOffset 参数值是否正确", e);
        }
        return false;
    }

    @Override
    protected void setCurrentPackLength(int packLength) {
        super.currentPackLength = packLength;
    }

    @Override
    protected void analysis(ByteBuf in, List<Object> out) {
        A a = ADecodeUtil.decode(in); // 重点  这里只需要把正确报文读完即可,父类已处理好拆包粘包问题了
        out.add(a);
    }
}

其他: AProcessPair

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AProcessPair<K, V> {
    private K process; // 头文件 ByteProcessor
    public V offset; // 报文位置
}

一.使用并解析报文

1.构建责任链
@Slf4j
public class AbstractRecognizerFactory {

    private static final List<ProtocolRecognizer> recognizers;
    private static final AtomicReference<ProtocolRecognizer> REFERENCE = new AtomicReference<>();

    static {
        Class<ProtocolRecognizer> recognizerClass = ProtocolRecognizer.class;
        recognizers = new ArrayList<>();
        TreeMap<Integer, ProtocolRecognizer> map = new TreeMap<>();
        try {
            Set<String> classNames = PathFileResolver.loadClassNames("定义你要扫描包的路径");
            log.info("协议识别器扫描路径: {}", classNames);
            for (String name : classNames) {

                Class<Object> clazz = ClassUtil.loadClass(name);

                if (!clazz.isInterface() && !ClassUtil.isAbstract(clazz) && recognizerClass.isAssignableFrom(clazz)) {
                    Recognizer annotation = clazz.getAnnotation(Recognizer.class); // 扫描注解 (注解的定义不贴代码了)
                    ProtocolRecognizer o = (ProtocolRecognizer) clazz.newInstance(); // 无参构造
                    int order = annotation.order();
                    map.put(order, o);
                }
            }

            for (Map.Entry<Integer, ProtocolRecognizer> recognizerEntry : map.entrySet()) {
                recognizers.add(recognizerEntry.getValue());
            }
        } catch (InstantiationException | IllegalAccessException e) {
            log.error("初始化 AbstractRecognizerFactory 失败!", e);
        } finally {
            map.clear();
        }
    }

    // 获取多个协议解析器链  A,B,C
    public static ProtocolRecognizer getRecognizer() {
        if (REFERENCE.get() == null) {
            Assert.notEmpty(recognizers);
            ProtocolRecognizer last = null;
            for (ProtocolRecognizer recognizer : recognizers) {
                if (REFERENCE.compareAndSet(null, recognizer)) {
                    last = recognizer;
                    continue;
                }
                if (last != null) {
                    last.setNext(recognizer);
                    last = recognizer;
                }
            }
        }
        return REFERENCE.get();
    }
}
2.协议的统一入口 (继承ByteToMessageDecoder)
/**
 * 当前解码器优先级高于HTTP和MQTT 这里跟加入 管道的顺序有关,最好置于其他管道之上
 * 
 */
@Slf4j
public class IotDecoder extends ByteToMessageDecoder {
    private static final ProtocolRecognizer recognizer = AbstractRecognizerFactory.getRecognizer();
    private static final HttpHeaderParser HTTP_PARSER = new HttpHeaderParser();

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (this.isSkip(in)) {
            SkipHelper.skipToNext(in, out); // 用来判断是否跳过,自定的协议,直接走到下游,这里我的下游有HTTP 协议,因此直接跳过自定义协议
                                            // 前面说过,一个端口可以处理不同的协议,http等 同样也能处理,加入责任链中即可
            return;
        }
        
        // 重要:
        // 【这里吸收了 DDD(领域驱动设计)的一些思想 把解析出的 A,B,C  看成不同的 Domain 对象实体,然后进行业务流转 。 下面(4) 步骤就是业务流转】
        
        recognizer.handler(ctx, in, out); // 这里其实是一个责任链,处理不了交给下一层处理解析出 A,B,C 等多个自定义的 domain (实体对象了,不是报文)
    }


    /**
     * 扩展 是否跳过当前handler
     */
    private boolean extend(ByteBuf in) {
        return HTTP_PARSER.isHttp(in);// || MQTT_PARSER.isMqtt(in); 比如: MQTT 协议的扩展
    }

    /**
     * 是否跳过
     * #这里需要注意,下游判断不能对buf的读指针发生偏移操作
     * 否则报文会被污染
     */
    private boolean isSkip(ByteBuf in) {
        try {
            in.markReaderIndex(); // 记录指针位置
            return extend(in);
        } catch (Exception ignore) {
        } finally {
            in.resetReaderIndex(); // 重置读指针
        }
        return false;
    }
}

(3)设备空闲检查-只需要检查读写即可

@Slf4j
@ChannelHandler.Sharable
public class HeartbeatHandler extends SimpleUserEventChannelHandler<IdleStateEvent> {

    @Override
    protected void eventReceived(ChannelHandlerContext ctx, IdleStateEvent idle) throws Exception {
        	// 触发检查空闲检查
            // 1.发送一个下线通知
            // 2.关闭channel
          
    }
}

(4)下游处理 -与业务相关

@Slf4j
@ChannelHandler.Sharable
public class AHandler extends SimpleChannelInboundHandler<"你定义的A协议实体类"> {
    
    private final DomainDispatch dispatch; // 实现一个消息分发处理逻辑,通过下面路由请求上报消息
    
   	// 省略 构造

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, A a) throws Exception {
        // 这里需要注意的是,需要删除当前channel多余的下游handler,比如http的handler(否则会出问题)
           // ctx.pipeline().remove(HttpServerCodec.class);
           // ctx.pipeline().remove(HttpObjectAggregator.class);
        // 为什么要删除的原因是,一开始的请求,我并不知道,具体是什么协议,因此我把所有的 handler 全部加入了pipeline中
        // 到这里已经确定,请求是由A协议处理,因此删除其他handler,避免被误解析到
        
        // 业务消息处理 domain A
        // 设备上下线,指令上报等

        ReportRequestEntry entry = ReportRequestEntry.builder().fromChannel(ctx).route(RemoteRoute.a).domain(a).build();
        dispatch.doReport(entry); // 上报消息到业务服务器

    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 设备下线等处理
    }
}

(4.1) 根据 具体的实体对象上报

/**
 * 上报指令request
 */
public class ReportRequestEntry<T extends "A,B,C 自定义协议实体的基类(比如设备编号,二进制转换的编码解码等)"> {
    @NonNull
    private final ChannelHandlerContext fromChannel;
    @NonNull
    private final RemoteRoute route; // 这是自定义的上报路由地址, 比如A协议上报业务系统,请求业务服务:http://A业务系统的url  处理响应A协议
    @NonNull
    private final T domain;
}

消息分发:
/**
 * 消息分发
 */
public interface DomainDispatch  {
    /**
     * 上报
     * —— Domain 通知系统
     *
     * @param reportRequest
     */
    void doReport(ReportRequestEntry reportRequest) throws Exception;

    /**
     * 下发
     * —— 根据Domain下发指令
     *
     * @param ctx
     * @param domain
     */
    void doCommand(ChannelHandlerContext ctx, BaseAttribute domain) throws Exception;

}

(5)上报业务

/**
 * 消息分发器
 */
@Slf4j
public class DefaultDomainDispatch  implements DomainDispatch {
	
    /**
     * 上报
     */
    @Override
    public void doReport(ReportRequestEntry reportRequest) throws Exception {
        // A B C 自定义协议的基类
        BaseAttribute domain = reportRequest.getDomain();
        ChannelHandlerContext ctx = reportRequest.getFromChannel();
		// 其他处理,比如下发指令的回调
        
        // 这里直接获取远端地址,进行上报 (Remote) 下面已经有相关的代码展示,
        Remote remote = context.getRemote(reportRequest);
        // Remote 具体的上报协议, 可以是短连接 http 或长连接的tcp协议,这里可以参考  dubbo 和feign 的处理方式
        
        boolean b = remote.tryReport(reportRequest);

        if (!b) {
            remote.reportFailed(ctx, domain);
        }
    }
    // 其他处理 省略
}
远端地址的定义
/**
 * 远端接口 即 对接业务系统
 */
public interface Remote {
    /**
     * 上报
     *
     * @param reportRequest
     * @throws Exception
     */
    boolean tryReport(ReportRequestEntry reportRequest);

    /**
     * 上报失败的响应,比如返回给设备的确认帧或者否认帧
     *
     * @param ctx
     * @param domain
     */
    void reportFailed(ChannelHandlerContext ctx, BaseAttribute domain);


}

我这里的处理是采用 http

public interface HttpRemote extends Remote {

    HttpResponse doRequest(HttpJsonRequest request) throws Exception;

}

负载均衡的定义:

public interface LoadBalance extends ConfigContext {

    /**
     * 轮询
     */
    String roundRobinRemoteHost(String lastFailedHost);

    /**
     * 随机
     */
    String randomRemoteHost(String lastFailedHost);


    /**
     * 权重
     */
    String weightRemoteHost(String lastFailedHost);

    /**
     * 刷新host 列表
     * @param rest true 直接刷新;false 当host为空的时候才会刷新
     */
    void refreshUpdateHosts(boolean rest);

    /**
     * 根据负载均衡策略获取host
     *
     * @param failedHost 最后失败的host
     * @return
     */
    String invokeGetHost(String failedHost);

}

总结: 很多思路其实在定义接口时已经体现了,第一篇文章,感觉文字太多,反而描述不清楚,这次直接上代码

源码地址: https://gitee.com/bxbz/iot-server (觉得不错的话,给一颗小星星鼓励一下吧)

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值