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