亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

Netty 粘包和拆包

1. 前言

前面幾個章節主要解析了 Netty 的編碼、解碼問題,那么是否有了編解碼器,我們的 Netty 通信就能正常了呢?

TCP 協議在傳輸數據時沒有辦法判斷數據是什么時候結束的,它無法識別一段完整的信息,因此可能會導致接受到的數據和發送時的數據不一致的情況。因此需要人為的指定一種規范的協議,從而保證數據的安全性,比如:我們所熟悉的 HTTP 協議。

本節內容,我們主要需要以下兩點知識

  1. TCP 拆包、粘包的原因;
  2. TCP 拆包、粘包的解決方案。

2. 學習目的

拆包、粘包在 TCP 協議當中,或者說 Netty 開發當中必須需要去解決的問題。在開發當中,你會發現你不需要解決拆包、粘包問題,數據也是能正常發送和接受,那么為什么需要去解決呢?

原因是,數據量比較小,TCP 發送之前它是有個緩沖池的,根據緩沖池的大小來把數據包拆分成多個小包進行發送。在高并發的情況下,拆包、粘包問題是經常會發生的,因此需要去 解決,否則接收方將獲取不到正確的數據。

3. 粘包和拆包問題解析

3.1 模擬拆包粘包問題

開始,之前我們先看一個簡單的案例,具體如下所示:

客戶端: 客戶端使用 for 循環,連續向服務端發送 hello world1000 遍(使用 StringEncoder 編碼器)。

public class ClientTestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {     
        for(int i=0;i<1000;i++){
            ctx.channel().writeAndFlush(
                Unpooled.copiedBuffer("hello world 世界你好,Netty技術學習".getBytes())
            );
        }
    }
}

服務端: 正常輸出客戶端的信息(使用 StringDecoder 解碼器)。

public class ServerTestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String str=msg.toString();
        System.out.println(str);
    }
}

輸出結果:
圖片描述

總結:
通過以上的輸出結果,我們發現,客戶端發送過來的數據,有時候能正確打印,有時候數據粘在了一起。以上輸出結果有亂碼想象、有多個信息輸出到一行,就是 ByteBuf 粘包和 ByteBuf 半包。

通過上面的簡單案例,我們發現 TCP 協議下會產生數據安全性問題,其實在 TCP 中粘包和拆包是不可避免的,因為在 TCP 協議中,數據流向水流一樣,根本不知道應該從哪里截取才是完整的數據包。TCP 并不了解上層業務的數據含義,它會根據 TCP 緩沖區的實際情況進行包的劃分,因此一個完整的業務包可能會被 TCP 拆分成多個包進行發送,也可能會把多個小包封裝成一個大包進行發送,這就是 TCP 粘包和拆包問題。

3.2 常見的原因分析

粘包和拆包其實是客戶端和服務端之間都會發生的事情,并不是說只是在客戶端產生或者服務端產生,具體分析如下:

發送方的粘包和拆包問題

  1. 要發送的數據大于 TCP 發送緩沖區剩余空間大小,將會發生拆包,也就是拆分幾次發送;
  2. 要發送數據大于最大報文長度,TCP 在傳輸前將進行拆包,也就是拆分幾次發送;
  3. 要發送的數據小于 TCP 發送緩沖區的大小,TCP 將多次寫入緩沖區的數據一次發送出去,將會發生粘包。

接收方的粘包和拆包問題

  1. 服務端分兩次讀取到獨立的數據包,那么解析出來的數據正常,沒有粘包和拆包問題;
  2. 服務端一次讀取兩個數據包,那么這些數據包就會粘合在一起,因此稱為粘包;
  3. 服務端分兩次讀取兩個數據包,第一次讀到數據 1 和數據 2 部分內容,第二次讀取數據 2 剩余內容,這被成為 TCP 拆包。

粘包和拆包的示意圖

圖片描述

總結,拆包和粘包問題并不是某一方的問題,可能是發送的粘包和拆包導致接收方讀取數據出錯,也可能是發送方正常,但是接收方讀取出錯。但是我們只需要了解,發送方和接收方什么情況下會拆包和粘包。

4. Netty 提供的粘包拆包解決方案

雖然,在 Netty 當中是基于 ByteBuf 字節容器去編程,但是底層還是會被轉換成字節流進行傳輸, 數據到了服務端,也是按照字節流的方式讀入,然后到了 Netty 應用層面,重新拼裝成 ByteBuf。如果為了數據的完整性,通常的解決方案如下:

  1. 每次讀取完都需要判斷是否是一個完整數據包 ;
  2. 如果當前讀取的數據不足以拼接成一個完整數據包,那就保留該數據,繼續從 TCP 緩沖器讀取,直到拼接成一個完整數據包為止;
  3. 如果拼接成了完整的數據包,但是有多余的數據,則仍然保留,以便和下次讀取的數據進行拼接。

思考:那么應該如何去判斷一個業務數據的完整結束呢?

方案一: 固定數據長度,客戶端在發送數據的時候,每個數據包的長度固定(比如:1024 個字節),如果發送數據不足 1024 字節時,以空格補齊;服務端則每次讀取固定長度是數據;
方案二: 分隔符,每個數據包的結尾加一個特殊分隔符,服務端則讀取到特殊分隔符則認為數據包結束;如果一次讀取的數據沒有結束符,則保留當前數據,等待下次讀取;
方案三: 將數據分為消息頭和消息體,在頭部保存了消息的數據長度,只有讀取指定長度的數據就算完整數據包;
方案四: 自定義協議,通過協議的規范進行發送和接受數據。

當然,以上的方案 Netty 官方也考慮到了,并且為了簡化開發人員的工作量,Netty 內置了常見的拆包器,具體如下:

1. 固定長度的拆包器 FixedLengthFrameDecoder

每個數據包的長度都是固定的,比如 1024,那么只需要把這個拆包器加到 pipeline 中,Netty 會把一個個長度為 1024 的數據包 (ByteBuf) 傳遞到下一個 channelHandler。

2. 行拆包器 LineBasedFrameDecoder

它是一個特殊的分隔符拆包器,以換行符作為結束符。

3. 分隔符拆包器 DelimiterBasedFrameDecoder

可以自定義自己的分隔符。

4. 基于長度域拆包器 LengthFieldBasedFrameDecoder

是最通用的一種拆包器,有一個存放數據長度的字段,讀到該字段之后,往后面的數據讀取一定長度的數據即可,只要你的自定義協議中包含長度域字段,均可以使用這個拆包器來實現應用層拆包。

5. 小結

本節內容需要掌握的知識點

  1. 什么是拆包、粘包問題,以及它的產生原因是什么?
  2. 解決拆包、粘包問題的思路以及常見解決方案是什么?