Netty自定义编-解码器解决TCP通讯粘包拆包的问题

1. TCP 粘包和拆包基本介绍

  1. TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
  2. 由于 TCP 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图
  3. 示意图 TCP 粘包、拆包图解

对图的说明: 假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包
  2. 服务端一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包
  3. 服务端分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这称之为 TCP 拆包
  4. 服务端分两次读取到了数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余部分内容 D1_2 和完整的 D2 包。

2. TCP 粘包和拆包解决方案

  1. 使用自定义协议+编解码器来解决
  2. 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的 TCP 粘包、拆包。

3. 看一个具体的实例

这是一个真实的案例,使我们公司开发的协议,我们在和充电桩进行通讯的时候,协议报文格式长这样:

 

 报文说明:

 报文里面有起始域域和长度域,我们可以先判断前两个字节是不是AAF5,再取3.4字节获取包长度,最后按照包长度取定长的数据

解决问题之前首先说明一个问题:

首先,我自定义了一个非常简单的解码器,其实并不具备解码功能,目的就是为了证实我的一个猜测:

public class DecodeHandler extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        System.out.println(byteBuf.writerIndex());
    }
}

在解码器中,输出的是写索引的位置。

然后,我开始尝试触发解码器,发现当我不断向byteBuf中写入内容后,写索引也不断增长,我写入两字节,写索引就增大2,写入三字节,写索引就增加3。由此,我猜测每次接收数据准备进行解码的bytebuf都是同一个!而不是新建的bytebuf,所以我们就可以使用这个bytebuf来实现粘包分包问题!

确定了读索引的位置就比较好办了,接下来的解析方式就看数据的具体格式了,在解析之前有必要检测一下数据长度是否完整,如果不完整,可以选择跳过这一波解析,等待数据接收完整再解析(记得要将读索引恢复到正确的位置)

大部分协议中,数据是有开头标识和长度域的,比如此协议。那么可以先找到数据的起始值和长度域在哪里。

具体我们看代码,本部分代码在解码器里实现:

/**
 * 解码器
 */
public class DecodeUtil extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) {
        try {
            //byteBuf的长度
            int bufNum = byteBuf.readableBytes();
            //byteBuf当前的读索引
            int readerIndex = byteBuf.readerIndex();
            byte[] bytes = new byte[2];
            if (bufNum >= 4) {   //byteBuf的长度大于4,
               //查看前两个字节判断消息头
                for (int index = 0; index < 2; index++) {
                    bytes[index] = byteBuf.getByte(readerIndex);
                    readerIndex++;
                }
                //将前2个字节转换为16进制
                String header = ConvertCode.receiveHexToString(bytes);
                int length = 0;
                if (header.toUpperCase().equals("AAF5")) {
                    //获取包长度
                    bytes = new byte[2];
                    bytes[0] = byteBuf.getByte(2);
                    bytes[1] = byteBuf.getByte(3);
                    length = ConvertCode.getShort(bytes, 0);
                } else {
                    return;
                }
                if (bufNum >= length) {
                    bytes = new byte[length];
                    byteBuf.readBytes(bytes);
                    list.add(bytes);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

工具类

public class ConvertCode {
    /**
     * @Title:bytes2HexString
     * @Description:字节数组转16进制字符串
     * @param b
     *            字节数组
     * @return 16进制字符串
     * @throws
     */
    public static String bytes2HexString(byte[] b) {
        StringBuffer result = new StringBuffer();
        String hex;
        for (int i = 0; i < b.length; i++) {
            hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            result.append(hex.toUpperCase());
        }
        return result.toString();
    }
    /**
     * @Title:hexString2Bytes
     * @Description:16进制字符串转字节数组
     * @param src  16进制字符串
     * @return 字节数组
     */
    public static byte[] hexString2Bytes(String src) {
        int l = src.length() / 2;
        byte[] ret = new byte[l];
        for (int i = 0; i < l; i++) {
            ret[i] = (byte) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();
        }
        return ret;
    }
    /**
     * @Title:string2HexString
     * @Description:字符串转16进制字符串
     * @param strPart  字符串
     * @return 16进制字符串
     */
    public static String string2HexString(String strPart) {
        StringBuffer hexString = new StringBuffer();
        for (int i = 0; i < strPart.length(); i++) {
            int ch = (int) strPart.charAt(i);
            String strHex = Integer.toHexString(ch);
            hexString.append(strHex);
        }
        return hexString.toString();
    }
    /**
     * @Title:hexString2String
     * @Description:16进制字符串转字符串
     * @param src
     *            16进制字符串
     * @return 字节数组
     * @throws
     */
    public static String hexString2String(String src) {
        String temp = "";
        for (int i = 0; i < src.length() / 2; i++) {
            //System.out.println(Integer.valueOf(src.substring(i * 2, i * 2 + 2),16).byteValue());
            temp = temp+ (char)Integer.valueOf(src.substring(i * 2, i * 2 + 2),16).byteValue();
        }
        return temp;
    }

    /**
     * @Title:char2Byte
     * @Description:字符转成字节数据char-->integer-->byte
     * @param src
     * @return
     * @throws
     */
    public static Byte char2Byte(Character src) {
        return Integer.valueOf((int)src).byteValue();
    }

    /**
     * @Title:intToHexString
     * @Description:10进制数字转成16进制
     * @param a 转化数据
     * @param len 占用字节数
     * @return
     * @throws
     */
    public static String intToHexString(int a,int len){
        len<<=1;
        String hexString = Integer.toHexString(a);
        int b = len -hexString.length();
        if(b>0){
            for(int i=0;i<b;i++)  {
                hexString = "0" + hexString;
            }
        }
        return hexString;
    }


    /**
     * 将16进制的2个字符串进行异或运算
     * http://blog.csdn.net/acrambler/article/details/45743157
     * @param strHex_X
     * @param strHex_Y
     * 注意:此方法是针对一个十六进制字符串一字节之间的异或运算,如对十五字节的十六进制字符串异或运算:1312f70f900168d900007df57b4884
    先进行拆分:13 12 f7 0f 90 01 68 d9 00 00 7d f5 7b 48 84
    13 xor 12-->1
    1 xor f7-->f6
    f6 xor 0f-->f9
    ....
    62 xor 84-->e6
    即,得到的一字节校验码为:e6
     * @return
     */
    public static String xor(String strHex_X,String strHex_Y){
        //将x、y转成二进制形式
        String anotherBinary=Integer.toBinaryString(Integer.valueOf(strHex_X,16));
        String thisBinary=Integer.toBinaryString(Integer.valueOf(strHex_Y,16));
        String result = "";
        //判断是否为8位二进制,否则左补零
        if(anotherBinary.length() != 8){
            for (int i = anotherBinary.length(); i <8; i++) {
                anotherBinary = "0"+anotherBinary;
            }
        }
        if(thisBinary.length() != 8){
            for (int i = thisBinary.length(); i <8; i++) {
                thisBinary = "0"+thisBinary;
            }
        }
        //异或运算
        for(int i=0;i<anotherBinary.length();i++){
            //如果相同位置数相同,则补0,否则补1
            if(thisBinary.charAt(i)==anotherBinary.charAt(i))
                result+="0";
            else{
                result+="1";
            }
        }
        return Integer.toHexString(Integer.parseInt(result, 2));
    }


    /**
     *  Convert byte[] to hex string.这里我们可以将byte转换成int
     * @param src byte[] data
     * @return hex string
     */
    public static String bytes2Str(byte[] src){
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }
    /**
     * @return 接收字节数据并转为16进制字符串
     */
    public static String receiveHexToString(byte[] by) {
        try {
            /*io.netty.buffer.WrappedByteBuf buf = (WrappedByteBuf)msg;
            ByteBufInputStream is = new ByteBufInputStream(buf);
            byte[] by = input2byte(is);*/
            String str = bytes2Str(by);
            str = str.toUpperCase();
            return str;
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("接收字节数据并转为16进制字符串异常");
        }
        return null;
    }



    /**
     * "7dd",4,'0'==>"07dd"
     * @param input 需要补位的字符串
     * @param size 补位后的最终长度
     * @param symbol 按symol补充 如'0'
     * @return
     * N_TimeCheck中用到了
     */
    public static String fill(String input, int size, char symbol) {
        while (input.length() < size) {
            input = symbol + input;
        }
        return input;
    }
//    public static void main(String args[]) {
//        String productNo = "3030303032383838";
//        System.out.println(hexString2String(productNo));
//        productNo = "04050103000001070302050304";
//        System.out.println(hexString2String(productNo));
//    }

    /**
     * 获取short,小端
     *
     * @param src
     * @param index
     * @return
     */
    public static short getShort(byte[] src, int index) {
        return (short) (((src[index + 1] << 8) | src[index] & 0xff));
    }



}

我们用真实的报文来验证处理的结果:

完整的报文:AAF56E0010026A0000000000363130313133303032373030303031000000000000000000000000000000000000A851000001006400000001010000010A02ED0B000020210414180000FF00000000000000000000000000000000000000000000000000000035C901000000000024

1.首先模拟完整报文发送100次

 

 服务端接收解码后的结果:没有粘包

 

2.模拟1包完整报文+半包报文,再发后半包报文

 

 

 

 至此,完美解决粘包问题,其他情况各位可以自己模拟!

posted @ 2021-05-18 10:50  william_zhao  阅读(1690)  评论(0编辑  收藏  举报