Netty之数据解码

Posted 复姓江山

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty之数据解码相关的知识,希望对你有一定的参考价值。

一、概况

   作为Java世界使用最广泛的网络通信框架Netty,其性能和效率是有目共睹的,好多大公司都在使用如苹果、谷歌、Facebook、Twitter、阿里巴巴等,所以不仅仅是因为Netty有高效的性能与效率,更重要的是:屏蔽了底层的复杂度,简单易懂的编程模型,适应更广泛的应用场景,以及活跃的开发者社区。
  本篇博客是作为Netty之数据编码的续篇,上一篇以抛砖引玉的方式讲解了怎么使用Netty的核心缓冲区ByteBuf怎么编码存储各种基本数据,本篇就是与之对应的怎么从缓冲区ByteBuf中的编码数据解码出来,因为我们的Java代码中处理数据一般不是按照字节流来处理,所以需要解码恢复出数据然后再进行处理。

二、代码实现

1. 解码工具类

 package com.ethan.cws.common.utils;
 
 import com.ethan.cws.common.enums.TypeEnum;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.util.CharsetUtil;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 /**
  * 解码工具类
  *
  * @author ethancws
  * @date 
  */
 public final class DecodeUtils 
     
     /**
      * FEP data数据文件后缀名
      */
     public final static String FILE_SUFFIX_EXTEND = ".xml";
     
     /**
      * 文件名
      */
     public final static String FILE_NAME = "Filename";
 
     private DecodeUtils() 
 
     
 
     /**
      * 解码
      *
      * @param symbol  符号
      * @param byteNum 字节数
      * @param buff    数据
      * @param type    枚举类型字符串
      * @param endian  编码
      * @return 解码数据
      */
     public static Object decode(String symbol, int byteNum, ByteBuf buff,
                                 String type, boolean endian) 
         Object value = null;
         //类型枚举
         final TypeEnum typeEnum = TypeEnum.match(type);
         switch (typeEnum) 
             case TYPE_STRING:
             case TYPE_ENUM_STRING:
             case TYPE_DATE_STRING:
                 value = readString(byteNum, buff, symbol);
                 break;
             case TYPE_HEX_STRING:
             case TYPE_ENUM_HEX_STRING:
                 value = readHexString(byteNum, buff);
                 break;
             case TYPE_USHORT:
                 value = readUnSignShort(buff, endian);
                 break;
             case TYPE_SHORT:
                 value = readShort(buff, endian);
                 break;
             case TYPE_INT:
             case TYPE_ENUM_INT:
                 value = readInt(buff, endian);
                 break;
             case TYPE_UINT:
                 value = readUnSignInt(buff, endian);
                 break;
             case TYPE_BYTE:
             case TYPE_ENUM_BYTE:
                 value = readByte(buff);
                 break;
             case TYPE_UBYTE:
                 value = readUnSignByte(buff);
                 break;
             case TYPE_BIT:
                 value = readBit(byteNum, buff);
                 break;
             case TYPE_MULTI_BIT:
                 value = readMultiBit(byteNum, buff);
                 break;
             case TYPE_BCD8421:
                 value = readBcd8421(byteNum, buff);
                 break;
 
         
 
         return value;
     
 
     /**
      * 读无符号byte
      *
      * @param buff 编码数据
      * @return 解码数据
      */
     public static short readUnSignByte(ByteBuf buff) 
         byte by = buff.readByte();
         return (short) (by & 0x0FF);
     
 
     /**
      * 读byte
      *
      * @param buff 编码数据
      * @return 解码数据
      */
     public static byte readByte(ByteBuf buff) 
         return buff.readByte();
     
 
     /**
      * 读无符号int
      *
      * @param buff   编码数据
      * @param endian 字节序
      * @return 解码数据
      */
     public static long readUnSignInt(ByteBuf buff, boolean endian) 
         int intValue = endian ? buff.readIntLE() : buff.readInt();
         return intValue & 0x0FFFFFFFFL;
     
 
     /**
      * 读int
      *
      * @param buff   编码数据
      * @param endian 字节序
      * @return 解码数据
      */
     public static int readInt(ByteBuf buff, boolean endian) 
         return endian ? buff.readIntLE() : buff.readInt();
     
 
     /**
      * 读short
      *
      * @param buff   编码数据
      * @param endian 字节序
      * @return 解码数据
      */
     public static short readShort(ByteBuf buff, boolean endian) 
         return endian ? buff.readShortLE() : buff.readShort();
     
 
     /**
      * 读无符号short
      *
      * @param buff   编码数据
      * @param endian 字节序
      * @return 解码数据
      */
     public static int readUnSignShort(ByteBuf buff, boolean endian) 
         short shortValue = endian ? buff.readShortLE() : buff.readShort();
         return shortValue & 0x0FFFF;
     
 
     /**
      * 读Hex字符串
      *
      * @param num  字节长度
      * @param buff 编码数据
      * @return 字符串
      */
     public static String readHexString(int num, ByteBuf buff) 
         String value = ByteBufUtil.hexDump(buff, 0, num);
         readByteBuf(num, buff);
         return value;
     
 
     /**
      * 读Hex字符串没有数据缓冲区偏移
      *
      * @param num  字节长度
      * @param buff 编码数据
      * @return 字符串
      */
     public static String readHexStringWithoutOffset(int num, ByteBuf buff) 
         return ByteBufUtil.hexDump(buff, 0, num);
     
 
     /**
      * 获取文件名称
      *
      * @param fileName 字符
      * @return 文件名称
      */
     private static String acquireFileName(String fileName) 
         String fileSuffixExtend = FILE_SUFFIX_EXTEND;
         int index = fileName.lastIndexOf(fileSuffixExtend);
         index += fileSuffixExtend.length();
         fileName = fileName.substring(1, index);
         return fileName;
     
 
     /**
      * 读字符串
      *
      * @param num    字节长度
      * @param buff   编码数据
      * @param symbol 编码标识
      * @return 字符串
      */
     public static String readString(int num, ByteBuf buff, String symbol) 
         final CharSequence charSequence = buff.getCharSequence(0, num, CharsetUtil.UTF_8);
         String value = charSequence.toString();
         if (FILE_NAME.equals(symbol)) 
             value = acquireFileName(value);
         
         //移动读指针
         readByteBuf(num, buff);
         return value;
     
 
 
     /**
      * 移动读指针
      *
      * @param num  移动字节数
      * @param buff 数据缓冲区ByteBuf
      */
     private static void readByteBuf(int num, ByteBuf buff) 
         assert num >= 1;
         if (num == 1) 
             buff.readByte();
          else 
             buff.readBytes(num);
         
     
 
     /**
      * 读bit
      *
      * @param num  字节长度
      * @param buff 数据缓冲区ByteBuf
      * @return bit位索引
      */
     public static int readBit(int num, ByteBuf buff) 
         ByteBuf buffCopy = buff.copy(0, num);
         int index = 0;
         for (; num > 0; num--) 
             byte b = buffCopy.readByte();
             if (b != 0) 
                 index += b / 2;
                 --num;
                 break;
             
         
         index += num * 8;
         //移动读指针
         readByteBuf(num, buff);
         return index;
     
 
     /**
      * 读多位bit
      *
      * @param num  字节长度
      * @param buff 数据缓冲区ByteBuf
      * @return 二进制数据为1的索引数组
      */
     public static int[] readMultiBit(int num, ByteBuf buff) 
         ByteBuf buffCopy = buff.copy(0, num);
         List<Integer> list = new ArrayList<>();
         int size = num;
         final int fixedNum = num;
         for (; num > 0; num--) 
             size--;
             int b = readUnSignByte(buffCopy);
             if (b != 0) 
                 String str = Integer.toBinaryString(b);
                 str = fullFillByteString(str);
                 gatherIndexes(str, size, list);
             
         
         //移动读指针
         readByteBuf(fixedNum, buff);
         return Arrays.stream(list.toArray(new Integer[0])).mapToInt(Integer::valueOf).toArray();
     
 
     /**
      * 补全byte二进制8位字符串
      *
      * @param str 字符串
      * @return 补全8位后的字符串
      */
     private static String fullFillByteString(String str) 
         int len = 8;
         int length = str.length();
         if (length < 8) 
             StringBuilder strBuilder = new StringBuilder(str);
             for (int i = 0; i < len - length; i++) 
                 strBuilder.insert(0, "0");
             
             str = strBuilder.toString();
         
         return str;
     
 
     /**
      * 收集索引存入List
      *
      * @param str  byte二进制字符串
      * @param size 剩余byte长度
      * @param list 集合List
      */
     private static void gatherIndexes(String str, int size, List<Integer> list) 
         int len = 8, lenFixed = 8;
         for (char ch : str.toCharArray()) 
             int totalIndex = 0;
             len--;
             if (ch == 48) 
                 continue;
             
             totalIndex = len + size * lenFixed;
             list.add(totalIndex);
         
     
 
     /**
      * 读Bcd码
      *
      * @param num  字节长度
      * @param buff 数据缓冲区ByteBuf
      * @return Bcd码解码数据
      */
     public static String readBcd8421(int num, ByteBuf buff) 
         return readHexString(num, buff);
     
 

 

2. 数据类型枚举类

 package com.ethan.cws.common.enums;
 
 /**
  * 数据枚举
  *
  * @author ethancws
  * @date 
  */
 public enum TypeEnum 
     /**
      * 字符串
      */
     TYPE_STRING("string"),
 
     /**
      * Binary-Coded Decimal
      * bcd码 8421码
      * 4位二进制数表示1位十进制数
      */
     TYPE_BCD8421("bcd8421"),
     /**
      * 时间字符串
      */
     TYPE_DATE_STRING("date_string"),
     /**
      * 枚举byte
      */
     TYPE_ENUM_BYTE("enum|byte"),
 
     /**
      * 枚举int
      */
     TYPE_ENUM_INT("enum|int"),
 
     /**
      * 枚举字符串
      */
     TYPE_ENUM_STRING("enum|string"),
 
     /**
      * 枚举HEX字符串
      */
     TYPE_ENUM_HEX_STRING("enum|hex_string"),
 
     /**
      * HEX字符串
      */
     TYPE_HEX_STRING("hex_string"),
 
     /**
      * -2^31~2^31-1
      * -2,147,483,648~2,147,483,647
      */
     TYPE_INT("int"),
     /**
      * 0~2^32
      * 0~4294967296L
      */
     TYPE_UINT("uint"),
     /**
      * -2^15~2^15-1
      * -32768~32767
      */
     TYPE_SHORT("short"),
     /**
      * 0~65535
      */
     TYPE_USHORT("ushort"),
     /**
      * -2^7~2^7-1
      * -128~127
      */
     TYPE_BYTE("byte"),
 
     /**
      * 0~256
      */
     TYPE_UBYTE("ubyte"),
 
     /**
      * 多位同选
      */
     TYPE_MULTI_BIT("multi_bit"),
     /**
      * 位
      */
     TYPE_BIT("bit");
 
     private String val;
 
     TypeEnum(String val) 
         this.val = val;
     
 
 
     /**
      * 字符串匹配枚举类型
      *
      * @param value 字符串
      * @return 对应枚举
      */
     public static TypeEnum match(String value) 
         String str = "TYPE_";
         if (value.indexOf("|") > 0) 
             value = value.replace("|", "_");
         
         str += value.toUpperCase();
         return valueOf(str);
     
 
 
 

三、后记

  随着对于Netty的理解和使用的深入,越来越对于Netty框架的痴迷,所以后面会不定期的更新Netty相关的使用与心得。欢迎与大家一起探讨一起学习。

netty系列之:netty中的frame解码器

简介

netty中的数据是通过ByteBuf来进行传输的,一个ByteBuf中可能包含多个有意义的数据,这些数据可以被称作frame,也就是说一个ByteBuf中可以包含多个Frame。

对于消息的接收方来说,接收到了ByteBuf,还需要从ByteBuf中解析出有用而数据,那就需要将ByteBuf中的frame进行拆分和解析。

一般来说不同的frame之间会有有些特定的分隔符,我们可以通过这些分隔符来区分frame,从而实现对数据的解析。

netty为我们提供了一些合适的frame解码器,通过使用这些frame解码器可以有效的简化我们的工作。下图是netty中常见的几个frame解码器:

<img src="https://img-blog.csdnimg.cn/6f394018c43a40a6a53a5260fc577575.png" style="zoom:67%;" />

接下来我们来详细介绍一下上面几个frame解码器的使用。

LineBasedFrameDecoder

LineBasedFrameDecoder从名字上看就是按行来进行frame的区分。根据操作系统的不同,换行可以有两种换行符,分别是 "\\n" 和 "\\r\\n" 。

LineBasedFrameDecoder的基本原理就是从ByteBuf中读取对应的字符来和"\\n" 跟 "\\r\\n",可以了可以准确的进行字符的比较,这些frameDecoder对字符的编码也会有一定的要求,一般来说是需要UTF-8编码。因为在这样的编码中,"\\n"和"\\r"是以一个byte出现的,并且不会用在其他的组合编码中,所以用"\\n"和"\\r"来进行判断是非常安全的。

LineBasedFrameDecoder中有几个比较重要的属性,一个是maxLength的属性,用来检测接收到的消息长度,如果超出了长度限制,则会抛出TooLongFrameException异常。

还有一个stripDelimiter属性,用来判断是否需要将delimiter过滤掉。

还有一个是failFast,如果该值为true,那么不管frame是否读取完成,只要frame的长度超出了maxFrameLength,就会抛出TooLongFrameException。如果该值为false,那么TooLongFrameException会在整个frame完全读取之后再抛出。

LineBasedFrameDecoder的核心逻辑是先找到行的分隔符的位置,然后根据这个位置读取到对应的frame信息,这里来看一下找到行分隔符的findEndOfLine方法:

    private int findEndOfLine(final ByteBuf buffer) 
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) 
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == \\r) 
                i--;
            
         else 
            offset = totalLength;
        
        return i;
    

这里使用了一个ByteBuf的forEachByte对ByteBuf进行遍历。我们要找的字符是:ByteProcessor.FIND_LF。

最后LineBasedFrameDecoder解码之后的对象还是一个ByteBuf。

DelimiterBasedFrameDecoder

上面讲的LineBasedFrameDecoder只对行分隔符有效,如果我们的frame是以其他的分隔符来分割的话LineBasedFrameDecoder就用不了了,所以netty提供了一个更加通用的DelimiterBasedFrameDecoder,这个frameDecoder可以自定义delimiter:

public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder 

        public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) 
        this(maxFrameLength, true, delimiter);
    

传入的delimiter是一个ByteBuf,所以delimiter可能不止一个字符。

为了解决这个问题在DelimiterBasedFrameDecoder中定义了一个ByteBuf的数组:

    private final ByteBuf[] delimiters;

    delimiters= delimiter.readableBytes();

这个delimiters是通过调用delimiter的readableBytes得到的。

DelimiterBasedFrameDecoder的逻辑和LineBasedFrameDecoder差不多,都是通过对比bufer中的字符来对bufer中的数据进行截取,但是DelimiterBasedFrameDecoder可以接受多个delimiters,所以它的用处会根据广泛。

FixedLengthFrameDecoder

除了进行ByteBuf中字符比较来进行frame拆分之外,还有一些其他常见的frame拆分的方法,比如根据特定的长度来区分,netty提供了一种这样的decoder叫做FixedLengthFrameDecoder。

public class FixedLengthFrameDecoder extends ByteToMessageDecoder 

FixedLengthFrameDecoder也是继承自ByteToMessageDecoder,它的定义很简单,可以传入一个frame的长度:

    public FixedLengthFrameDecoder(int frameLength) 
        checkPositive(frameLength, "frameLength");
        this.frameLength = frameLength;
    

然后调用ByteBuf的readRetainedSlice方法来读取固定长度的数据:

in.readRetainedSlice(frameLength)

最后将读取到的数据返回。

LengthFieldBasedFrameDecoder

还有一些frame中包含了特定的长度字段,这个长度字段表示ByteBuf中有多少可读的数据,这样的frame叫做LengthFieldBasedFrame。

netty中也提供了一个对应的处理decoder:

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder 

读取的逻辑很简单,首先读取长度,然后再根据长度再读取数据。为了实现这个逻辑,LengthFieldBasedFrameDecoder提供了4个字段,分别是 lengthFieldOffset,lengthFieldLength,lengthAdjustment和initialBytesToStrip。

lengthFieldOffset指定了长度字段的开始位置,lengthFieldLength定义的是长度字段的长度,lengthAdjustment是对lengthFieldLength进行调整,initialBytesToStrip表示是否需要去掉长度字段。

听起来好像不太好理解,我们举几个例子,首先是最简单的:

   BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
   +--------+----------------+      +--------+----------------+
   | Length | Actual Content |----->| Length | Actual Content |
   | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
   +--------+----------------+      +--------+----------------+

要编码的消息有个长度字段,长度字段后面就是真实的数据,0x000C是一个十六进制,表示的数据是12,也就是"HELLO, WORLD" 中字符串的长度。

这里4个属性的值是:

   lengthFieldOffset   = 0
   lengthFieldLength   = 2
   lengthAdjustment    = 0
   initialBytesToStrip = 0 

表示的是长度字段从0开始,并且长度字段占有两个字节,长度不需要调整,也不需要对字段进行调整。

再来看一个比较复杂的例子,在这个例子中4个属性值如下:

   lengthFieldOffset   = 1  
   lengthFieldLength   = 2
   lengthAdjustment    = 1  
   initialBytesToStrip = 3  

对应的编码数据如下所示:

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
   +------+--------+------+----------------+      +------+----------------+
   | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
   | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
   +------+--------+------+----------------+      +------+----------------+

上面的例子中长度字段是从第1个字节开始的(第0个字节是HDR1),长度字段占有2个字节,长度再调整一个字节,最终数据的开始位置就是1+2+1=4,然后再截取前3个字节的数据,得到了最后的结果。

总结

netty提供的这几个基于字符集的frame decoder基本上能够满足我们日常的工作需求了。当然,如果你传输的是一些更加复杂的对象,那么可以考虑自定义编码和解码器。自定义的逻辑步骤和上面我们讲解的保持一致就行了。

以上是关于Netty之数据解码的主要内容,如果未能解决你的问题,请参考以下文章

netty系列之:netty中常用的xml编码解码器

netty系列之:netty中的核心解码器json

netty编解码之使用protobuf

netty系列之:netty中常用的对象编码解码器

netty系列之:netty中的懒人编码解码器

从0到1 ▏Netty编解码框架之多种常用解码器使用示例解析