Netty之常见解码器分析
上一篇文章中我们简单的介绍了最顶层解码的流程,并且我们知道真正实现解码逻辑的是它的子类,那么,这节我们就将介绍集中解码器的分析
1、基于固定长度的解码器
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
private final int frameLength;
...
}
在这个解码器里面只有一个成员变量frameLength,用来表示固定长度的大小
A decoder that splits the received {@link ByteBuf}s by the fixed number
-
of bytes. For example, if you received the following four fragmented packets:
解析前 -
±–±—±-----±—+
-
| A | BC | DEFG | HI |
-
±–±—±-----±—+
解析后 -
±----±----±----+
-
| ABC | DEF | GHI |
-
±----±----±----+
比如我们传进来一个字符串如上面解析前的,由于我们将固定长度设定为3,所以解析的时候会以3个为一组进行解析。
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//解析对象
Object decoded = decode(ctx, in);
if (decoded != null) {
//将解析的对象放到容器里面
out.add(decoded);
}
}
它的核心方法就是上面的decode,首先会调用解析的方法返回一个解析出来的对象,如果这个对象不是null,就将他放到容器里面。
protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//判断当前可读字节是否小于我们的固定长度
if (in.readableBytes() < frameLength) {
return null;
} else {
return in.readRetainedSlice(frameLength);
}
}
这个方法首先会判断当前可读字节是否小于我们的固定长度,如果小于,说明当前可读字节并不足以解析出来一个对象就直接返回null。反之,就会真正去解析对象。
2、基于行的解码器
public class LineBasedFrameDecoder extends ByteToMessageDecoder {
/** Maximum length of a frame we're willing to decode. */
private final int maxLength;
/** Whether or not to throw an exception as soon as we exceed maxLength. */
private final boolean failFast;
private final boolean stripDelimiter;
/** True if we're discarding input because we're already over maxLength. */
private boolean discarding;
private int discardedBytes;
}
maxLength表示当前解码器可以解码的最大长度。
failFast表示如果超过了这个最大长度是否马上抛出异常
stripDelimiter表示是否带着换行符一起解析
discarding表示当前解析器处于丢弃模式
discardedBytes表示已经丢弃的字节
下面我们着重来看一下这个解码器的decode方法:
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
//找到下一个换行符
final int eol = findEndOfLine(buffer);
//当前处于非丢弃模式
if (!discarding) {
//找到了=换行符
if (eol >= 0) {
final ByteBuf frame;
//得到从当前的读指针到下一个换行符之间有多少个字节
final int length = eol - buffer.readerIndex();
//得到换行符的长度
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
//如果需要读取的长度超过了最大的长度,就将当前的读指针移动到分隔符之后的那个字节上,丢弃这部分内容,然后抛出一个异常,返回一个空对象
if (length > maxLength) {
buffer.readerIndex(eol + delimLength);
fail(ctx, length);
return null;
}
//根据是否考虑解析分隔符来分割要读的字节
if (stripDelimiter) {
frame = buffer.readRetainedSlice(length);
buffer.skipBytes(delimLength);
} else {
frame = buffer.readRetainedSlice(length + delimLength);
}
返回
return frame;
} else {
//如果没有找到分隔符,那么就获得全部可以读的字节的数目
final int length = buffer.readableBytes();
//如果获取到的长度超过了我们最大的长度
if (length > maxLength) {
//那么就需要把这部分内容的长度记录下来
discardedBytes = length;
//将当前的读指针移动到写指针上
buffer.readerIndex(buffer.writerIndex());
//修改当前的默认为丢弃模式
discarding = true;
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
//如果长度没有超过最大长度说明数据不完整,等待一下补充一点数据后在进行解析
return null;
}
} else {
//当前处于丢弃模式
if (eol >= 0) {
//获得要解析的字符串的长度
final int length = discardedBytes + eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
//将当前读指针移动到分隔符的后面的第一个字节
buffer.readerIndex(eol + delimLength);
discardedBytes = 0;
//修改为非丢弃模式
discarding = false;
if (!failFast) {
fail(ctx, length);
}
} else {
//如果还是没有找到分隔符,继续将读指针移动到写指针的位置上
discardedBytes += buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());
}
return null;
}
}
这部分逻辑还是挺简单的,但有个地方就是由非丢弃模式转向丢弃模式的时候,我们在这里解释一下:由于在非丢弃模式下,我们没有找到分隔符,并且长度已经大于了最大长度,这部分内容肯定是要丢弃的,那么下一次数据来的时候,第一个分隔符之前的内容已经不完整,这部分不完的内容也要丢弃,所以要修改为丢弃模式,下次数据来了,直接进行丢弃处理。
3、基于分隔符的解码器
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
private final ByteBuf[] delimiters;
private final int maxFrameLength;
private final boolean stripDelimiter;
private final boolean failFast;
private boolean discardingTooLongFrame;
private int tooLongFrameLength;
/** Set only when decoding with "\n" and "\r\n" as the delimiter. */
private final LineBasedFrameDecoder lineBasedDecoder;
}
我们看他的成员变量有这么几个:
delimiters:这个数组是用来存储分隔符的,说明我们可以向构造器中传多个分隔符
maxFrameLength:这个解码器能够解析的最大长度
stripDelimiter:是否将分隔符一起解析
failFast:是否立即抛出异常
discardingTooLongFrame:是否处于丢弃模式
tooLongFrameLength:丢弃的长度
lineBasedDecoder:当分隔符是按行的时候,就直接使用行解码器了
下面我们看一下它的decode方法是怎么处理的:
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
//如果分隔符是行分隔符,那么就直接使用行解码器进行解析
if (lineBasedDecoder != null) {
return lineBasedDecoder.decode(ctx, buffer);
}
// Try all delimiters and choose the delimiter which yields the shortest frame.
int minFrameLength = Integer.MAX_VALUE;
ByteBuf minDelim = null;
//找到一个使截取后的数据段最小的分隔符
for (ByteBuf delim: delimiters) {
int frameLength = indexOf(buffer, delim);
if (frameLength >= 0 && frameLength < minFrameLength) {
minFrameLength = frameLength;
minDelim = delim;
}
}
//如果找到了分割符
if (minDelim != null) {
//得到分隔符的长度
int minDelimLength = minDelim.capacity();
ByteBuf frame;
//如果现在处于丢弃状态下
if (discardingTooLongFrame) {
// 首先修改成非丢弃状态
discardingTooLongFrame = false;
//使读指针越过这之间的数据
buffer.skipBytes(minFrameLength + minDelimLength);
//得到已经掠过的长度
int tooLongFrameLength = this.tooLongFrameLength;
this.tooLongFrameLength = 0;
if (!failFast) {
fail(tooLongFrameLength);
}
return null;
}
//处于非丢弃状态下,但是当前所获得的数据的长度大与可以解析的最大长度
if (minFrameLength > maxFrameLength) {
// 直接略过这段数据
buffer.skipBytes(minFrameLength + minDelimLength);
fail(minFrameLength);
return null;
}
//是否考虑解析分隔符的解析
if (stripDelimiter) {
frame = buffer.readRetainedSlice(minFrameLength);
buffer.skipBytes(minDelimLength);
} else {
frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
}
return frame;
} else {
//没有找到分隔符,并且处于非丢弃状态下
if (!discardingTooLongFrame) {
//如果当前可读长度大于最大解析长度
if (buffer.readableBytes() > maxFrameLength) {
// 记录被丢弃的字节长度
tooLongFrameLength = buffer.readableBytes();
//将读指针移动到写指针的位置上
buffer.skipBytes(buffer.readableBytes());
//修改为丢弃模式
discardingTooLongFrame = true;
if (failFast) {
fail(tooLongFrameLength);
}
}
} else {
// 处于丢弃模式下直接将这段数据丢弃
tooLongFrameLength += buffer.readableBytes();
buffer.skipBytes(buffer.readableBytes());
}
return null;
}
}
逻辑大致上和行解码器差不多,只不过多了一步找一个最近的分隔符的操作。
3、基于长度域的解码器
我们首先看这两个重要的参数,第一个参数的意思是我们的长度域在数据流中的偏移量是多少,第二个参数表示我们的长度域是由几个字节表示的十进制数,而这个十进制数就表示我们的内容是接下来的几个字节,这里面还有两个比较重要的参数:
lengthAdjustment:需要再向后读几个字节
initialBytesToStrip:略过前面几个字节
我们接下来还是看他的decode方法:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//如果当前为丢弃模式
if (discardingTooLongFrame) {
//获得需要丢弃的字节数目
long bytesToDiscard = this.bytesToDiscard;
//从可读字节和还需要丢弃的字节长读中选择一个最小的
int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
//略过这部分
in.skipBytes(localBytesToDiscard);
//去掉丢弃的长度
bytesToDiscard -= localBytesToDiscard;
this.bytesToDiscard = bytesToDiscard;
failIfNecessary(false);
}
//如果可读字节的长度小于我们的长度域偏移量+长度域的长度,说明数据缺少,继续读
if (in.readableBytes() < lengthFieldEndOffset) {
return null;
}
//得到真正的可读数据的位置,就是当前读指针的位置+长度域的偏移量
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
//得到一个完整的数据帧的长度:数据的长度
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);
//如果这个长度为0,需要抛出异常
if (frameLength < 0) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException(
"negative pre-adjustment length field: " + frameLength);
}
//得到一个最终的数据的长度:长度域偏移量+长度域的长度+长度域确定的长度+需要调整的长度
frameLength += lengthAdjustment + lengthFieldEndOffset;
//如果获得的需要读的数据小于长度域+偏移量的长度,肯定有问题,需要抛出异常
if (frameLength < lengthFieldEndOffset) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException(
"Adjusted frame length (" + frameLength + ") is less " +
"than lengthFieldEndOffset: " + lengthFieldEndOffset);
}
//如果需要读的长度大于了最大的长度,需要将这些丢弃
if (frameLength > maxFrameLength) {
//去掉frameLength后,获得还需要丢弃多少字节
long discard = frameLength - in.readableBytes();
tooLongFrameLength = frameLength;
//如果小于0,说明需要丢弃的字节全部位于当前可读字节内部,直接略过这部分就好了
if (discard < 0) {
// buffer contains more bytes then the frameLength so we can discard all now
in.skipBytes((int) frameLength);
} else {
//否则当前可读字节不够丢弃的,需要从下一次读完后继续丢弃,将当前解码器的模式修改为丢弃模式,并且略过当前可读字节
discardingTooLongFrame = true;
bytesToDiscard = discard;
in.skipBytes(in.readableBytes());
}
failIfNecessary(true);
return null;
}
// 最终要读的字节数小于最大长度,说明是合法的,但是当前可读字节小于需要读的字节长度,那么不处理,等待下一次解码的时候处理
int frameLengthInt = (int) frameLength;
if (in.readableBytes() < frameLengthInt) {
return null;
}
//如果略过的字节大于了需要读的字节,那么抛出异常
if (initialBytesToStrip > frameLengthInt) {
in.skipBytes(frameLengthInt);
throw new CorruptedFrameException(
"Adjusted frame length (" + frameLength + ") is less " +
"than initialBytesToStrip: " + initialBytesToStrip);
}
//否则从头部略过指定的长度
in.skipBytes(initialBytesToStrip);
int readerIndex = in.readerIndex();
//得到最终需要读的数据的长度
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);
return frame;
}
这个方法做了这么几件事:
1、计算需要抽取的数据包的长度
2、跳过字节逻辑处理
3、丢弃模式下的处理
我们常见的几个解码器就介绍完了,让我们回顾一下解码的步骤结束这部分内容:
1、累加字节流
2、调用子类的decode方法进行解析
3、将解析得到ByteBuf向下传播