1. 程式人生 > >Netty中粘包和拆包的解決方案

Netty中粘包和拆包的解決方案

粘包和拆包是TCP網路程式設計中不可避免的,無論是服務端還是客戶端,當我們讀取或者傳送訊息的時候,都需要考慮TCP底層的粘包/拆包機制。

TCP粘包和拆包

TCP是個“流”協議,所謂流,就是沒有界限的一串資料。TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是所謂的TCP粘包和拆包問題。

img

如圖所示,假設客戶端分別傳送了兩個資料包D1和D2給服務端,由於服務端一次讀取到的位元組數是不確定的,故可能存在以下4種情況。

  1. 服務端分兩次讀取到了兩個獨立的資料包,分別是D1和D2,沒有粘包和拆包;
  2. 服務端一次接收到了兩個資料包,D1和D2粘合在一起,被稱為TCP粘包;
  3. 服務端分兩次讀取到了兩個資料包,第一次讀取到了完整的D1包和D2包的部分內容,第二次讀取到了D2包的剩餘內容,這被稱為TCP拆包
  4. 服務端分兩次讀取到了兩個資料包,第一次讀取到了D1包的部分內容D1_1,第二次讀取到了D1包的剩餘內容D1_2和D2包的整包。

如果此時服務端TCP接收滑窗非常小,而資料包D1和D2比較大,很有可能會發生第五種可能,即服務端分多次才能將D1和D2包接收完全,期間發生多次拆包。

TCP粘包和拆包產生的原因

資料從傳送方到接收方需要經過作業系統的緩衝區,而造成粘包和拆包的主要原因就在這個緩衝區上。粘包可以理解為緩衝區資料堆積,導致多個請求資料粘在一起,而拆包可以理解為傳送的資料大於緩衝區,進行拆分處理。

img

詳細來說,造成粘包和拆包的原因主要有以下三個:

  1. 應用程式write寫入的位元組大小大於套介面傳送緩衝區大小
  2. 進行MSS大小的TCP分段
  3. 乙太網幀的payload大於MTU進行IP分片。

img

粘包和拆包的解決方法

由於底層的TCP無法理解上層的業務資料,所以在底層是無法保證資料包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,可以歸納如下。

  1. 訊息長度固定,累計讀取到長度和為定長LEN的報文後,就認為讀取到了一個完整的資訊
  2. 將回車換行符作為訊息結束符
  3. 將特殊的分隔符作為訊息的結束標誌,回車換行符就是一種特殊的結束分隔符
  4. 通過在訊息頭中定義長度欄位來標識訊息的總長度

Netty中的粘包和拆包解決方案

針對上一小節描述的粘包和拆包的解決方案,對於拆包問題比較簡單,使用者可以自己定義自己的編碼器進行處理,Netty並沒有提供相應的元件。對於粘包的問題,由於拆包比較複雜,程式碼比較處理比較繁瑣,Netty提供了4種解碼器來解決,分別如下:

  1. 固定長度的拆包器 FixedLengthFrameDecoder,每個應用層資料包的都拆分成都是固定長度的大小
  2. 行拆包器 LineBasedFrameDecoder,每個應用層資料包,都以換行符作為分隔符,進行分割拆分
  3. 分隔符拆包器 DelimiterBasedFrameDecoder,每個應用層資料包,都通過自定義的分隔符,進行分割拆分
  4. 基於資料包長度的拆包器 LengthFieldBasedFrameDecoder,將應用層資料包的長度,作為接收端應用層資料包的拆分依據。按照應用層資料包的大小,拆包。這個拆包器,有一個要求,就是應用層協議中包含資料包的長度

以上解碼器在使用時只需要新增到Netty的責任鏈中即可,大多數情況下這4種解碼器都可以滿足了,當然除了以上4種解碼器,使用者也可以自定義自己的解碼器進行處理。具體可以參考以下程式碼示例:

// Server主程式
public class XNettyServer {
  public static void main(String[] args) throws Exception {
    // accept 處理連線的執行緒池
    NioEventLoopGroup acceptGroup = new NioEventLoopGroup();
    // read io 處理資料的執行緒池
    NioEventLoopGroup readGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap serverBootstrap = new ServerBootstrap();
      serverBootstrap
          .group(acceptGroup, readGroup)
          .channel(NioServerSocketChannel.class)
          .childHandler(
              new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                  ChannelPipeline pipeline = ch.pipeline();

                  // 增加解碼器
                  pipeline.addLast(new XDecoder());

                  // 打印出內容 handdler
                  pipeline.addLast(new XHandler());
                }
              });
      System.out.println("啟動成功,埠 7777");
      serverBootstrap.bind(7777).sync().channel().closeFuture().sync();
    } finally {
      acceptGroup.shutdownGracefully();
      readGroup.shutdownGracefully();
    }
  }
}


// 解碼器
public class XDecoder extends ByteToMessageDecoder {

  static final int PACKET_SIZE = 220;

  // 用來臨時保留沒有處理過的請求報文
  ByteBuf tempMsg = Unpooled.buffer();

  /**
   * @param ctx
   * @param in 請求的資料
   * @param out 將粘在一起的報文拆分後的結果保留起來
   * @throws Exception
   */
  @Override
  protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    System.out.println(Thread.currentThread() + "收到了一次資料包,長度是:" + in.readableBytes());

    // 合併報文
    ByteBuf message = null;
    int tmpMsgSize = tempMsg.readableBytes();
    // 如果暫存有上一次餘下的請求報文,則合併
    if (tmpMsgSize > 0) {
      message = Unpooled.buffer();
      message.writeBytes(tempMsg);
      message.writeBytes(in);
      System.out.println("合併:上一資料包餘下的長度為:" + tmpMsgSize + ",合併後長度為:" + message.readableBytes());
    } else {
      message = in;
    }

    int size = message.readableBytes();
    int counter = size / PACKET_SIZE;
    for (int i = 0; i < counter; i++) {
      byte[] request = new byte[PACKET_SIZE];
      // 每次從總的訊息中讀取220個位元組的資料
      message.readBytes(request);

      // 將拆分後的結果放入out列表中,交由後面的業務邏輯去處理
      out.add(Unpooled.copiedBuffer(request));
    }

    // 多餘的報文存起來
    // 第一個報文: i+  暫存
    // 第二個報文: 1 與第一次
    size = message.readableBytes();
    if (size != 0) {
      System.out.println("多餘的資料長度:" + size);
      // 剩下來的資料放到tempMsg暫存
      tempMsg.clear();
      tempMsg.writeBytes(message.readBytes(size));
    }
  }
}


// 處理器
public class XHandler extends ChannelInboundHandlerAdapter {

  @Override
  public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    ctx.flush();
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf byteBuf = (ByteBuf) msg;
    byte[] content = new byte[byteBuf.readableBytes()];
    byteBuf.readBytes(content);
    System.out.println(Thread.currentThread() + ": 最終列印" + new String(content));
    ((ByteBuf) msg).release();
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    cause.printStackTrace();
    ctx.close();
  }
}

相關推薦

Netty解決方案

粘包和拆包是TCP網路程式設計中不可避免的,無論是服務端還是客戶端,當我們讀取或者傳送訊息的時候,都需要考慮TCP底層的粘包/拆包

Netty處理TCP

什麼是粘包和拆包 TCP是個”流”協議,流其實就是沒有界限的一串資料。 TCP底層中並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包劃分,所以在TCP中就有可能一個完整地包會被TCP拆分成多個包,也有可能吧多個小的包封裝成一個大的資料包傳

Netty解決問題的四種方案

兩個 json反序列 obj 合並 間隔 分享圖片 發送數據 返回 想是 在RPC框架中,粘包和拆包問題是必須解決一個問題,因為RPC框架中,各個微服務相互之間都是維系了一個TCP長連接,比如dubbo就是一個全雙工的長連接。由於微服務往對方發送信息的時候,所有的請求都是使

Netty入門系列(2) --使用Netty解決問題

前言 上一篇我們介紹瞭如果使用Netty來開發一個簡單的服務端和客戶端,接下來我們來討論如何使用解碼器來解決TCP的粘包和拆包問題 TCP為什麼會粘包/拆包 我們知道,TCP是以一種流的方式來進行網路轉播的,當tcp三次握手簡歷通訊後,客戶端服務端之間就建立了一種通訊管道,我們可以想象成自來水管道,流出來的水

Netty學習10-

1 粘包拆包基本概念 TPC是一個面向流的協議。所謂流就是沒有邊界的一串資料,如同河水般連成一片,其中並沒有分界線。TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的具體情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆成多個包傳送,也有可能

Unity Socket傳輸 TCP原因以及解決策略

3. 乙太網的payload大於MTU進行IP分片。MTU指:一種通訊協議的某一層上面所能通過的最大資料包大小。如果IP層有一個數據包要傳,而且資料的長度比鏈路層的MTU大,那麼IP層就會進行分片,把資料包分成若干片,讓每一片都不超過MTU。注意,IP分片可以發生在原始傳送端主機上,也可以發生在中間路由器上

Tcp的原因

最近研究Netty網路程式設計,以前專案中也遇到過資料接收過程中資料質量太差問題,很可能是TCP傳輸過程中問題,特此記錄。 問題產生 一個完整的業務可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這個就是TCP的拆包和封包問

tcp、斷

    while (1) {       var packLen = _.unpack('packLen', tcpBuffer.slice(0, 2));       len = packLen.len;       // console.log('pack len------:' + len);    

tcp的處理方案

隨著智慧硬體越來越流行,很多後端開發人員都有可能接觸到socket程式設計。而很多情況下,伺服器與端上需要保證資料的有序,穩定到達,自然而然就會選擇基於tcp/ip協議的socekt開發。開發過程中,經常會遇到tcp粘包,拆包的問題,本文將從產生原因,和解決方案以及work

CocoaAsyncSocket + Protobuf 處理問題

 在上一篇文章《iOS之ProtocolBuffer搭建和示例demo》分享環境的搭建, 我們和伺服器進行IM通訊用了github有名的框架CocoaAsynSocket, 然後和伺服器之間的資料媒介是ProtoBuf。然後後面在開發的過程中也碰到了拆包和粘包問題,這方面

SOCKET 封

資源 isl 個數 遊戲服務器 指正 長度 num 部分 程序開發 對於基於TCP開發的通訊程序,有個很重要的問題需要解決,就是封包和拆包.自從我從事網絡通訊編程工作以來(大概有三年的時間了),我一直在思索和改進封包和拆包的方法.下面就針對這個問題談談我的想法,拋磚引玉.若

描述在IPSec傳輸模式下ESP報文裝過程

        AH(Authentication Header):提供資料完整性驗證,通過Hash實現;資料來源身份認證,在計算驗證碼時加入共享金鑰;防止重放攻擊,AH包頭的序列號可防止重放攻擊。ESP(Encapsulating Security Payload):ESP的協議號是50,提供AH的三種服務

django同源策略跨域解決方案

一  同源策略 1.1何謂同源? 如果兩個頁面的協議,埠(如果有指定)和域名都相同,則兩個頁面具有相同的源。 舉個例子: 下表給出了相對http://a.xyz.com/dir/page.html同源檢測的示例:  1.2什麼是同源策略? 同源策略是瀏覽

netty權威指南》之問題及解決方案1

客戶端和服務端程式碼 package com.lyzx.netty.netty02; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.net

3、netty(二)

list ride int 簡化 iter getclass null message som 上篇博客留了個尾巴“而且LineBasedFrameDecoder據說還有一種不要求攜帶結束符的解碼方式”,今天就從源碼來看看是怎麽回事。 一、基本原理 如果沒有netty,

3.Netty(二)

length coder 解碼器 generate trac main med con ++ Netty提供的TCP數據拆包、粘包解決方案 1.前言 關於TCP的數據拆包、粘包的介紹,我在上一篇文章裏面已經有過介紹。 想要了解一下的,請點擊這裏 Chick Here! 今天

2.Netty(一)

Netty粘包、拆包 1.什麼是拆包、粘包 (1)拆包、粘包介紹 TCP是個“流”協議,所謂流,就是沒有界限的一串資料。大家可以想想河裡的流水,是連成一片的,其間並沒有分界線。TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆

關於TCP解決方法

在進行Java NIO學習時,發現,如果客戶端連續不斷的向服務端傳送資料包時,服務端接收的資料會出現兩個資料包粘在一起的情況,這就是TCP協議中經常會遇到的粘包以及拆包的問題。 我們都知道TCP屬於傳輸層的協議,傳輸層除了有TCP協議外還有UDP協議。那麼UDP是否會發生粘包或拆包的現象呢?答案

TCP解決方法

在進行Java NIO學習時,發現,如果客戶端連續不斷的向服務端傳送資料包時,服務端接收的資料會出現兩個資料包粘在一起的情況,這就是TCP協議中經常會遇到的粘包以及拆包的問題。 我們都知道TCP屬於傳輸層的協議,傳輸層除了有TCP協議外還有UDP協議。那麼UDP

Java網路程式設計之Netty-yellowcong

Netty中,解決拆包和黏包中,解決方式有三種 1、在每個包尾部,定義分隔符,通過回車符號,或者其他符號來解決 2、通過定義每個包的大小,如果包不夠就空格填充 3、自定義協議的方式,將訊息分為訊息頭和訊息體,在訊息頭中表示出訊息的總長度,