TCP粘包/拆包

TCP粘包/拆包

TCP編程底層都有粘包和拆包機制,由於咱們在C/S這種傳輸模型下,以TCP協議傳輸的時候,在網絡中的byte其實就像是河水,TCP就像一個搬運工,將這流水從一端轉送到另外一端,這時又分兩種狀況:

1. 若是客戶端的每次製造的水比較多,也就是咱們常說的客戶端給的包比較大,TCP這個搬運工就會分屢次去搬運
2. 若是客戶端每次製造的水比較少的話,TCP可能會等客戶端屢次生產以後,把全部的水一塊兒再運輸到另外一端

上述第一種狀況,就是須要咱們進行粘包,在另外一端接收的時候,須要把屢次獲取的結果粘在一塊兒,變成咱們能夠理解的信息,第二種狀況,咱們在另外一端接收的時候,就必須進行拆包處理,由於每次接收的信息,多是另外一個遠程端屢次發送的包,被TCP粘在一塊兒的

咱們考慮一下這樣的狀況:咱們編寫了一個機器人控制程序,經過一個遙控器(客戶端)向機器人(服務器)創建了一個長鏈接,並經過這個鏈接接二連三的從遙控器發送控制指令給機器人。因爲是連續控制指令,因此指令與指令之間沒有間隔(實際上您還能夠想一想不少相似場景,例如:開發的Online對戰遊戲)。

咱們使用JSON格式做爲指令數據的承載格式。那麼發送方和接收方的數據發送-接受過程可能以下圖所示。

zhangzeli

經過上圖咱們看到了接收方爲了接受這兩條連貫的指令,一共作了三次接受,第二次接收的時候,收到了一部分message1的內容和一部分message2的內容。這裏要說明幾個注意事項:

1. MSS:MSS屬性是TCP鏈接雙方在三次握手時所確認的每個TCP報文段中數據字段的最大長度。注意,一是鏈接雙方協商出來的;二是隻是數據段的最大長度,不包括IP協議頭和TCP協議頭的最大長度。

2. 半包是指接收方應用程序在接收信息時,沒有接收到一個完成的信息格式塊;粘包是指,接收方應用程序在接受信息時,除了接收到發送方應用程序發送的某一個完整數據信息描述外,還接受到了一下發送方應用程序發送的下一個數據信息的一部分。

3. 半包和粘包是針對應用程序來講的,這個問題只會發生在TCP一些進行連續發送數據時(TCP長鏈接)。UDP不會出現這個問題,由於UDP都是有邊界的數據報;TCP短鏈接也不會出現,由於發送完一個指令信息後鏈接就斷開了,不會發送第二個指令數據。

4. 半包和粘包問題產生的根本是由於TCP本質上沒有「數據塊」的概念,而是一連串的數據流。在應用程序層面上咱們所定義的「數據塊」在TCP層面上並不被協議承認

5. 半包/粘包是一個應用層問題。要解決半包/粘包問題,就是在應用程序層面創建協商一致的信息還原依據。常見的有兩種方式:一是消息定長,即保證每個完整的信息描述的長度都是必定的,這樣不管TCP/IP協議如何進行分片,
數據接收方均可以按照固定長度進行消息的還原。二是在完整的一塊數據結束後增長協商一致的分隔符(例如增長一個回車符)。

在JAVA NIO技術框架中,半包和粘包問題咱們須要本身解決,若是使用Netty框架,它其中提供了多種解碼方式的封裝幫助咱們解決半包和粘包問題。甚至針對不一樣的數據格式,Netty都提供了半包和粘包問題的現成解決方式,例如以前咱們提到的ProtobufVarint32FrameDecoder解碼方式,就是專門解決Protobuf數據格式在TCP長鏈接傳輸時的半包問題的。java

下面咱們會介紹FixedLengthFrameDecoder、DelimiterBasedFrameDecoder、LineBasedFrameDecoder來解決半包/粘包的問題。

  1. 使用FixedLengthFrameDecoder解決問題
//FixedLengthFrameDecoder解碼處理器將TCP/IP的數據按照指定的長度進行從新拆分,若是接收到的數據不知足設置的固定長度,Netty將等待新的數據到達:
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        ch.pipeline().addLast(new ByteArrayEncoder());
        ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
        ch.pipeline().addLast(new TCPServerHandler());
        ch.pipeline().addLast(new ByteArrayDecoder());
    }
});

Netty上層的channelRead事件方法將在Channel接收到20個字符的狀況下被觸發;而若是剩餘的內容不到20個字符,channelRead方法將不會被觸發(但注意channelReadComplete方法會觸發的啦)。linux

  1. 使用LineBasedFrameDecoder解決問題
//LineBasedFrameDecoder,基於最簡單的「換行符」進行接收到的信息的再組織。windows和linux兩個操做系統中的「換行符」是不同的,LineBasedFrameDecoder都支持。固然這個類沒有咱們後面介紹的DelimiterBasedFrameDecoder類靈活。
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        ch.pipeline().addLast(new ByteArrayEncoder());
        ch.pipeline().addLast(new LineBasedFrameDecoder(100));
        ch.pipeline().addLast(new TCPServerHandler());
        ch.pipeline().addLast(new ByteArrayDecoder());
    }
});

那麼若是客戶端發送的數據是: this is 0 client \r\n request 1 \r\n」 那麼接收方從新經過「換行符」從新組織後,將分兩次接受到數據: this is 0 client request 1編程

  1. 使用DelimiterBasedFrameDecoder解決問題
//DelimiterBasedFrameDecoder是按照「自定義」分隔符(也能夠是「回車符」或者「空字符」注意windows系統中和linux系統中「回車符」的表示是不同的)進行信息的從新拆分。

serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        ch.pipeline().addLast(new ByteArrayEncoder());
        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1500, false, Delimiters.lineDelimiter()));
        ch.pipeline().addLast(new TCPServerHandler());
        ch.pipeline().addLast(new ByteArrayDecoder());
    }
});

DelimiterBasedFrameDecoder有三個參數,這裏介紹一下: DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf... delimiters) maxFrameLength:最大分割長度,若是接收方在一段長度 大於maxFrameLength的數據段中,沒有找到指定的分隔符,那麼這個處理器會拋出TooLongFrameException異常。 stripDelimiter:這個是一個布爾型參數,指代是否保留指定的分隔符。windows

相關文章
相關標籤/搜索