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

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