Netty之粘包拆包解决

粘包拆包表现形式
产生粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。下面示意图能更加形象的说明问题。

现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为如下三种

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。normal
第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
one

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。half_one

粘包拆包解决方案
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。业界的主流协议的解决方案,可以归纳如下: 

  1. 消息定长,报文大小固定长度,例如每个报文的长度固定为200字节,如果不够空位补空格。
  2. 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
  3. 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段。
  4. 更复杂的自定义应用层协议。

Netty粘包和拆包解决方案
Netty提供了4种解码器来解决,分别如下:

  1. 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小
  2. 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分
  3. 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分
  4. 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度

以上解码器在使用时只需要添加到Netty的责任链中即可,大多数情况下这4种解码器都可以满足了,当然除了以上4种解码器,用户也可以自定义自己的解码器进行处理。

以下是各个解码器的代码示例

public class NettyClient {

    public void start() {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap()
                .group(group)
                //该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输
                .option(ChannelOption.TCP_NODELAY, true)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() { // 绑定I/O事件处理类
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
//                       // 行拆包器,以换行符作为分隔符
//                        socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
//                        socketChannel.pipeline().addLast(new StringDecoder());

                        // 自定义分隔符
//                        ByteBuf delimiter = Unpooled.copiedBuffer("$_$".getBytes());
//                        // DelimiterBasedFrameDecoder第一个参数表示一行最大的长度,如果超过这个长度依然没有检测到\n或者\r\n,将会抛出TooLongFrameException
//                        socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, delimiter));
//                        socketChannel.pipeline().addLast(new StringDecoder());

//                      // 固定长度
//                        socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(13));

                        // 自定义协议
                        socketChannel.pipeline().addLast(new CustomEncoder());
                        socketChannel.pipeline().addLast(new CustomDecoder());

                        // 自定义长度解码器
//                        socketChannel.pipeline().addLast(new MyLengthFieldEncoder());

                        socketChannel.pipeline().addLast(new NettyClientHandler());
                    }
                });
        try {
            ChannelFuture future = bootstrap.connect("127.0.0.1", 9595).sync();
            System.out.println("客户端成功....");
            // 等待连接被关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }
}
public class NettyClientHandler extends ChannelHandlerAdapter {

    private ByteBuf msgSendBuf;
    private int counter = 0;

    public NettyClientHandler() {
    }

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    // 链路建立成功后,将Server Time请求发送给服务端
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("NettyClientHandler.channelActive");
        // 行分隔符
//        String req = "Server Time" + System.getProperty("line.separator");
//        for (int i = 0; i < 100; i++) {
//            msgSendBuf = Unpooled.copiedBuffer(req.getBytes());
//            ctx.writeAndFlush(msgSendBuf);
//        }

        // 自定义分隔符
//        String req = "i don't know what I do now is right, those are wrong, and when I finally Laosi when I know these. So I can do now is to try to do well in everything, and then wait to die a natural death.Sometimes I can be very happy to talk to everyone, can be very presumptuous, but no one knows, it is but very deliberatelycamouflage, camouflage; I can make him very happy very happy, but couldn't find the source of happiness, just giggle" +
//                " If not to the sun for smiling, warm is still in the sun there, but wewill laugh more confident calm; if turned to found his own shadow, appropriate escape, the sun will be through the heart,warm each place behind the corner; if an outstretched palm cannot fall butterfly, then clenched waving arms, given power; if I can't have bright smile, it will face to the sunshine, and sunshine smile together, in full bloom." +
//                " Time is like a river, the left bank is unable to forget the memories, right is worth grasp the youth, the middle of the fast flowing, is the sad young faint. There are many good things, buttruly belong to own but not much. See the courthouse blossom,honor or disgrace not Jing, hope heaven Yunjuanyunshu, has no intention to stay. In this round the world, all can learn to use a normal heart to treat all around, is also a kind of realm!$_$";
//        msgSendBuf = Unpooled.copiedBuffer(req.getBytes());
//        ctx.writeAndFlush(msgSendBuf);

        // 固定长度
//        ByteBuf A = Unpooled.buffer().writeBytes("A".getBytes());
//        ByteBuf BC = Unpooled.buffer().writeBytes("BC".getBytes());
//        ByteBuf DEFG = Unpooled.buffer().writeBytes("DEFG".getBytes());
//        ByteBuf HI = Unpooled.buffer().writeBytes("HI".getBytes());
//        ctx.writeAndFlush(A);
//        ctx.writeAndFlush(BC);
//        ctx.writeAndFlush(DEFG);
//        ctx.writeAndFlush(HI);

        // 自定义协议
        String req = "i am client ...";
        byte[] content = req.getBytes();
        int contentLength = content.length;
        CustomProtocol protocol = new CustomProtocol(contentLength, content);
        ctx.writeAndFlush(protocol);

        // 自定义长度解码器
//        String body = "你好,netty server";
//        Message msg = new Message((byte) 0xCA, body.length(), body);
//        ctx.writeAndFlush(msg);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
          // 行分隔符、自定义分隔符
//        String response = (String) msg;
//        System.out.println("接收到服务端响应:" + response + ",counter:" + ++counter);

          // 固定长度
//        ByteBuf packet = (ByteBuf) msg;
//        System.out.println("接收到服务端响应:" + packet.toString(Charset.defaultCharset()));

          // 自定义协议
        CustomProtocol protocol = (CustomProtocol) msg;
        System.out.println("接收到服务端响应:" + new String(protocol.getContent()));
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
}
public class NettyServer {

    private static final int MAX_FRAME_LENGTH = 1024 * 1024;
    private static final int LENGTH_FIELD_LENGTH = 4;
    private static final int LENGTH_FIELD_OFFSET = 1;
    private static final int LENGTH_ADJUSTMENT = 0;
    private static final int INITIAL_BYTES_TO_STRIP = 0;


    public void start(InetSocketAddress socketAddress) {
        // new 一个主线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //new 一个工作线程组
        EventLoopGroup workGroup = new NioEventLoopGroup(200);
        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel)
                            throws Exception {
                        ChannelPipeline p = socketChannel.pipeline();

//                       // 1.换行分隔符
//                        p.addLast(new LineBasedFrameDecoder(1024));
//                        p.addLast(new StringDecoder());

                        // 2.自定义分隔符
//                        ByteBuf delimiter = Unpooled.copiedBuffer("$_$".getBytes());
//                        // DelimiterBasedFrameDecoder第一个参数表示一行最大的长度,如果超过这个长度依然没有检测到\n或者\r\n,将会抛出TooLongFrameException
//                        p.addLast(new DelimiterBasedFrameDecoder(2048, delimiter));
//                        p.addLast(new StringDecoder());

                        // 3.固定长度
//                        // 如果固定长度设置为超出发送字符长度,需要补齐到指定长度
//                        p.addLast(new FixedLengthFrameDecoder(3));

                        // 4.自定义协议
                        p.addLast(new CustomEncoder());
                        p.addLast(new CustomDecoder());

                        // 5.自定义长度
//                        p.addLast(new MyLengthFieldDecoder(MAX_FRAME_LENGTH, 1, 4, 0, 5, true));

                        p.addLast(new NettyServerHandler());// 用来处理Server端接收和处理消息的逻辑
                    }
                })
                .localAddress(socketAddress)
                // 设置队列大小
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
                .childOption(ChannelOption.SO_KEEPALIVE, true);

        // 绑定端口,开始接收进来的连接
        try {
            ChannelFuture future = bootstrap.bind(socketAddress).sync();
            System.out.println("服务器启动开始监听端口: " + socketAddress.getPort());
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 关闭主线程组
            bossGroup.shutdownGracefully();
            // 关闭工作线程组
            workGroup.shutdownGracefully();
        }
    }
}
public class NettyServerHandler extends ChannelHandlerAdapter {

    private int counter = 0;

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
          // 行分隔符
//        String body = (String) msg;
//        System.out.println("接收到客户端请求:" + body + ",counter:" + ++counter);
//        // 如果接受到的消息时Server Time,则异步将服务端当前时间发送给客户端。
//        if ("Server Time".equalsIgnoreCase(body)) {
//            byte[] data = ((new Date()).toString() + System.getProperty("line.separator")).getBytes();
//            ByteBuf resp = Unpooled.copiedBuffer(data);
//            // 这里write方法只是将数据写入缓冲区,并没有真正发送
//            ctx.write(resp);
//        }

          // 自定义分隔符
//        String body = (String) msg;
//        System.out.println("接收到客户端请求:" +(++counter)+":"+ body);
//        String currentTime = System.currentTimeMillis()+"$_$";
//        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
//        ctx.write(resp);


          // 固定长度
//        if (msg instanceof ByteBuf) {
//            ByteBuf packet = (ByteBuf) msg;
//            System.out.println(new Date().toLocaleString() + ":" + packet.toString(Charset.defaultCharset()));
//            ByteBuf resp = Unpooled.copiedBuffer((System.currentTimeMillis() + "").getBytes());
//            ctx.write(resp);
//        }

          // 自定义协议
        CustomProtocol protocol = (CustomProtocol) msg;
        System.out.println("接收到客户端请求:" + new String(protocol.getContent()));
        // 响应数据给客户端
        String str = "hi, i am server ...";
        CustomProtocol response = new CustomProtocol(str.getBytes().length, str.getBytes());
        ctx.writeAndFlush(response);
        
        // 自定义长度解码器
//        if (msg instanceof Message) {
//            Message body = (Message) msg;
//            System.out.println("接收到客户端请求:" + ctx.channel().remoteAddress() + " 发送 " + body.getMsgBody());
//        }
    }

    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 将缓冲区的数据写入SocketChannel
        ctx.flush();
    }
}
public class ClientApplication {

    public static void main(String[] args) {
        NettyClient nettyClient = new NettyClient();
        nettyClient.start();
    }
}
public class ServerApplication {
    public static void main(String[] args) {
        NettyServer nettyServer = new NettyServer();
        nettyServer.start(new InetSocketAddress("127.0.0.1", 9595));
    }
}

验证自定义长度解码器所需类

public class Message implements Serializable {

    private static final long serialVersionUID = -1347507788193899520L;

    // 消息类型
    private byte type;
    // 消息长度
    private int length;
    // 消息体
    private String msgBody;

    public Message(byte type, int length, String msgBody) {
        this.type = type;
        this.length = length;
        this.msgBody = msgBody;
    }

    public byte getType() {
        return type;
    }

    public void setType(byte type) {
        this.type = type;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String getMsgBody() {
        return msgBody;
    }

    public void setMsgBody(String msgBody) {
        this.msgBody = msgBody;
    }
}
public class MyLengthFieldDecoder extends LengthFieldBasedFrameDecoder {
    /**
     * 我们在Message类中定义了type和length,这都放在消息头部
     * type占1个字节,length占4个字节所以头部总长度是5个字节
     */
    private static final int HEADER_SIZE = 5;
    private byte type;
    private int length;
    private String msgBody;


    // byteOrder:表示字节流表示的数据是大端还是小端,用于长度域的读取;
    // maxFrameLength:表示的是包的最大长度,超出包的最大长度netty将会做一些特殊处理;
    // lengthFieldOffset:长度域偏移
    // lengthFieldLength:长度域长(字节数如int为4)
    // lengthAdjustment:矫正包体长度大小(长度域的值包含除有效数据域之外,还有长度域自身或其它,那么需要矫正),矫正值 = 包长 - 长度域的值 - 长度域偏移 - 长度域长
    // initialBytesToStrip:从数据帧中跳过的字节数,表示获取完一个完整的数据包之后,忽略前面的指定的位数个字节,应用解码器拿到的就是不带长度域的数据包;
    // failFast:如果为true则表示读取到长度域,它的值超过maxFrameLength就抛出TooLongFrameException,而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。
    public MyLengthFieldDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        if (in == null) {
            return null;
        }
        if (in.readableBytes() < HEADER_SIZE) {
            throw new Exception("接收到的字节数未达到head字节大小");
        }
        /**
         * 通过源码我们能看到在读的过程中
         * 每读一次读过的字节即被抛弃
         * 即指针会往前跳
         */
        type = in.readByte();
        length = in.readInt();

        if (in.readableBytes() < length) {
            throw new Exception("接收到包的字节数不足");
        }

        ByteBuf buf = in.readBytes(length);
        byte[] b = new byte[buf.readableBytes()];
        buf.readBytes(b);

        msgBody = new String(b, "UTF-8");
        Message msg = new Message(type, length, msgBody);
        return msg;
    }
}
public class MyLengthFieldEncoder extends MessageToByteEncoder<Message> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
        if(message == null){
            throw new Exception("消息为空");
        }
        String msgBody = message.getMsgBody();
        byte[] b = msgBody.getBytes(Charset.forName("utf-8"));
        byteBuf.writeByte(message.getType());
        byteBuf.writeInt(b.length);
        byteBuf.writeBytes(b);
    }
}

验证自定义协议解码器所需类

public class Constant {

    // 协议开始标志
    public static final int HEAD_DATA = 0X76;

}
// 自定义解码器
public class CustomDecoder extends ByteToMessageDecoder {

    // ByteToMessageDecoder -> inBoundHandler

    /**
     * 协议开始的标准head_data,int类型占据4个字节.
     * 表示数据的长度contentLength,int类型占据4个字节.
     */
    private final int BASE_LENGTH = 4 + 4;

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf buffer, List<Object> out) throws Exception {

        System.out.println("CustomDecoder.decode");
        // 1.首先确认可读长度大于基本长度
        if (buffer.readableBytes() > BASE_LENGTH) {

            // 2.防止socket字节流攻击
            // 防止客户端传来的数据过大
            // 因为太大的数据,是不合理的
            if (buffer.readableBytes() > 2048) {
                // 将readerIndex移动
                buffer = buffer.skipBytes(buffer.readableBytes());
            }

            int beginRead;
            while (true) {
                // 获取包头开始的index;
                beginRead = buffer.readerIndex();
                // 标记包头开始的index
                buffer.markReaderIndex();
                //如果读到了数据包的协议开头,那么就结束循环
                if (buffer.readInt() == Constant.HEAD_DATA) {
                    break;
                }
                // 没读到协议开头,退回到标记
                buffer.resetReaderIndex();
                // 跳过一个字节
                buffer.readByte();
                // 如果可读长度小于基本长度
                if (buffer.readableBytes() < BASE_LENGTH) {
                    return;
                }
            }

            // 获取消息的长度
            int length = buffer.readInt();

            // 判断请求数据包是否到齐
            if (buffer.readableBytes() < length) {
                buffer.readerIndex(beginRead);
                return;
            }

            byte[] content = new byte[length];
            buffer.readBytes(content);
            CustomProtocol customProtocol = new CustomProtocol(length, content);
            out.add(customProtocol);
        }
    }
}
// 自定义编码器
public class CustomEncoder extends MessageToByteEncoder<CustomProtocol> {

    // MessageToByteEncoder -> outBoundHandler

    /**
     * 功能描述: 将用户定义的数据类型转换为byte<br>
     *
     * @Param: [channelHandlerContext, customProtocol, out]
     * @Return: void
     * @Author: jiangtj
     * @Date: 2020/4/26 16:18
     */
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, CustomProtocol protocol, ByteBuf out) throws Exception {
        System.out.println("CustomEncoder.encode");
        out.writeInt(protocol.getHead_data());
        out.writeInt(protocol.getContentLength());
        out.writeBytes(protocol.getContent());

    }
}
public class CustomProtocol implements Serializable {

    private static final long serialVersionUID = -986926382300551084L;

    // 消息开头标识
    private int head_data = Constant.HEAD_DATA;

    // 消息长度
    private int contentLength;

    // 消息内容
    private byte[] content;

    public CustomProtocol(int contentLength, byte[] content) {
        this.contentLength = contentLength;
        this.content = content;
    }

    public int getHead_data() {
        return head_data;
    }

    public void setHead_data(int head_data) {
        this.head_data = head_data;
    }

    public int getContentLength() {
        return contentLength;
    }

    public void setContentLength(int contentLength) {
        this.contentLength = contentLength;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "CustomProtocol{" +
                "head_data=" + head_data +
                ", contentLength=" + contentLength +
                ", content=" + Arrays.toString(content) +
                '}';
    }
}

LengthFieldBasedFrameDecoder介绍
参考:https://www.cnblogs.com/crazymakercircle/p/10294745.html
LengthFieldBasedFrameDecoder使用场景
参考:https://blog.csdn.net/ligang_csdn/article/details/77892359

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章