Skip to content

netty数据包

一、TCP粘包和拆包

  1. TCP拆包:一个完整的包可能会被TCP拆分成多个包进行发送

  2. TCP粘包:把多个小的包封装成一个大的数据包发送,Client发送的若干数据包,Server接收粘成一包。

发送方和接收方都可能出现这个问题:

发送方原因:TCP默认使用Nagle算法

接收方原因:TCP接收数据防止缓存中,应用程序中缓存中读取

解决办法:

发送方:可以关闭nagle算法

接收方:TCO是无边界的数据流,并没有处理粘包现象的机制

协议本身无法避免粘包,半包读写的发生需要在应用层处理。应用层处理半包读写的办法:

  1. 设置定长消息(如10字符),aaaaaaaaa0aaaaaaaaa0

FixedLengthFrameDecoder

  1. 设置消息的边界(如&&进行切割),sfdsfsdfasdf&&dfasfas&&sfasdf

LineBasedFrameDecoderr和DelemiterBasedFrameDecoder

  1. 使用带消息头的协议,消息头存储消息开始标识及消息的长度信息:header+body

LengthFieldBasedFrameDecoder

在clienthandler里通过以下方式可以模拟粘包问题,server端如果没有做特定的处理,就会出现一次性读取数据的操作。

    //通道准备就绪
    @Override public void channelActive(ChannelHandlerContext ctx) throws Exception{
        ByteBuf byteBuf = null;
        for (int i = 0; i < 10; i++) {
            byte[] bytes = ("tcp client" + System.lineSeparator()).getBytes();
            byteBuf = Unpooled.buffer(bytes.length);
            byteBuf.writeBytes(bytes);
            ctx.writeAndFlush(byteBuf);
        }

    }

二、半包问题解决方式

  1. LineBasedFrameDecoder:换行的方式解决,但是每次换行的数据包长度不能大于设定的长度,否则会报错,如client端发送了一个2048字节长度的才换行,server端接收为1024字节,server端就会报异常,无法处理这个消息。

    //将对应的channelHandler添加到管道口
    //解决半包问题,设置长度
    pipeline.addLast(new LineBasedFrameDecoder(1024));
    //将消息转成字符串,重写read方法的时候,message对象会是个String对象,不是ByteBuf对象
    pipeline.addLast(new StringDecoder());
    pipeline.addLast(new TcpServerHandler());
  2. DelemiterBasedFrameDecoder:指定分隔符的方式类似上面的换行分割。与上面不同的是DelemiterBasedFrameDecoder构造器有四个参数

    • maxlength:表示一行的最大长,超过这个长度还没检测到分隔符就会报TooLongFrameException

    • failFast:如果为true,则超出maxLength长度就抛出异常,不继续进行解码。如果为false,等取到完整的消息并且解码后,在抛出TooLongFrameException

    • stripDelimiter:解码后的消息是否去掉分隔符。

    • delimiters:分隔符,参数对象为ByteBuf类型

    //将对应的channelHandler添加到管道口
    //以字符串内容为分隔符
    ByteBuf byteBuf = Unpooled.copiedBuffer("&&".getBytes());
    //解决半包问题,设置长度
    pipeline.addLast(new DelimiterBasedFrameDecoder(1024,byteBuf));
    //将消息转成字符串,重写read方法的时候,message对象会是个String对象,不是ByteBuf对象
    pipeline.addLast(new StringDecoder());
    pipeline.addLast(new TcpServerHandler());
  3. LengthFieldBasedFrameDecoder

    • maxFrameLength:数据包的最大长度

    • lengthFieldOffset:长度字段的偏移位,长度字段开始的地方,意思是跳过指定长度的字节以后才是真正的消息体

    • lengthFieldLength:长度字段占的字节数,帧数据长度的字段本身的长度

    • lengthAdjustment:一般是header+body,添加到长度字段的补偿值,如果为负数,开发人员认为这个header长度是整个消息包的长度,则netty应该减去对应的数字

    • initialBytesToStrip:从解码帧当中第一次去除的字节数,获取一个完整的数据包以后,忽略前面指定位数的长度字节,应用解码器拿到的是不带长度域的数据包

    • failFast:是否快速失败

三、自定义数据处理

数据包类:

import lombok.Data;

/**
 * @Description netty自定义消息对象
 * @Author lcy
 * @Date 2021/10/25 16:01
 */
@Data
public class MessageProtocol {

    /**
     * 消息长度
     */
    private int length;

    /**
     * 消息体
     */
    private byte[] bytes;

}

编码器:

/**
 * @Description 自定义消息编码器对象
 * @Author lcy
 * @Date 2021/10/25 16:00
 */
public class MessageEncode extends MessageToByteEncoder<MessageProtocol> {

    @Override protected void encode(ChannelHandlerContext ctx,MessageProtocol msg,ByteBuf out){
        System.out.println("调用编码器");
        //设置数据包长度
        out.writeInt(msg.getLength());
        //设置数据包
        out.writeBytes(msg.getBytes());
    }
}

解码器:

import java.util.List;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

/**
 * @Description 自定义消息解码器对象
 * @Author lcy
 * @Date 2021/10/25 16:00
 */
public class MessageDecode extends ByteToMessageDecoder {

    /**
     * 记录获取数据包长度,用于解决拆包问题,默认为0
     */
    private int length = 0;

    @Override protected void decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out){

        //判断是否为真实的数据,前面4个字节为报文的长度
        if (in.readableBytes() < 4) {
            return;
        }
        //判断数据包长度,如果没有发生拆包,赋值
        if (length == 0) {
            length = in.readInt();
        }
        //如果缓冲区字节数据小于数据包长度,直接返回。发生拆包,等待下一个包再处理
        if (in.readableBytes() < length) {
            System.out.println("发生拆包,等待下一个数据包");
            return;
        }

        //读取数据
        byte[] content = new byte[length];
        if (in.readableBytes() >= length) {
            in.readBytes(content);

            //封装成自定义对象
            MessageProtocol messageProtocol = new MessageProtocol();
            messageProtocol.setLength(length);
            messageProtocol.setBytes(content);
            //传输到下一个管道当中
            out.add(messageProtocol);
        }
        //重置长度
        length = 0;

    }
}

netty客户端和服务端在添加pipeline的时候只需要添加对应的编码器和解码器,传输的时候通过数据类来传输即可