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之数据解码的主要内容,如果未能解决你的问题,请参考以下文章