第 9 章 TCP 粘包和拆包及解決方案

9.1 TCP 粘包和拆包基本介紹

  1. TCP 是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的 socket,因此,發送端爲了將多個發給接收端的包,更有效的發給對方,使用了優化方法(Nagle 算法),將多次間隔較小且數據量小的數據,合併成一個大的數據塊,然後進行封包。這樣做雖然提高了效率,但是接收端就難於分辨出完整的數據包了,因爲面向流的通信是無消息保護邊界的

  2. 由於 TCP 無消息保護邊界, 需要在接收端處理消息邊界問題,也就是我們所說的粘包、拆包問題

  3. 示意圖 TCP 粘包、拆包圖解
    在這裏插入圖片描述

對圖的說明:

假設客戶端分別發送了兩個數據包 D1 和 D2 給服務端,由於服務端一次讀取到字節數是不確定的,故可能存在以下四種情況:

  1. 服務端分兩次讀取到了兩個獨立的數據包,分別是 D1 和 D2,沒有粘包和拆包

  2. 服務端一次接受到了兩個數據包,D1 和 D2 粘合在一起,稱之爲 TCP 粘包

  3. 服務端分兩次讀取到了數據包,第一次讀取到了完整的 D1 包和 D2 包的部分內容,第二次讀取到了 D2 包的剩餘內容,這稱之爲 TCP 拆包

  4. 服務端分兩次讀取到了數據包,第一次讀取到了 D1 包的部分內容 D1_1,第二次讀取到了 D1 包的剩餘部分內容 D1_2 和完整的 D2 包。

9.2 TCP 粘包和拆包現象實例

在編寫 Netty 程序時,如果沒有做處理,就會發生粘包和拆包的問題
看一個具體的實例:

public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf>{
    private int count;//每個Channel有各自的ChannelHandler

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

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {

        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);

        //將buffer轉成字符串
        String message = new String(buffer, Charset.forName("utf-8"));

        System.out.println("服務器接收到數據 " + message);
        System.out.println("服務器接收到消息量=" + (++this.count));//服務端收到消息的次數

        //服務器回送數據給客戶端, 回送一個隨機id ,
        ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString() 
        + " ", Charset.forName("utf-8"));
        ctx.writeAndFlush(responseByteBuf);
    }
}

public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private int count;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客戶端發送10條數據 hello,server 編號
        for(int i= 0; i< 10; ++i) {
            ByteBuf buffer = Unpooled.copiedBuffer("hello,server " + i, Charset.forName("utf-8"));
            ctx.writeAndFlush(buffer);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);

        String message = new String(buffer, Charset.forName("utf-8"));
        System.out.println("客戶端接收到消息=" + message);
        System.out.println("客戶端接收消息數量=" + (++this.count));

    }

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

出現:
Server端6次接收到消息
在這裏插入圖片描述
Server端7次接收到消息
在這裏插入圖片描述

9.3 TCP 粘包和拆包解決方案

  1. 使用 自定義協議 + 編解碼器 來解決

  2. 關鍵就是要解決 服務器端每次讀取數據長度的問題, 這個問題解決,就不會出現服務器多讀或少讀數據的問題,從而避免的 TCP 粘包、拆包 。

9.4 看一個具體的實例:

  1. 要求客戶端發送 5 個 Message 對象, 客戶端每次發送一個 Message 對象

  2. 服務器端每次接收一個 Message, 分 5 次進行解碼, 每讀取到 一個 Message , 會回覆一個 Message 對象 給客戶端.
    在這裏插入圖片描述

//Server
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new MyMessageDecoder());//解碼器
        pipeline.addLast(new MyMessageEncoder());//編碼器
        pipeline.addLast(new MyServerHandler());
    }
}

public class MyMessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyMessageDecoder decode 被調用");
        //需要將得到二進制字節碼-> MessageProtocol 數據包(對象)
        int length = in.readInt();

        byte[] content = new byte[length];
        in.readBytes(content);

        //封裝成 MessageProtocol 對象,放入 out, 傳遞下一個handler業務處理
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(length);
        messageProtocol.setContent(content);

        out.add(messageProtocol);
    }
}

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder encode 方法被調用");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}

//處理業務的handler
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol>{//MessageProtocol接收
    private int count;

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

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {

        //接收到數據,並處理    第一部分
        int len = msg.getLen();
        byte[] content = msg.getContent();

        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("服務器接收到信息如下");
        System.out.println("長度=" + len);
        System.out.println("內容=" + new String(content, Charset.forName("utf-8")));

        System.out.println("服務器接收到消息包數量=" + (++this.count));




        //回覆消息  第二部分

        String responseContent = UUID.randomUUID().toString();
        int responseLen = responseContent.getBytes("utf-8").length;
        byte[]  responseContent2 = responseContent.getBytes("utf-8");
        //構建一個協議包
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(responseLen);
        messageProtocol.setContent(responseContent2);

        ctx.writeAndFlush(messageProtocol);
    }
}

//Client
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {

        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyMessageEncoder()); //加入編碼器
        pipeline.addLast(new MyMessageDecoder()); //加入解碼器
        pipeline.addLast(new MyClientHandler());
    }
}

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder encode 方法被調用");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}

public class MyMessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyMessageDecoder decode 被調用");
        //需要將得到二進制字節碼-> MessageProtocol 數據包(對象)
        int length = in.readInt();

        byte[] content = new byte[length];
        in.readBytes(content);

        //封裝成 MessageProtocol 對象,放入 out, 傳遞下一個handler業務處理
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(length);
        messageProtocol.setContent(content);

        out.add(messageProtocol);
    }
}

public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {

    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客戶端發送10條數據 "今天天氣冷,吃火鍋" 編號

        for(int i = 0; i< 5; i++) {
            String mes = "今天天氣冷,吃火鍋";
            byte[] content = mes.getBytes(Charset.forName("utf-8"));
            int length = mes.getBytes(Charset.forName("utf-8")).length;

            //創建協議包對象
            MessageProtocol messageProtocol = new MessageProtocol();
            messageProtocol.setLen(length);
            messageProtocol.setContent(content);
            ctx.writeAndFlush(messageProtocol);

        }

    }

//    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {

        int len = msg.getLen();
        byte[] content = msg.getContent();

        System.out.println("客戶端接收到消息如下");
        System.out.println("長度=" + len);
        System.out.println("內容=" + new String(content, Charset.forName("utf-8")));

        System.out.println("客戶端接收消息數量=" + (++this.count));

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("異常消息=" + cause.getMessage());
        ctx.close();
    }
}

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