粘包 和拆包
TCP是基于流传输的,而流是没有边界的,TCP相对于上层业务属于底层协议,对于TCP而言,并不关心上层的业务,所以TCP都在进行数据发送的时候只会按照自己发送的规则进行发送,导致的结果就是在业务上的一个完整的数据包可能会被TCP拆成多个包进行发送,这就是拆包,同时也有可能将业务上多个小数据包合成一个包进行发送,这就是粘包
如何解决粘包和拆包
对于粘包和拆包常用的解决方案由三种
1.固定消息和报文的长度
2.通过添加分隔符
3.单独传输一个消息长度的属性
ByteToMessageDecoder
ByteToMessageDecoder 在 netty 中是很多解码器的父类,但是这个抽象的解码器没有解决粘包拆包的问题,而是交给子类去解决
如果用户想自定义解码器只需要重写 decode()这个方法就好了
不论是编码器还是解码器,都是一个handler,所以我们来看看这个解码器的channelRead方法
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);
}
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
first = cumulation == null ,first 这个变量是什么意思呢?
其实不难猜出,就是去判断这是不是第一次从管道里面拿数据,再来看看cumulation,可以发现这个属性的类型是 netty 中的 ByteBuf,所以,netty 解决拆包的思路就是,将没有读完的数据存储到这个ByteBuf 里面,直到读完了才会进行数据的发送
如果是第一次读取就会将传入的数据直接进行赋值,如果不是第一次,就会将之前保存的数据和传入的数据进行累加,然后调用
cumulator.cumulate(ctx.alloc(), cumulation, data),在调用 callDecode(ctx, cumulation, out) 进行数据的解析
finally部分如果cumulation 不为null 并且没有可以读的字节,那么就会释放,fireChannelRead(ctx, out, size)在进行向下传播
首先来看一看cumulator这个对象到底是个啥玩意,是如何进行累加的
private Cumulator cumulator = MERGE_CUMULATOR;
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
try {
final ByteBuf buffer;
if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
|| cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
} else {
buffer = cumulation;
}
buffer.writeBytes(in);
return buffer;
} finally {
in.release();
}
}
};
这个if判断是一个扩容判断 cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
cumulation的写指针>cumulation.最大容量-in.的可读数据
也就是说 in的数据+comlation的数据 是否大于 cumlation的最大容量,如果大于,那么就是需要扩容,然后调用 buffer.writeBytes(in) 将in的数据写入 cumlation,最后将 in的数据释放
接下来就是调用callDecode() 这个方法
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
...
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);
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
...
}
参数:
ChannelHandlerContext ctx:客户端 channel
ByteBuf in:cumulation中保存的数据
List out:一个netty自定义的list集合
首先是判断当前 list 里面是否有对象,第一次的话一定是没有的,接着调用了一个非常重要的方法
decodeRemovalReentryProtection(ctx, in, out);在这个方法的里面调用的是 decode(ctx, in, out) ,这个方法就是前面说过的交给子类去实现的方法,这个方法做了什么事情后面举例分析
执行完decode(ctx, in, out) 后进行判断 ,就是判断解码前后这个集合的大小有没有发生改变,如果相同就说明没有解析到数据,又会进一步判断 oldInputLength == in.readableBytes(),解析前后的长度是不是相同,如果说长度都相同,说明当前数据解析玩了,那么就会break,等待下次解析;如果说长度不同,就说明解没有解析到足够数据,但是解析到了数据,就contiune,继续解析
netty提供的解码器
LineBasedFrameDecoder
LineBasedFrameDecoder是一种以换行符(\r\n)
对于LineBasedFrameDecoder的decode而言有两种模式,一是丢弃模式,二是非丢弃模式
非丢弃模式下
if (eol >= 0) {
final ByteBuf frame;
//算出本次要截取数据的长度
final int length = eol - buffer.readerIndex();
//判断\n前面是否是\r 是返回2 不是返回1
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
//如果读取的长度>指定的最大长度
if (length > maxLength) {
//设置此缓冲区的readerIndex。 跳过这段数据
buffer.readerIndex(eol + delimLength);
fail(ctx, length);
return null;
}
//判断解析的数据是否要带\r\n
if (stripDelimiter) {
//返回从当前readerIndex开始的缓冲区子区域的一个新保留的片
//返回到\r\n的有效数据 不包括\r\n
frame = buffer.readRetainedSlice(length);
//跳过\r\n
buffer.skipBytes(delimLength);
} else {
//截取到\r\n的有效数据 包括\r\n
frame = buffer.readRetainedSlice(length + delimLength);
}
return frame;
} else {
//如果没有找到\r\n
final int length = buffer.readableBytes();
//如果本次数据的可读长度》最大可读长度
if (length > maxLength) {
//设置丢弃的长度为本次buffer的可读取长度
discardedBytes = length;
//跳过本次数据
buffer.readerIndex(buffer.writerIndex());
//设置为丢弃模式
discarding = true;
offset = 0;
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
return null;
}
这里的eol返回的是 /r/n 的下标的位置,如果找到了下标就会进入if ,反之进入else
首先会获取本次读取数据的长度,然后 如果读取的长度>指定的最大长度,因为当前是非丢弃模式,就会抛出异常,
从而回调 exceptionCaught()
如果说没有找到/r/n的下标位置,如果本次数据的可读长度》最大可读长度,那么就会跳过本次数据,然后设置为丢弃模式
返回null
丢弃模式
if (eol >= 0) {
//以前丢弃的数据长度+本次可读的数据长度
final int length = discardedBytes + eol - buffer.readerIndex();
//拿到分隔符的长度
final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
//跳过(丢弃)本次数据
buffer.readerIndex(eol + delimLength);
//设置丢弃数据长度为0
discardedBytes = 0;
//设置非丢弃模式
discarding = false;
if (!failFast) {
fail(ctx, length);
}
} else {
//没找到\r\n
//丢弃的数据长度+本次可读数据的长度
discardedBytes += buffer.readableBytes();
//跳过本次可读取的数据
buffer.readerIndex(buffer.writerIndex());
// 我们跳过缓冲区中的所有内容,需要再次将偏移量设置为0。
offset = 0;
}
return null;
在丢弃模式下如果找到了 /r/n,那么这次要进行丢弃的就是/r/n之前的内容了,也就是说
上一次丢弃的内容 + 本次丢弃的内容 == 一条完整的消息,然后设置为非丢弃模式
如果说没有找到 /r/n 呢?还会继续进行丢弃,直到丢弃了一条完整的数据