Netty4 自定義Decoder,Encoder進行對象傳遞


   首先我們必須知道Tcp粘包和拆包的,TCP是個“流”協議,所謂流,就是沒有界限的一串數據,TCP底層並不了解上層業務數據的具體含義,它會根據TCP緩沖區的實際數據進行包的划分,一個完整的包可能會被拆分成多個包進行發送,也有可能把多個小的包封裝成一個大的數據包進行發送。這里引用Netty官網的User guide里面的圖進行說明:

 

 

Dealing with a Stream-based Transport

One Small Caveat of Socket Buffer

In a stream-based transport such as TCP/IP, received data is stored into a socket receive buffer. Unfortunately, the buffer of a stream-based transport is not a queue of packets but a queue of bytes. It means, even if you sent two messages as two independent packets, an operating system will not treat them as two messages but as just a bunch of bytes. Therefore, there is no guarantee that what you read is exactly what your remote peer wrote. For example, let us assume that the TCP/IP stack of an operating system has received three packets:

Three packets received as they were sent

Because of this general property of a stream-based protocol, there's high chance of reading them in the following fragmented form in your application:

Three packets split and merged into four buffers

Therefore, a receiving part, regardless it is server-side or client-side, should defrag the received data into one or more meaningful frames that could be easily understood by the application logic. In case of the example above, the received data should be framed like the following:

Four buffers defragged into three

 

 

  那么一般情況下我們是如何解決這種問題的呢?我所知道的有這幾種方案:

    >1.消息定長

    >2.在包尾增加一個標識,通過這個標志符進行分割

    >3.將消息分為兩部分,也就是消息頭和消息尾,消息頭中寫入要發送數據的總長度,通常是在消息頭的第一個字段使用int值來標識發送數據的長度。

 

  這里以第三種方式為例,進行對象傳輸。Netty4本身自帶了ObjectDecoder,ObjectEncoder來實現自定義對象的序列化,但是用的是java內置的序列化,由於java序列化的性能並不是很好,所以很多時候我們需要用其他序列化方式,常見的有Kryo,Jackson,fastjson,protobuf等。這里要寫的其實用什么序列化不是重點,而是我們怎么設計我們的Decoder和Encoder。

 

  首先我們寫一個Encoder,我們繼承自MessageToByteEncoder<T> ,把對象轉換成byte,繼承這個對象,會要求我們實現一個encode方法:

    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
        byte[] body = convertToBytes(msg);  //將對象轉換為byte,偽代碼,具體用什么進行序列化,你們自行選擇。可以使用我上面說的一些
        int dataLength = body.length;  //讀取消息的長度
        out.writeInt(dataLength);  //先將消息長度寫入,也就是消息頭
        out.writeBytes(body);  //消息體中包含我們要發送的數據
    }

  那么當我們在Decode的時候,該怎么處理發送過來的數據呢?這里我們繼承ByteToMessageDecoder方法,繼承這個對象,會要求我們實現一個decode方法

  

public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < HEAD_LENGTH) {  //這個HEAD_LENGTH是我們用於表示頭長度的字節數。  由於上面我們傳的是一個int類型的值,所以這里HEAD_LENGTH的值為4.
            return;
        }
        in.markReaderIndex();                  //我們標記一下當前的readIndex的位置
        int dataLength = in.readInt();       // 讀取傳送過來的消息的長度。ByteBuf 的readInt()方法會讓他的readIndex增加4
        if (dataLength < 0) { // 我們讀到的消息體長度為0,這是不應該出現的情況,這里出現這情況,關閉連接。
            ctx.close();
        }

        if (in.readableBytes() < dataLength) { //讀到的消息體長度如果小於我們傳送過來的消息長度,則resetReaderIndex. 這個配合markReaderIndex使用的。把readIndex重置到mark的地方
            in.resetReaderIndex();
            return;
        }

        byte[] body = new byte[dataLength];  //  嗯,這時候,我們讀到的長度,滿足我們的要求了,把傳送過來的數據,取出來吧~~
        in.readBytes(body);  //
        Object o = convertToObject(body);  //將byte數據轉化為我們需要的對象。偽代碼,用什么序列化,自行選擇
        out.add(o);  
    }

  當然我們Netty也有自帶的LengthFieldBasedFrameDecoder,但是在使用自定義序列化的時候,我覺得還是自己寫比較方便一點,反正總不是要寫代碼。

  我走過的坑:用讀取ByteBuf的使用,一定要注意,其中的方法是否會增加readIndex,不然的話會造成無法正常讀到我們想要的數據。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM