netty粘包和拆包

粘包和拆包是TCP網(wǎng)絡(luò)編程中不可避免的镐依,無論是服務(wù)端還是客戶端眉厨,當(dāng)我們讀取或者發(fā)送消息的時(shí)候滑黔,都需要考慮TCP底層的粘包/拆包機(jī)制。

拆包與粘包同時(shí)發(fā)生在數(shù)據(jù)的發(fā)送方與接收方兩方宵距。

產(chǎn)生原因

發(fā)送方通過網(wǎng)絡(luò)每發(fā)送一批二進(jìn)制數(shù)據(jù)包猜极,那么這次所發(fā)送的數(shù)據(jù)包就稱為一幀,即

Frame消玄。在進(jìn)行基于 TCP 的網(wǎng)絡(luò)傳輸時(shí)跟伏,TCP 協(xié)議會(huì)將用戶真正要發(fā)送的數(shù)據(jù)根據(jù)當(dāng)前緩存

的實(shí)際情況對其進(jìn)行拆分或重組,變?yōu)橛糜诰W(wǎng)絡(luò)傳輸?shù)?Frame翩瓜。在 Netty 中就是將 ByteBuf

中的數(shù)據(jù)拆分或重組為二進(jìn)制的 Frame受扳。而接收方則需要將接收到的 Frame 中的數(shù)據(jù)進(jìn)行重

組或拆分,重新恢復(fù)為發(fā)送方發(fā)送時(shí)的 ByteBuf 數(shù)據(jù)兔跌。

具體場景描述:

  • 發(fā)送方發(fā)送的 ByteBuf 較大勘高,在傳輸之前會(huì)被 TCP 底層拆分為多個(gè) Frame 進(jìn)行發(fā)送,這

個(gè)過程稱為發(fā)送拆包坟桅;接收方在接收到需要將這些 Frame 進(jìn)行合并华望,這個(gè)合并的過程稱

為接收方粘包。

  • 發(fā)送方發(fā)送的 ByteBuf 較小仅乓,無法形成一個(gè) Frame赖舟,此時(shí) TCP 底層會(huì)將很多的這樣的小

的 ByteBuf 合并為一個(gè) Frame 進(jìn)行傳輸,這個(gè)合并的過程稱為發(fā)送方的粘包夸楣;接收方在

接收到這個(gè) Frame 后需要進(jìn)行拆包宾抓,拆分出多個(gè)原來的小的 ByteBuf,這個(gè)拆分的過程

稱為接收方拆包豫喧。

  • 當(dāng)一個(gè) Frame 無法放入整數(shù)倍個(gè) ByteBuf 時(shí)石洗,最后一個(gè) ByteBuf 會(huì)會(huì)發(fā)生拆包。這個(gè)

ByteBuf 中的一部分入入到了一個(gè) Frame 中紧显,另一部分被放入到了另一個(gè) Frame 中讲衫。這

個(gè)過程就是發(fā)送方拆包。但對于將這些 ByteBuf 放入到一個(gè) Frame 的過程孵班,就是發(fā)送方

粘包涉兽;當(dāng)接收方在接收到兩個(gè) Frame 后,對于第一個(gè) Frame 的最后部分重父,與第二個(gè) Frame

的最前部分會(huì)進(jìn)行合并花椭,這個(gè)合并的過程就是接收方粘包。但在將 Frame 中的各個(gè)

ByteBuf 拆分出來的過程房午,就是接收方拆包。

具體情況如下圖所示:

netty粘包拆包.png

解決方案

固定長度

對于使用固定長度的粘包和拆包場景丹允,可以使用:

FixedLengthFrameDecoder:每次讀取固定長度的消息郭厌,如果當(dāng)前讀取到的消息不足指定長度袋倔,那么就會(huì)等待下一個(gè)消息到達(dá)后進(jìn)行補(bǔ)足。其使用也比較簡單折柠,只需要在構(gòu)造函數(shù)中指定每個(gè)消息的長度即可宾娜。

bootstrap.group(parentGroup, childGroup)
         .channel(NioServerSocketChannel.class)
         .option(ChannelOption.SO_BACKLOG, 1024)
        //接收套接字緩沖區(qū)大小
        .option(ChannelOption.SO_RCVBUF, 1024 * 1024)
        //發(fā)送套接字緩沖區(qū)大小
        .option(ChannelOption.SO_SNDBUF, 1024 * 1024)
        .option(ChannelOption.SO_KEEPALIVE, true)
        .option(ChannelOption.TCP_NODELAY, true)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                // 這里將FixedLengthFrameDecoder添加到pipeline中,指定長度為100
                pipeline.addLast(new FixedLengthFrameDecoder(100));
                // StringEncoder:字符串編碼器扇售,將String編碼為將要發(fā)送到Channel中的ByteBuf
                pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
                // StringDecoder:字符串解碼器前塔,將Channel中的ByteBuf數(shù)據(jù)解碼為String
                pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
                //綁定處理器(可綁定多個(gè))
                pipeline.addLast(new ServerHandler()); //處理業(yè)務(wù)
            }
        });
行拆包

LineBasedFrameDecoder:每個(gè)應(yīng)用層數(shù)據(jù)包,都以換行符作為分隔符承冰,進(jìn)行分割拆分华弓,LineBasedFrameDecoder依次遍歷ByteBuf中的可讀字節(jié),判斷是否有"\n"或者"\r\n"困乒,如果有就以此位置為結(jié)束位置寂屏,從可讀索引到結(jié)束位置區(qū)間的字節(jié)就組成了一行,它是以換行符為結(jié)束標(biāo)志的解碼器娜搂,支持?jǐn)y帶結(jié)束符或者不攜帶結(jié)束符兩種解碼方式迁霎,同時(shí)支持配置單行的最大長度,如果連續(xù)讀到最大長度后仍然沒有發(fā)現(xiàn)換行符百宇,就會(huì)拋出異常考廉,同時(shí)忽略掉之前讀到的異常碼流。這個(gè)使用也比較簡單:

childHandler(new ChannelInitializer<SocketChannel>() {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 這里將FixedLengthFrameDecoder添加到pipeline中携御,指定長度為100
        //pipeline.addLast(new FixedLengthFrameDecoder(100));
        //這里將LineBasedFrameDecoder添加到pipeline中芝此,設(shè)置最大長度為1024
        pipeline.addLast(new LineBasedFrameDecoder(1024));
        // StringEncoder:字符串編碼器,將String編碼為將要發(fā)送到Channel中的ByteBuf
        pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
        // StringDecoder:字符串解碼器因痛,將Channel中的ByteBuf數(shù)據(jù)解碼為String
        pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
        //綁定處理器(可綁定多個(gè))
        pipeline.addLast(new ServerHandler()); //處理業(yè)務(wù)
    }
});
指定分隔符

對于通過分隔符進(jìn)行粘包和拆包問題的處理婚苹,Netty提供了

DelimiterBasedFrameDecoder:通過用戶指定的分隔符對數(shù)據(jù)進(jìn)行粘包和拆包處理,用法如下:

childHandler(new ChannelInitializer<SocketChannel>() {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 這里將FixedLengthFrameDecoder添加到pipeline中鸵膏,指定長度為100
        // pipeline.addLast(new FixedLengthFrameDecoder(100));
        //這里將LineBasedFrameDecoder添加到pipeline中膊升,設(shè)置最大長度為1024
        // pipeline.addLast(new LineBasedFrameDecoder(1024));

        //被按照$_$進(jìn)行分隔,這里1024指的是分隔的最大長度谭企,即當(dāng)讀取到1024個(gè)字節(jié)的數(shù)據(jù)之后廓译,
        // 若還是未讀取到分隔符,則舍棄當(dāng)前數(shù)據(jù)段债查,因?yàn)槠浜苡锌赡苁怯捎诖a流紊亂造成的
        ByteBuf delimiter = copiedBuffer(Constants.MESSAGE_DELIMITER.getBytes(Charset.forName("UTF-8")));
        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));

        // StringEncoder:字符串編碼器非区,將String編碼為將要發(fā)送到Channel中的ByteBuf
        pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
        // StringDecoder:字符串解碼器,將Channel中的ByteBuf數(shù)據(jù)解碼為String
        pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
        //綁定處理器(可綁定多個(gè))
        pipeline.addLast(new ServerHandler()); //處理業(yè)務(wù)
    }
});
基于數(shù)據(jù)包長度的拆包

LengthFieldBasedFrameDecoder:將應(yīng)用層數(shù)據(jù)包的長度盹廷,作為接收端應(yīng)用層數(shù)據(jù)包的拆分依據(jù)征绸。按照應(yīng)用層數(shù)據(jù)包的大小,拆包。這個(gè)拆包器管怠,有一個(gè)要求淆衷,就是應(yīng)用層協(xié)議中包含數(shù)據(jù)包的長度,應(yīng)用如下:

childHandler(new ChannelInitializer<SocketChannel>() {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 這里將LengthFieldBasedFrameDecoder添加到pipeline的首位渤弛,因?yàn)槠湫枰獙邮盏降臄?shù)據(jù)
        // 進(jìn)行長度字段解碼祝拯,這里也會(huì)對數(shù)據(jù)進(jìn)行粘包和拆包處理
        pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
        // LengthFieldPrepender是一個(gè)編碼器,主要是在響應(yīng)字節(jié)數(shù)據(jù)前面添加字節(jié)長度字段
        pipeline.addLast(new LengthFieldPrepender(2));
        // StringEncoder:字符串編碼器她肯,將String編碼為將要發(fā)送到Channel中的ByteBuf
        pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
        // StringDecoder:字符串解碼器佳头,將Channel中的ByteBuf數(shù)據(jù)解碼為String
        pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
        //綁定處理器(可綁定多個(gè))
        pipeline.addLast(new ServerHandler()); //處理業(yè)務(wù)
    }
});
自定義粘包拆包器

可以通過實(shí)現(xiàn)MessageToByteEncoderByteToMessageDecoder來實(shí)現(xiàn)自定義粘包和拆包處理的目的。

  • MessageToByteEncoder:作用是將響應(yīng)數(shù)據(jù)編碼為一個(gè)ByteBuf對象

  • ByteToMessageDecoder:將接收到的ByteBuf數(shù)據(jù)轉(zhuǎn)換為某個(gè)對象數(shù)據(jù)

最后我們也可以自定義編碼器MessageToMessageEncoder和自定義解碼器MessageToMessageDecoder晴氨,來實(shí)現(xiàn)消息內(nèi)容的轉(zhuǎn)換康嘉,比如序列化成某個(gè)對象,處理器里面我們就可以不用再去轉(zhuǎn)換對象瑞筐,具體實(shí)現(xiàn)如下:

自定義解碼器
/**
 * @Description:  自定義解碼器
 * @author: dy
 */
public class CustomDecoder extends MessageToMessageDecoder<ByteBuf> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        System.out.println("====1111111111===="+msg.toString(Charset.forName("UTF-8")));
        out.add(JSON.parseObject(msg.toString(Charset.forName("UTF-8")), Message.class));
    }
}

使用只需要在構(gòu)造器里面加入我們自定義的編碼器就可以了:

childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                   
                            // 這里將LengthFieldBasedFrameDecoder添加到pipeline的首位凄鼻,因?yàn)槠湫枰獙邮盏降臄?shù)據(jù)
                            // 進(jìn)行長度字段解碼,這里也會(huì)對數(shù)據(jù)進(jìn)行粘包和拆包處理
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
                            // LengthFieldPrepender是一個(gè)編碼器聚假,主要是在響應(yīng)字節(jié)數(shù)據(jù)前面添加字節(jié)長度字段
                            pipeline.addLast(new LengthFieldPrepender(2));
                            // StringEncoder:字符串編碼器块蚌,將message對象編碼為將要發(fā)送到Channel中的ByteBuf
                            pipeline.addLast(new CustomDecoder());
                             // StringDecoder:字符串解碼器,將Channel中的ByteBuf數(shù)據(jù)解碼為String
                            pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
                            //綁定處理器(可綁定多個(gè))
                            pipeline.addLast(new ServerHandler()); //處理業(yè)務(wù)
                        }
                    });
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末膘格,一起剝皮案震驚了整個(gè)濱河市峭范,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘪贱,老刑警劉巖纱控,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異菜秦,居然都是意外死亡甜害,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門球昨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尔店,“玉大人,你說我怎么就攤上這事主慰∠荩” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵共螺,是天一觀的道長该肴。 經(jīng)常有香客問我,道長藐不,這世上最難降的妖魔是什么匀哄? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任秦效,我火速辦了婚禮,結(jié)果婚禮上拱雏,老公的妹妹穿的比我還像新娘棉安。我一直安慰自己底扳,他們只是感情好铸抑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衷模,像睡著了一般鹊汛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上阱冶,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天刁憋,我揣著相機(jī)與錄音,去河邊找鬼木蹬。 笑死至耻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的镊叁。 我是一名探鬼主播尘颓,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼晦譬!你這毒婦竟也來了疤苹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤敛腌,失蹤者是張志新(化名)和其女友劉穎卧土,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體像樊,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡尤莺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了生棍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颤霎。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖足绅,靈堂內(nèi)的尸體忽然破棺而出捷绑,到底是詐尸還是另有隱情,我是刑警寧澤氢妈,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布粹污,位于F島的核電站,受9級特大地震影響首量,放射性物質(zhì)發(fā)生泄漏壮吩。R本人自食惡果不足惜进苍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸭叙。 院中可真熱鬧觉啊,春花似錦、人聲如沸沈贝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宋下。三九已至嗡善,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間学歧,已是汗流浹背罩引。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枝笨,地道東北人袁铐。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像横浑,于是被迫代替她去往敵國和親剔桨。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容