Netty中粘包/拆包處理

TCP 是基於流傳輸的協議,請求數據在其傳輸的過程當中是沒有界限區分,因此咱們在讀取請求的時候,不必定能獲取到一個完整的數據包。若是一個包較大時,可能會切分紅多個包進行屢次傳輸。同時,若是存在多個小包時,可能會將其整合成一個大包進行傳輸。這就是 TCP 協議的粘包/拆包概念。

本文基於 Netty5 進行分析java

粘包/拆包描述

假設當前有123abc兩個數據包,那麼他們傳輸狀況示意圖以下:bootstrap

  • I 爲正常狀況,兩次傳輸兩個獨立完整的包。
  • II 爲粘包狀況,123abc封裝成了一個包。
  • III 爲拆包狀況,圖中的描述是將123拆分紅了123,而且1abc一塊兒傳輸。123abc也多是abc進行拆包。甚至123abc進行屢次拆分也有可能。

Netty 粘包/拆包問題

爲突出 Netty 的粘包/拆包問題,這裏經過例子進行重現問題,如下爲突出問題的主要代碼:segmentfault

服務端:緩存

/**
 * 服務端網絡事件的讀寫操做類
 * 
 * Created by YangTao.
 */
public class ServerHandler extends ChannelHandlerAdapter {
    // 接收消息計數器
    private int i = 0;

    // client端消息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        i++;
        
        System.out.print(msg);
        
        // 對每條讀取到的消息進行打數標記
        System.out.println("================== ["+ i +"]");
        // 發送應答消息給客戶端
        ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i).getBytes());
        ctx.write(rmsg);
    }
    
    // 其餘操做 .......
}

客戶端:網絡

/**
 * 客戶端發送數據
 * 
 * Created by YangTao.
 */
public class NettyClient {

    public void send() {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();

        try {
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));
                            pipeline.addLast(new ClientHandler());
                        }
                    });
            Channel channel = bootstrap.connect(HOST, PORT).channel();
            int i = 1;
            while (i <= 300){
                channel.writeAndFlush(String.format("【時間 %s: \t%s】", new Date(), i));
                // 打印發送請求的次數
                System.out.println(i);
                i++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
             if (group != null)
                 group.shutdownGracefully();
         }
    }
}

以上代碼中,咱們第一反應理解的是,若是非異常狀況下客戶端全部數據發送成功,而且服務端所有接收到。那麼從打印信息中能夠看到客戶端的發送次數i和服務端的接收消息計數i應該是相同的數。那麼下面經過運行程序,查看打印結果。app

如上圖所示,【】中的最後一個數字與[]中數字對上的是已獨立完整的包接收到(粘包/拆包示意圖中的狀況 I)。可是【】中爲3738的出現了粘包狀況(粘包/拆包示意圖中的狀況 II),兩條數據粘合在一塊兒。ide

上圖中能夠看到【】167的數據被拆分爲了兩部分(圖中畫綠線數據),該狀況爲拆包(粘包/拆包示意圖中的狀況 III)oop

上面程序沒有考慮到 TCP 的粘包/拆包問題,因此若是是咱們實際應用的程序的話,不能保證數據的正常狀況,就會致使程序異常。測試

Netty 解決粘包/拆包問題

LineBasedFrameDecoder 換行符處理

Netty 的強大,方便,簡單使用的優點,在粘包/拆包問題上也提供了多種編解碼解決方案,而且很容易理解和掌握。
這裏使用 LineBasedFrameDecoder 和 StringDecoder(將接收到得對象轉換成字符串) 來解決粘包/拆包問題。
只需在服務端和客戶端分別添加 LineBasedFrameDecoder 和 StringDecoder解碼器,由於是雙向會話,因此兩端都要添加,因爲我一開始就添加 StringDecoder 編碼器,因此只需添加 LineBasedFrameDecoder 就夠了。
服務端:編碼

客戶端:

服務端網絡事件操做:

/**
 * 服務端網絡事件的讀寫操做類
 * 
 * Created by YangTao.
 */
public class ServerHandler extends ChannelHandlerAdapter {
    // 接收消息計數器
    private int i = 0;

    // client端消息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        i++;
        
        System.out.print(msg);
        
        // 對每條讀取到的消息進行打數標記
        System.out.println("================== ["+ i +"]");
        // 發送應答消息給客戶端
        ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i + System.getProperty("line.separator")).getBytes());
        ctx.write(rmsg);
    }
    
    // 其餘操做 .......
}

客戶端發送數據:

/**
 * 客戶端發送數據
 * 
 * Created by YangTao.
 */
public class NettyClient {

    public void send() {
        // 鏈接操做 .......

        try {
            // 獲取 channel
            Channel channel = channel();
            int i = 1;
            ByteBuf buf = null;
            while (i <= 300){
                String str = String.format("【時間 %s: \t%s】", new Date(), i) + System.getProperty("line.separator");
                byte[] bytes = str.getBytes();
                // 寫入緩衝區
                buf = Unpooled.buffer(bytes.length);
                buf.writeBytes(bytes);
                channel.writeAndFlush(buf);
                // 打印發送請求的次數
                System.out.println(i);
                i++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        
        // 退出操做 .......
    }
}

細心觀察代碼的變化,應該會發現如今的代碼每次在發送消息的時候,在消息末尾後加了換行分隔符。注意,使用 LineBasedFrameDecoder 時,換行分隔符必須加,不然接收消息端收不到消息,若是手寫換行分割,要記得區分不一樣系統得適配

通過屢次測試 3W 條請求,沒有再出現過粘包/拆包狀況,看最後一條數據數字是否相同便知。

DelimiterBasedFrameDecoder 自定義分隔符

自定義分隔符和換行分隔符差很少,只需將發送的數據後換行符換成你本身設定的分割符便可。

服務端和客戶端均在 pipeline 添加 DelimiterBasedFrameDecoder:

// 指定的分隔符
public static final String DELIMITER = "$@$";

// 若是當前數據2048個字節中沒有分隔符,就會拋出異常,避免內存溢出。也能夠自定義預檢查當前讀取的數據,自定義這裏超過的規則
pipeline.addLast(new DelimiterBasedFrameDecoder(
        2048, 
        Unpooled.wrappedBuffer(DELIMITER.getBytes())) // 分割符緩衝對象
);

FixedLengthFrameDecoder 根據固定長度

設定固定長度,進行數據傳輸,若是不達固定長度,使用空格補全。

服務端和客戶端均在 pipeline 添加 FixedLengthFrameDecoder:

// 100爲指定的固定長度
ch.pipeline().addLast(new FixedLengthFrameDecoder(100));

每次讀取數據時都會按照 FixedLengthFrameDecoder 中設置的固定長度進行解碼,若是出現粘包,那麼會進行屢次解碼,若是出現拆包的狀況,那麼 FixedLengthFrameDecoder 會先緩存當前部分包的信息,當接收下一個包時,會與緩存的部分包進行拼接,知道知足規定的長度。

動態指定長度

動態指定長度就是說,每條消息的長度都是隨着消息頭進行指定,這裏使用的編碼器爲 LengthFieldBasedFrameDecoder。

pipeline().addLast(
        new LengthFieldBasedFrameDecoder(
        2048, // 幀的最大長度,即每一個數據包最大限度
        0, // 長度字段偏移量
        4, // 長度字段所佔的字節數
        0, // 消息頭的長度,能夠爲負數
        4) // 須要忽略的字節數,從消息頭開始,這裏是指整個包
);

發送消息時,建立本身的消息對象編碼器

// 建立 byteBuf
ByteBuf buf = getBuf();

// .....

// 設置該條消息內容長度
buf.writeInt(msg.length());
// 設置消息內容
buf.writeBytes(msg.getBytes("UTF-8"));

服務端讀取的時候就直接讀取便可,沒其餘特殊操做。

除了以上 Netty 提供的現成方案,還能夠經過重寫 MessageToByteEncoder 編碼實現自定義協議。

總結

Netty 極大的爲使用者提供了多種解決粘包/拆包方案,而且能夠很愉快的對多種消息進行自動解碼,在使用過程當中也極容易掌握和理解,很大程度上提高開發效率和穩定性。
我的博客: https://ytao.top
個人公衆號 ytao
個人公衆號
相關文章
相關標籤/搜索