使用Netty通信時(shí),遇到TCP粘包拆包問題如何解決?答案如此簡(jiǎn)單
這篇文章會(huì)按照以下步驟進(jìn)行講解,希望對(duì)你有所收獲:
1、什么是TCP粘包拆包2、Netty中粘包問題的問題重現(xiàn)3、Netty中粘包問題的解決方案
OK,在你心中有這么一個(gè)基本的脈絡(luò)之后就可以開始今天的文章了。本系列所有的文章都會(huì)給出完整的代碼,且在電腦上真實(shí)運(yùn)行了一遍,確保無誤。
一、什么是TCP拆包和粘包
我們使用TCP協(xié)議在傳輸數(shù)據(jù)的時(shí)候,如果數(shù)據(jù)塊比較大,就會(huì)考慮將其切分。把一個(gè)大的數(shù)據(jù)包進(jìn)行切割成一個(gè)個(gè)小的數(shù)據(jù)包發(fā)送。這時(shí)候就會(huì)遇到拆包和粘包的問題。
比如說在這里客戶端發(fā)送了兩個(gè)數(shù)據(jù)包D1和D2到服務(wù)端,在傳輸?shù)臅r(shí)候就可能會(huì)遇到下列問題:

通過上面這張圖相信你基本上能夠理解了。不過我們?cè)谶@里還是需要稍微解釋一下:
情況1:D1和D2正常發(fā)送,每次發(fā)送一個(gè)整包。
情況2:D1數(shù)據(jù)包比較大,D2比較小。第一次發(fā)送D1的一部分,第二次發(fā)送D1剩下的和D2整包。這叫拆包。
情況2:D1和D2數(shù)據(jù)包都比較小,一次發(fā)送兩個(gè)整包,這就叫做粘包。
情況4:D1數(shù)據(jù)包比較小,D2比較大。第一次發(fā)送D1整包和D2一部分,第二次發(fā)送D2剩下的。這叫拆包。
情況5:D1和D2數(shù)據(jù)包都比較大,這時(shí)候分開發(fā)。
為什么會(huì)出現(xiàn)這樣的問題呢?想要解釋清楚,就必須要考慮到計(jì)算機(jī)網(wǎng)絡(luò)的相關(guān)知識(shí)了,TCP在接受數(shù)據(jù)的時(shí)候,有一個(gè)滑動(dòng)窗口來控制接受數(shù)據(jù)的大小,這個(gè)滑動(dòng)窗口你就可以理解為一個(gè)緩沖區(qū)的大小。緩沖區(qū)滿了就會(huì)把數(shù)據(jù)發(fā)送。數(shù)據(jù)包的大小是不固定的,有時(shí)候比緩沖區(qū)大,有時(shí)候小。這時(shí)候就會(huì)出現(xiàn)上面的現(xiàn)象。
下面我們使用代碼來重現(xiàn)這個(gè)現(xiàn)象。
二、問題重現(xiàn)
1、前提準(zhǔn)備
我們是基于Springboot開發(fā)的,因此還是和上一節(jié)一樣,首先創(chuàng)建一個(gè)Springboot的web工程,添加一下依賴:

如果你沒有使用maven,下載相關(guān)jar包,直接導(dǎo)入IDE中即可。
2、服務(wù)端代碼開發(fā)
步驟一:創(chuàng)建server類
這個(gè)server類,在上一篇文章中提到,是一個(gè)模板類,直接拿來用即可。

在上面的這個(gè)代碼中同樣我們最主要的是關(guān)注ServerUAVHandler的實(shí)現(xiàn)。
步驟二:Handler的實(shí)現(xiàn)

在這個(gè)類中,使用channelRead方法來讀取客戶端發(fā)送過來的信息。
(1)首先定義了一個(gè)counter,用于計(jì)算客戶端發(fā)送了多少條消息。
(2)在channelRead內(nèi)部,首先將msg轉(zhuǎn)化為ByteBuf。
(3)將buf的數(shù)據(jù)轉(zhuǎn)化為字節(jié)byte
(4)將buf的字節(jié)數(shù)據(jù)轉(zhuǎn)化為String類型,然后輸出。
(5)使用ctx的writeAndFlush方法,每收到一個(gè)客戶端的數(shù)據(jù),給對(duì)方回復(fù)一個(gè)A。別忘了還有一個(gè)換行符。
在上面的這個(gè)代碼中,最主要的就是服務(wù)端每收到一條客戶端的信息,就給其回復(fù)一條。也就是說客戶端和服務(wù)端的消息數(shù)量應(yīng)該是一樣的。
3、客戶端代碼開發(fā)
步驟一:創(chuàng)建client類

同樣的代碼的邏輯在上一篇文章中已經(jīng)說了,我們還是最關(guān)注的事件處理類Handler。
步驟二:Handler實(shí)現(xiàn)

這個(gè)客戶端的Handler看起來有點(diǎn)多,一共有兩個(gè)方法,channelActive和channelRead。
(1)channelActive里面使用for循環(huán)給服務(wù)器發(fā)送了100條,我愛你。每次發(fā)送還有在末尾添加一個(gè)換行符。
(2)channelRead里面接受服務(wù)器返回的消息。
按道理來講,客戶端給服務(wù)端發(fā)送了100條數(shù)據(jù),那么服務(wù)端也會(huì)返回回來100條。我們來驗(yàn)證一下。

這里輸出的是服務(wù)端的信息,從上面的輸出結(jié)果你就會(huì)發(fā)現(xiàn),其實(shí)客戶端的“我愛你”都被黏在了一塊。本來100條但是現(xiàn)在卻只有17條了,這就是發(fā)生了粘包現(xiàn)象。
如何來解決呢?下面我們看看。
三、粘包問題解決
解決的思路很簡(jiǎn)單,也就是每次發(fā)送一個(gè)數(shù)據(jù)包的時(shí)候,添加一個(gè)標(biāo)識(shí)符,讀的時(shí)候一直讀到這個(gè)標(biāo)識(shí)符才表示一個(gè)完整的數(shù)據(jù)包。在上面我們添加的是line.separator,也就是換行符“\n”。
1、服務(wù)端server類更改。

2、服務(wù)端Handler類更改

3、客戶端Client更改

4、客戶端Handler更改

客戶端和服務(wù)端改的地方都一樣,不過還是貼了出來,現(xiàn)在我們?cè)龠\(yùn)行一波。

看到?jīng)]是不是很神奇。我們來分析一下我們都修改了什么。
好像我們就只是在server和client類添加了兩個(gè)類,一個(gè)是LineBasedFrameDecoder,一個(gè)是StringDecoder,其他的都是直接刪除,這兩個(gè)類有什么作用呢?
(1)LineBasedFrameDecoder的作用是在讀取數(shù)據(jù)的時(shí)候,一直讀到是否含有換行符“\n”或者是“\r\n”。如果讀到了就表示該結(jié)束了。因此就拿到了這一行的數(shù)據(jù)包。
(2)StringDecoder用于對(duì)之前LineBasedFrameDecoder讀取的這一行數(shù)據(jù)包進(jìn)行解碼。將對(duì)象轉(zhuǎn)換為字符串。
OK,好像他們倆搭配,干活真不累,現(xiàn)在我們終于可以解決粘包的問題了,但是同時(shí)也出現(xiàn)了一個(gè)新的問題,那就是如果我們的標(biāo)識(shí)符不是換行符“\n”或者是“\r\n”又該怎么辦呢?幸好Netty同樣為我們提供了幾種其他的解碼器,叫做DelimiterBasedFrameDecoder和FixedLengthFrameDecoder,前面這個(gè)可以自動(dòng)完成以分隔符做結(jié)束標(biāo)志的消息,后面這個(gè)可以自動(dòng)完成對(duì)定長(zhǎng)消息的解碼。都可以解決粘包拆包問題。