• 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包完整报文+半包报文,再发后半包报文

     

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

  • 相关阅读:
    网络嗅探器
    struct udphdr
    struct tcphdr
    struct iphdr
    socket函数
    SQL SERVER-解析Extendevent文件数据
    SQL SERVER-日期按时区转换
    SQL SERVER-CROSS APPLY
    WinServer-文件共享端口
    SQL SERVER-修改实例的排序规则
  • 原文地址:https://www.cnblogs.com/wiliamzhao/p/14780007.html
Copyright © 2020-2023  润新知