粘包半包问题
1.TCP层传递的字节数据包是没有任何含义的,不清楚哪写字节需要组装在一起是有意义的
2.在我们程序中,一般是需要将一部分字节数组当作一个完整包进行反序列化的,如果多一个
字节或者少一个字节都会反序列失败
3.我们可以做一个标记,比如以一个换行符作为一个反序列单元,那么就可以正常取出我们
需要的数据包,LineBasedFrameDecoder这个处理器就是这个原理,使用方式很简单,只要
在发送一个反序列化的字节数组后,后面加上换行符即可
LineBasedFrameDecoder解决粘包原理图
LineBasedFrameDecoder解决粘包半包源码
//LineBasedFrameDecoder逻辑是根据换行符来进行拆包的,认为一个换行符前是一个完整包
//ByteToMessageDecoder的channelRead方法
//LineBasedFrameDecoder继承了ByteToMessageDecoder
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
//1.对上个拦截器过来的数据进行拆包
//2.查找到一个换行符,那么是认为是一个完整的字节包,那么添加到out中
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
//将out中完整的包,交给下一个拦截器
//如果只是半包,那么out里面是空,不会触发下一个处理器
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
//ByteToMessageDecoder的callDecode方法
//主要作用是以换行符切割性读取Buff里的数据,所以如果有粘包,那么会读取多次
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
//处理当一个完整包,将完整包,交给下一个处理器处理
if (outSize > 0) {
//将完整包交给下一个处理
fireChannelRead(ctx, out, outSize);
out.clear();
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
//读取到换行符就会停止读取,进度下一轮
decodeRemovalReentryProtection(ctx, in, out);
//省去.....
}
} catch (DecoderException e) {
throw e;
} catch (Throwable cause) {
throw new DecoderException(cause);
}
}
//ByteToMessageDecoder的fireChannelRead方法
//将一个分解的完整包,交给下一个处理器处理
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
}
//decodeRemovalReentryProtection方法内部调用
//LineBasedFrameDecoder的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;
offset = 0;
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
return null;
}
}
}