netty系列之:基於流的資料傳輸

flydean發表於2021-08-10

簡介

我們知道由兩種資料的傳輸方式,分別是字元流和位元組流,字元流的意思是傳輸的物件就是字串,格式已經被設定好了,傳送方和接收方按照特定的格式去讀取就行了,而位元組流是指將資料作為最原始的二進位制位元組來進行傳輸。

今天給大家介紹一下在netty中的基於流的資料傳輸。

package和byte

熟悉TCP/IP協議的同學應該知道,在TCP/IP中,因為底層協議有支援的資料包的最大值,所以對於大資料傳輸來說,需要對資料進行拆分和封包處理,並將這些拆分組裝過的包進行傳送,最後在接收方對這些包進行組合。在各個包中有固定的結構,所以接收方可以很清楚的知道到底應該組合多少個包作為最終的結果。

那麼對於netty來說,channel中傳輸的是ByteBuf,實際上最最最底層的就是byte陣列。對於這種byte陣列來說,接收方並不知道到底應該組合多少個byte來合成原來的訊息,所以需要在接收端對收到的byte進行組合,從而生成最終的資料。

那麼對於netty中的byte資料流應該怎麼組合呢?我們接下來看兩種組合方法。

手動組合

這種組合的方式的基本思路是構造一個目標大小的ByteBuf,然後將接收到的byte通過呼叫ByteBuf的writeBytes方法寫入到ByteBuf中。最後從ByteBuf中讀取對應的資料。

比如我們想從服務端傳送一個int數字給客戶端,一般來說int是32bits,然後一個byte是8bits,那麼一個int就需要4個bytes組成。

在server端,可以建立一個byte的陣列,陣列中包含4個元素。將4個元素的byte傳送給客戶端,那麼客戶端該如何處理呢?

首先我們需要建立一個clientHander,這個handler應該繼承ChannelInboundHandlerAdapter,並且在其handler被新增到ChannelPipeline的時候初始化一個包含4個byte的byteBuf。

handler被新增的時候會觸發一個handlerAdded事件,所以我們可以這樣寫:

    private ByteBuf buf;
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        //建立一個4個byte的緩衝器
        buf = ctx.alloc().buffer(4); 
    }

上例中,我們從ctx分配了一個4個位元組的緩衝器,並將其賦值給handler中的私有變數buf。

當handler執行完畢,從ChannelPipeline中刪除的時候,會觸發handlerRemoved事件,在這個事件中,我們可以對分配的Bytebuf進行清理,通常來說,可以呼叫其release方法,如下所示:

    public void handlerRemoved(ChannelHandlerContext ctx) {
        buf.release(); // 釋放buf
        buf = null;
    }

然後最關鍵的一步就是從channel中讀取byte並將其放到4個位元組的byteBuf中。在之前的文章中我們提到了,可以在channelRead方法中,處理訊息讀取的邏輯。

    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        buf.writeBytes(m); // 寫入一個byte
        m.release();
        
        if (buf.readableBytes() >= 4) { // 已經湊夠4個byte,將4個byte組合稱為一個int
            long result = buf.readUnsignedInt();
            ctx.close();
        }
    }

每次觸發channelRead方法,都會將讀取到的一個位元組的byte通過呼叫writeBytes方法寫入buf中。當buf的可讀byte大於等於4個的時候就說明4個位元組已經讀滿了,可以對其進行操作了。

這裡我們將4個位元組組合成一個unsignedInt,並使用readUnsignedInt方法從buf中讀取出來組合稱為一個int數字。

上面的例子雖然可以解決4個位元組的byte問題,但是如果資料結構再負責一點,上面的方式就會力不從心,需要考慮太多的資料組合問題。接下來我們看另外一種方式。

Byte的轉換類

netty提供了一個ByteToMessageDecoder的轉換類,可以方便的對Byte轉換為其他的型別。

我們只需要重新其中的decode方法,就可以實現對ByteBuf的轉換:

       public class SquareDecoder extends ByteToMessageDecoder {
            @Override
           public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
                   throws Exception {
               out.add(in.readBytes(in.readableBytes()));
           }
       }

上面的例子將byte從input轉換到output中,當然,你還可以在上面的方法中進行格式轉換,如下所示:

public class TimeDecoder extends ByteToMessageDecoder { 
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { 
        if (in.readableBytes() < 4) {
            return; 
        }
        
        out.add(in.readBytes(4)); 
    }
}

上面的例子會先判斷in中是否有4個byte,如果有就將其讀出來放到out中去。那麼有同學會問了,輸入不是一個byte一個byte來的嗎?為什麼這裡可以一次讀取到4個byte?這是因為ByteToMessageDecoder內建了一個快取裝置,所以這裡的in實際上是一個快取集合。

ReplayingDecoder

netty還提供了一個更簡單的轉換ReplayingDecoder,如果使用ReplayingDecoder重新上面的邏輯就是這樣的:

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

只需要一行程式碼即可。

事實上ReplayingDecoder 是ByteToMessageDecoder 的子類,是在ByteToMessageDecoder上豐富了一些功能的結果。

他們兩的區別在於ByteToMessageDecoder 還需要通過呼叫readableBytes來判斷是否有足夠的可以讀byte,而使用ReplayingDecoder直接讀取即可,它假設的是所有的bytes都已經接受成功了。

比如下面使用ByteToMessageDecoder的程式碼:

   public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder {
  
      @Override
     protected void decode(ChannelHandlerContext ctx,
                             ByteBuf buf, List<Object> out) throws Exception {
  
       if (buf.readableBytes() < 4) {
          return;
       }
  
       buf.markReaderIndex();
       int length = buf.readInt();
  
       if (buf.readableBytes() < length) {
          buf.resetReaderIndex();
          return;
       }
  
       out.add(buf.readBytes(length));
     }
   }
   

上例假設在byte的頭部是一個int大小的陣列,代表著byte陣列的長度,需要先讀取int值,然後再根據int值來讀取對應的byte資料。

和下面的程式碼是等價的:

   public class IntegerHeaderFrameDecoder
        extends ReplayingDecoder<Void> {
  
     protected void decode(ChannelHandlerContext ctx,
                             ByteBuf buf, List<Object> out) throws Exception {
  
       out.add(buf.readBytes(buf.readInt()));
     }
   }
   

上面程式碼少了判斷的步驟。

那麼這是怎麼實現的呢?

事實上ReplayingDecoder 會傳遞一個會丟擲 Error的 ByteBuf , 當 ByteBuf 讀取的byte個數不滿足要求的時候,會丟擲異常,當ReplayingDecoder 捕獲到這個異常之後,會重置buffer的readerIndex到最初的狀態,然後等待後續的資料進來,然後再次呼叫decode方法。

所以,ReplayingDecoder的效率會比較低,為了解決這個問題,netty提供了checkpoint() 方法。這是一個儲存點,當報錯的時候,可以不會退到最初的狀態,而是回退到checkpoint() 呼叫時候儲存的狀態,從而可以減少不必要的浪費。

總結

本文介紹了在netty中進行stream操作和變換的幾種方式,希望大家能夠喜歡。

本文已收錄於 http://www.flydean.com/07-netty-stream-based-transport/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章