Netty 粘包和拆包
1. 前言
前面幾個章節主要解析了 Netty 的編碼、解碼問題,那么是否有了編解碼器,我們的 Netty 通信就能正常了呢?
TCP 協議在傳輸數據時沒有辦法判斷數據是什么時候結束的,它無法識別一段完整的信息,因此可能會導致接受到的數據和發送時的數據不一致的情況。因此需要人為的指定一種規范的協議,從而保證數據的安全性,比如:我們所熟悉的 HTTP 協議。
本節內容,我們主要需要以下兩點知識
- TCP 拆包、粘包的原因;
- TCP 拆包、粘包的解決方案。
2. 學習目的
拆包、粘包在 TCP 協議當中,或者說 Netty 開發當中必須需要去解決的問題。在開發當中,你會發現你不需要解決拆包、粘包問題,數據也是能正常發送和接受,那么為什么需要去解決呢?
原因是,數據量比較小,TCP 發送之前它是有個緩沖池的,根據緩沖池的大小來把數據包拆分成多個小包進行發送。在高并發的情況下,拆包、粘包問題是經常會發生的,因此需要去 解決,否則接收方將獲取不到正確的數據。
3. 粘包和拆包問題解析
3.1 模擬拆包粘包問題
開始,之前我們先看一個簡單的案例,具體如下所示:
客戶端: 客戶端使用 for 循環,連續向服務端發送 hello world
1000 遍(使用 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 常見的原因分析
粘包和拆包其實是客戶端和服務端之間都會發生的事情,并不是說只是在客戶端產生或者服務端產生,具體分析如下:
發送方的粘包和拆包問題
- 要發送的數據大于 TCP 發送緩沖區剩余空間大小,將會發生拆包,也就是拆分幾次發送;
- 要發送數據大于最大報文長度,TCP 在傳輸前將進行拆包,也就是拆分幾次發送;
- 要發送的數據小于 TCP 發送緩沖區的大小,TCP 將多次寫入緩沖區的數據一次發送出去,將會發生粘包。
接收方的粘包和拆包問題
- 服務端分兩次讀取到獨立的數據包,那么解析出來的數據正常,沒有粘包和拆包問題;
- 服務端一次讀取兩個數據包,那么這些數據包就會粘合在一起,因此稱為粘包;
- 服務端分兩次讀取兩個數據包,第一次讀到數據 1 和數據 2 部分內容,第二次讀取數據 2 剩余內容,這被成為 TCP 拆包。
粘包和拆包的示意圖
總結,拆包和粘包問題并不是某一方的問題,可能是發送的粘包和拆包導致接收方讀取數據出錯,也可能是發送方正常,但是接收方讀取出錯。但是我們只需要了解,發送方和接收方什么情況下會拆包和粘包。
4. Netty 提供的粘包拆包解決方案
雖然,在 Netty 當中是基于 ByteBuf 字節容器去編程,但是底層還是會被轉換成字節流進行傳輸, 數據到了服務端,也是按照字節流的方式讀入,然后到了 Netty 應用層面,重新拼裝成 ByteBuf。如果為了數據的完整性,通常的解決方案如下:
- 每次讀取完都需要判斷是否是一個完整數據包 ;
- 如果當前讀取的數據不足以拼接成一個完整數據包,那就保留該數據,繼續從 TCP 緩沖器讀取,直到拼接成一個完整數據包為止;
- 如果拼接成了完整的數據包,但是有多余的數據,則仍然保留,以便和下次讀取的數據進行拼接。
思考:那么應該如何去判斷一個業務數據的完整結束呢?
方案一: 固定數據長度,客戶端在發送數據的時候,每個數據包的長度固定(比如:1024 個字節),如果發送數據不足 1024 字節時,以空格補齊;服務端則每次讀取固定長度是數據;
方案二: 分隔符,每個數據包的結尾加一個特殊分隔符,服務端則讀取到特殊分隔符則認為數據包結束;如果一次讀取的數據沒有結束符,則保留當前數據,等待下次讀取;
方案三: 將數據分為消息頭和消息體,在頭部保存了消息的數據長度,只有讀取指定長度的數據就算完整數據包;
方案四: 自定義協議,通過協議的規范進行發送和接受數據。
當然,以上的方案 Netty 官方也考慮到了,并且為了簡化開發人員的工作量,Netty 內置了常見的拆包器,具體如下:
1. 固定長度的拆包器 FixedLengthFrameDecoder
每個數據包的長度都是固定的,比如 1024,那么只需要把這個拆包器加到 pipeline 中,Netty 會把一個個長度為 1024 的數據包 (ByteBuf) 傳遞到下一個 channelHandler。
2. 行拆包器 LineBasedFrameDecoder
它是一個特殊的分隔符拆包器,以換行符作為結束符。
3. 分隔符拆包器 DelimiterBasedFrameDecoder
可以自定義自己的分隔符。
4. 基于長度域拆包器 LengthFieldBasedFrameDecoder
是最通用的一種拆包器,有一個存放數據長度的字段,讀到該字段之后,往后面的數據讀取一定長度的數據即可,只要你的自定義協議中包含長度域字段,均可以使用這個拆包器來實現應用層拆包。
5. 小結
本節內容需要掌握的知識點
- 什么是拆包、粘包問題,以及它的產生原因是什么?
- 解決拆包、粘包問題的思路以及常見解決方案是什么?