1. 粘包和拆包发生的原因
1.1. 拆包的原因:
- 发送的数据大于TCP的发送缓冲区会进行第一次拆包。
- 发送的数据大于MSS(一般来说是1460个字节)进行第二次拆包。
- 大于IP以太网的MTU(最大传输单元),IP协议会进行数据的分片,进行第三次拆包。
1.2. 粘包的原因
- 开启nagle算法,data1,data2同时放到tcp的发送缓冲区。
- 应用处理数据不及时,data1,data2同时积压在tcp的接收方缓冲区。
2. 解决方法
- 基于分隔符的协议。
- 基于定长的协议。
- 基于变长的协议,将消息分为消息体和消息头。
- 自定义更复杂的协议。
3. Netty的提供的解决方式
一般来说我们都会定义自己的协议,Netty提供了ByteToMessageDecoder用于自定义消息的解码。
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
// 累加器
ByteBuf cumulation;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// 初始化一个集合用来存储解码后的数据
CodecOutputList out = CodecOutputList.newInstance();
try {
first = cumulation == null;
// 分配一个ByteBuf用于累加这个channel的可读数据
cumulation = cumulator.cumulate(ctx.alloc(),
first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
// 解码
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) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
numReads = 0;
discardSomeReadBytes();
if (!firedChannelRead && !ctx.channel().config().isAutoRead()) {
ctx.read();
}
firedChannelRead = false;
ctx.fireChannelReadComplete();
}
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
// 判断集合中是不是已经存在解码后的数据
if (outSize > 0) {
// 后面的handler处理数据
fireChannelRead(ctx, out, outSize);
out.clear();
// Check if this handler was removed before continuing with decoding.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See:
// - https://github.com/netty/netty/issues/4635
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
// 解码前记录下可读的字节数
int oldInputLength = in.readableBytes();
// 调用具体的子类进行解码
decodeRemovalReentryProtection(ctx, in, out);
// Check if this handler was removed before continuing the loop.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See https://github.com/netty/netty/issues/1664
if (ctx.isRemoved()) {
break;
}
// 解码后,如果没有解码到数据
if (outSize == out.size()) {
// 没有读取数据
if (oldInputLength == in.readableBytes()) {
break;
} else {
// 读取了数据,但是没有解码到可用的bean, 可能是占包,继续进行解码。
continue;
}
}
// 没有读取任何数据,但是发生了解码,抛出一个异常
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
}
3.1. 优化Netty解码器
static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) {
if (msgs instanceof CodecOutputList) {
fireChannelRead(ctx, (CodecOutputList) msgs, numElements);
} else {
for (int i = 0; i < numElements; i++) {
ctx.fireChannelRead(msgs.get(i));
}
}
}
// 可以优化这个方法,将集合穿给下个channleHandler,减少channleHandler的调用次数。
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
}