android中ble蓝牙分包接收的粘包处理

前言

在android中,BLE的特征一次读写最大长度20字节。对于长时间连续发送的大于20字节的帧结构,如果安卓终端进行接收的话,就需要我们进行重新组帧(即如何处理粘包和丢包问题)。对于如何处理这个问题,首先需要在帧结构上做好设计。一个比较完整的帧,应包含帧头,帧长度,帧序号以及帧尾,通过这些信息来做判断是否丢帧和重新组帧。
就拿我们最近做的一个蓝牙接收项目来说,我们设计每帧是128字节。其具体定义为:

/**
 * Created by sks on 2017/12/1.
 * /**
 * @author sks
 * *  定义帧的结构
 *
 *
 *       帧头:4个字节   ,  0xbebebebe
 *       帧计数:1个字节      
 *      帧长度:1个字节     128
 *      sessionId: 4个字节
 *      heart_array: 心电数组  100字节
 *      breath_array:呼吸数组  10字节
 *      temperature_value:体温  2字节
 *      heart_value: 心率  1字节
 *      breath_value: 呼吸率 1字节
 *      power_level:电池标志位  1字节
 *      reserved_array: 保留字节数组  1字节
 *      帧尾:   2字节   0xaaaa
 *
 *
 */

public class EcgMsg {

    /**
     *
     */
    public EcgMsg() {
        super();
    }

    private byte[] frameHeader;
    private int frameNum;   //一个字节
    private int frameLength;
    private byte[] sessionID;
    private byte[] heart_array;
    private byte[] breath_array;
    private float temperature_value;
    private int heart_value;
    private int breath_value;
    private int power_level;
    private byte[] reserved_array;

    public EcgMsg(byte[] frameHeader, int frameNum, int frameLength, byte[] sessionID, byte[] heart_array, byte[] breath_array, float temperature_value, int heart_value, int breath_value, int power_level, byte[] reserved_array) {
        this.frameHeader = frameHeader;
        this.frameNum = frameNum;
        this.frameLength = frameLength;
        this.sessionID = sessionID;
        this.heart_array = heart_array;
        this.breath_array = breath_array;
        this.temperature_value = temperature_value;
        this.heart_value = heart_value;
        this.breath_value = breath_value;
        this.power_level = power_level;
        this.reserved_array = reserved_array;
    }

    public byte[] getFrameHeader() {
        return frameHeader;
    }

    public void setFrameHeader(byte[] frameHeader) {
        this.frameHeader = frameHeader;
    }

    public int getFrameNum() {
        return frameNum;
    }

    public void setFrameNum(int frameNum) {
        this.frameNum = frameNum;
    }

    public int getFrameLength() {
        return frameLength;
    }

    public void setFrameLength(int frameLength) {
        this.frameLength = frameLength;
    }

    public byte[] getSessionID() {
        return sessionID;
    }

    public void setSessionID(byte[] sessionID) {
        this.sessionID = sessionID;
    }

    public byte[] getHeart_array() {
        return heart_array;
    }

    public void setHeart_array(byte[] heart_array) {
        this.heart_array = heart_array;
    }

    public byte[] getBreath_array() {
        return breath_array;
    }

    public void setBreath_array(byte[] breath_array) {
        this.breath_array = breath_array;
    }

    public float getTemperature_value() {
        return temperature_value;
    }

    public void setTemperature_value(float temperature_value) {
        this.temperature_value = temperature_value;
    }

    public int getHeart_value() {
        return heart_value;
    }

    public void setHeart_value(int heart_value) {
        this.heart_value = heart_value;
    }

    public int getBreath_value() {
        return breath_value;
    }

    public void setBreath_value(int breath_value) {
        this.breath_value = breath_value;
    }

    public int getPower_level() {
        return power_level;
    }

    public void setPower_level(int power_level) {
        this.power_level = power_level;
    }

    public byte[] getReserved_array() {
        return reserved_array;
    }

    public void setReserved_array(byte[] reserved_array) {
        this.reserved_array = reserved_array;
    }

    @Override
    public String toString() {
        return "EcgMsg{" +
                "frameHeader=" + Arrays.toString(frameHeader) +
                ", frameNum=" + frameNum +
                ", frameLength=" + frameLength +
                ", sessionID=" + Arrays.toString(sessionID) +
                ", heart_array=" + Arrays.toString(heart_array) +
                ", breath_array=" + Arrays.toString(breath_array) +
                ", temperature_value=" + temperature_value +
                ", heart_value=" + heart_value +
                ", breath_value=" + breath_value +
                ", power_level=" + power_level +
                ", reserved_array=" + Arrays.toString(reserved_array) +
                '}';
    }
}

硬件前端采用了串口转蓝牙模块,通过安卓蓝牙接收,可以发现接收方式是128个字节分为7次接收,即20*6+8。传输过程中,这7次接收都有可能丢失某一次,因此我们受到的数有可能是
- 找不到帧头,即7次接收中第一次发的数丢了;
- 找不到帧尾,即最后一次接收8字节失败
- 中间某次接收失败,这种情况是帧头帧尾在,但中间数据丢失
- 以上几种情况组合。
对于上述情况,我们在接收后重新组帧,都必须考虑。
在java中,tcp框架常采用netty库,其通过封装好的方法,可以自动为我们进行tcp的组包处理(处理粘包)。其核心是定义了核心类:缓冲区 ByteBuf,其比java原生NIO的bytebuffer更强大。 因此,我们可以考虑借用netty的ByteBuf缓冲区来为我们的蓝牙接收进行组帧处理。

关于Netty的Bytebuf类介绍

ByteBuf都是维护一个byte数组的,它的内部格式是:

 *      +-------------------+------------------+------------------+
 *      | discardable bytes |  readable bytes  |  writable bytes  |
 *      |                   |     (CONTENT)    |                  |
 *      +-------------------+------------------+------------------+
 *      |                   |                  |                  |
 *      0      <=      readerIndex   <=   writerIndex    <=    capacity

与原生态的ByteBuffer相比,它维护了两个指针,一个是读指针,一个是写指针,而原生态的ByteBuffer只维护了一个指针,你需要调用flip方法来切换读写的状态,不易用户管理维护

读的时候,可读的区域是下标区间是[readerIndex,writeIndex),可写区间的是[writerIndex,capacity-1],但是discardable这段区间就会变得相对无用,既不能读,也不能写

所以我们可以使用discardReadBytes的方法进行内存空间的回收,回收之后是这样的:

 *      +------------------+--------------------------------------+
 *      |  readable bytes  |    writable bytes (got more space)   |
 *      +------------------+--------------------------------------+
 *      |                  |                                      |
 * readerIndex (0) <= writerIndex (decreased)        <=        capacity

bytebuf还有个重要功能是标记Mark和重置reset。

private int markedReaderIndex;//标记读索引
private int markedWriterIndex;//标记写索引

通过重置方法,可以在读指针回到标记读索引位置

实现

首先定义Bytebuf变量,初始化缓冲区大小

 private ByteBuf buffer= Unpooled.buffer(1024*1000);

假设在蓝牙接收数据的回调方法中,每次收到的数通过下面代码获得:

  final byte[] data = characteristic.getValue();

对于源源不断的data数组,需要送进缓冲区中,然后进行组帧处理

        if (data != null && data.length > 0) {
            buffer.writeBytes(data);     //将蓝牙收到的byte数组放入bytebuf缓冲区中
         //   Log.i("tag", "缓冲区大小" + buffer.readableBytes() + "");

            //重新组帧
            while (buffer.readableBytes() > MyApplication.FRAME_LENGTH) {     //判断缓冲区大小大于一帧长度,就    
   可以进行解帧
                ByteBuf bufTemp = buffer.readBytes(1);    //先取第一个字节,判断是不是帧头的第一个字节0xbe
                byte[] bytesTemp = new byte[1];
                bufTemp.readBytes(bytesTemp);
                if (bytesTemp[0] == (byte) 0xbe) {        //判断第一个字节是不是0xbe,如果不是,直接丢弃,如果是,则进入if判断
                    buffer.markReaderIndex();     //取后三个数时候,考虑如果第1个是0xbe,但是后面三个不是0xbe,这个时候需要进行回滚操作
                    ByteBuf bufTemp1 = buffer.readBytes(3);   //如果第一个字节是0xbe,就再取后三个字节,继续对帧头进行判断
                    byte[] bytesTemp1 = new byte[3];
                    bufTemp1.readBytes(bytesTemp1);
                    if (bytesTemp1[0] == (byte) 0xbe && bytesTemp1[1] == (byte) 0xbe && bytesTemp1[2] == (byte) 0xbe) {      //如果后三位是0xbebebe,说明找到了帧头,就需要取一帧的后续部分(帧长-帧头)=128-4=124个字节
                            ByteBuf bufTemp2 = buffer.readBytes(MyApplication.FRAME_LENGTH - 4);
                            byte[] bytesTemp2 = new byte[MyApplication.FRAME_LENGTH - 4];
                            bufTemp2.readBytes(bytesTemp2);
                           //取出帧的后续部分,还需要判断帧尾是不是0xaa,0xaa;如果不是0xaaaa,说明这个帧不完整,即需要重新进入第二个字节搜索帧头
                            if (bytesTemp2[bytesTemp2.length-2]!=(byte)0xaa&&bytesTemp2[bytesTemp2.length-1]!=(byte)0xaa){
                                buffer.resetReaderIndex();   //指针回滚,回滚到只是取出第一个数
                                continue;       //重新进入while循环
                             }
                   //         查看帧计数
                            new_frame_index=bytesTemp2[0]&0xff;
                            if (new_frame_index!=(old_frame_index+1)||((new_frame_index==0)&&(old_frame_index!=255))){
                                    error_Frame_count++;
                                   log2Flie.log2FileFun("log","帧计数错误"+"error_Frame_count-->"+error_Frame_count+";old_frame-->"+old_frame_index+";new_frame-->"+new_frame_index);
                            }
                            old_frame_index=new_frame_index;
                            //重新组帧
                            byte[] bytesTemp3 = new byte[MyApplication.FRAME_LENGTH];
                            for (int i = 0; i < 4; i++) {
                                bytesTemp3[i] = (byte) 0xbe;
                            }
                            System.arraycopy(bytesTemp2, 0, bytesTemp3, 4, bytesTemp2.length);
                            //如果是,则放入list
                            synchronized (byteListInbuf) {
                                byteListInbuf.add(bytesTemp3);     //放入byteListInbuf链表的帧shiw

                            }
                            buffer.discardReadBytes();   //将取出来的这一帧数据在buffer的内存进行清除,释放内存

                    }else{       //第一个字节是0xbe,后三个字节不是0xbebebe情况
                            buffer.resetReaderIndex();   //指针回滚,回滚到只是取出第一个数
                            continue;
                        }
                }
            }
        }

猜你喜欢

转载自blog.csdn.net/andyzhu_2005/article/details/78816142