1. 程式人生 > >Netty學習篇⑤--編、解碼原始碼分析

Netty學習篇⑤--編、解碼原始碼分析

前言

學習Netty也有一段時間了,Netty作為一個高效能的非同步框架,很多RPC框架也運用到了Netty中的知識,在rpc框架中豐富的資料協議及編解碼可以讓使用者更加青睞;
Netty支援豐富的編解碼框架,其本身內部提供的編解碼也可以應對各種業務場景;
今天主要就是學習下Netty中提供的編、解碼類,之前只是簡單的使用了下Netty提供的解碼類,今天更加深入的研究下Netty中編、解碼的原始碼及部分使用。

編、解碼的概念

  • 編碼(Encoder)

    編碼就是將我們傳送的資料編碼成位元組陣列方便在網路中進行傳輸,類似Java中的序列化,將物件序列化成位元組傳輸
  • 解碼(Decoder)

    解碼和編碼相反,將傳輸過來的位元組陣列轉化為各種物件來進行展示等,類似Java中的反序列化
    如:
    // 將位元組陣列轉化為字串
    new String(byte bytes[], Charset charset)

編、解碼超類

ByteToMessageDecoder: 解碼超類,將位元組轉換成訊息

解碼解碼一般用於將獲取到的訊息解碼成系統可識別且自己需要的資料結構;因此ByteToMessageDecoder需要繼承ChannelInboundHandlerAdapter入站介面卡來獲取到入站的資料,在handler使用之前通過channelRead獲取入站資料進行一波解碼;
ByteToMessageDecoder類圖

原始碼分析

通過channelRead獲取入站資料,將資料快取至cumulation資料緩衝區,最後在傳給decode進行解碼,在read完成之後清空快取的資料

1. 獲取入站資料

/**
*  通過重寫channelRead方法來獲取入站資料
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 檢測是否是byteBuf物件格式資料
    if (msg instanceof ByteBuf) {
        // 例項化位元組解碼成功輸出集合 即List<Object> out
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 獲取到的請求的資料
            ByteBuf data = (ByteBuf) msg;
            // 如果緩衝資料區為空則代表是首次觸發read方法
            first = cumulation == null;
            if (first) {
                // 如果是第一次read則當前msg資料為緩衝資料
                cumulation = data;
            } else {
                // 如果不是則觸發累加,將緩衝區的舊資料和新獲取到的資料通過        expandCumulation 方法累加在一起存入緩衝區cumulation
                // cumulator 累加類,將緩衝池中資料和新資料進行組合在一起
                // private Cumulator cumulator = MERGE_CUMULATOR;
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            // 將緩衝區資料cumulation進行解碼
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new DecoderException(t);
        } finally {
            // 在解碼完畢後釋放引用和清空全域性位元組緩衝區
            if (cumulation != null && !cumulation.isReadable()) {
                numReads = 0;
                cumulation.release();
                cumulation = null;
                // discardAfterReads為netty中設定的讀取多少次後開始丟棄位元組 預設值16
                // 可通過setDiscardAfterReads(int n)來設定值不設定預設16次
            } else if (++ numReads >= discardAfterReads) {
                // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                // 在我們讀取了足夠的資料可以嘗試丟棄一些位元組已保證不出現記憶體溢位的異常
                // 
                // See https://github.com/netty/netty/issues/4275
                // 讀取次數重置為0
                numReads = 0;
                // 重置讀寫指標或丟棄部分已讀取的位元組
                discardSomeReadBytes();
            }
            // out為解碼成功的傳遞給下一個handler
            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            // 結束當前read傳遞到下個ChannelHandler
            fireChannelRead(ctx, out, size);
            // 回收響應集合 將insertSinceRecycled設定為false;
            // insertSinceRecycled用於channelReadComplete判斷使用
            out.recycle();
        }
    } else {
        // 不是的話直接fire傳遞給下一個handler
        ctx.fireChannelRead(msg);
    }
}
2. 初始化位元組緩衝區計算器: Cumulator主要用於全域性位元組緩衝區和新讀取的位元組緩衝區組合在一起擴容
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
    
    /**
    * alloc ChannelHandlerContext分配的位元組緩衝區
    * cumulation 當前ByteToMessageDecoder類全域性的位元組緩衝區
    * in 入站的位元組緩衝區
    **/
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        final ByteBuf buffer;
        // 如果全域性ByteBuf寫入的位元組+當前入站的位元組資料大於全域性緩衝區最大的容量或者全域性緩衝區的引用數大於1個或全域性緩衝區只讀
        if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
            || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
            // Expand cumulation (by replace it) when either there is not more room in the buffer
            // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
            // duplicate().retain() or if its read-only.
            //
            // See:
            // - https://github.com/netty/netty/issues/2327
            // - https://github.com/netty/netty/issues/1764
            // 進行擴充套件全域性位元組緩衝區(容量大小 = 新資料追加到舊資料末尾組成新的全域性位元組緩衝區)
            buffer = expandCumulation(alloc, cumulation, in.readableBytes());
        } else {
            buffer = cumulation;
        }
        // 將新資料寫入緩衝區
        buffer.writeBytes(in);
        // 釋放當前的位元組緩衝區的引用
        in.release();
        
        return buffer;
    }
};


/**
* alloc 位元組緩衝區操作類
* cumulation 全域性累加位元組緩衝區
* readable 讀取到的位元組數長度
*/
// 位元組緩衝區擴容方法
static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
    // 舊資料
    ByteBuf oldCumulation = cumulation;
    // 通過ByteBufAllocator將緩衝區擴大到oldCumulation + readable大小
    cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
    // 將舊資料重新寫入到新的位元組緩衝區
    cumulation.writeBytes(oldCumulation);
    // 舊位元組緩衝區引用-1
    oldCumulation.release();
    return cumulation;
}
3. ByteBuf釋放當前位元組緩衝區的引用: 通過呼叫ReferenceCounted介面中的release方法來釋放
@Override
public boolean release() {
    return release0(1);
}

@Override
public boolean release(int decrement) {
    return release0(checkPositive(decrement, "decrement"));
}

/**
* decrement 減量
*/
private boolean release0(int decrement) {
    for (;;) {
        int refCnt = this.refCnt;
        // 當前引用小於減量
        if (refCnt < decrement) {
            throw new IllegalReferenceCountException(refCnt, -decrement);
        }
        // 這裡就利用裡執行緒併發中的知識CAS,執行緒安全的設定refCnt的值
        if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
            // 如果減量和引用量相等
            if (refCnt == decrement) {
                // 全部釋放
                deallocate();
                return true;
            }
            return false;
        }
    }
}

4. 將全域性位元組緩衝區進行解碼

/**
* ctx ChannelHandler的上下文,用於傳輸資料與下一個handler來互動
* in 入站資料
* out 解析之後的出站集合 (此出站不是返回給客戶端的而是傳遞給下個handler的)
*/
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        // 如果入站資料還有沒解析的
        while (in.isReadable()) {
            // 解析成功的出站集合長度
            int outSize = out.size();
            // 如果大於0則說明解析成功的資料還沒被消費完,直接fire掉給通道中的後續handler繼續                消費
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();

                // Check if this handler was removed before continuing with decoding.
                // 在這個handler刪除之前檢查是否還在繼續解碼
                // If it was removed, it is not safe to continue to operate on the buffer.
                // 如果移除了,它繼續操作緩衝區是不安全的
                //
                // See:
                // - https://github.com/netty/netty/issues/4635
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }
            // 入站資料位元組長度
            int oldInputLength = in.readableBytes();
            // 開始解碼資料
            decodeRemovalReentryProtection(ctx, in, out);

            // Check if this handler was removed before continuing the loop.
            // 
            // If it was removed, it is not safe to continue to operate on the buffer.
            //
            // See https://github.com/netty/netty/issues/1664
            if (ctx.isRemoved()) {
                break;
            }

            // 解析完畢跳出迴圈
            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }

            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                    StringUtil.simpleClassName(getClass()) +
                    ".decode() did not read anything but decoded a message.");
            }

            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 設定解碼狀態為正在解碼  STATE_INIT = 0; STATE_CALLING_CHILD_DECODE = 1;             STATE_HANDLER_REMOVED_PENDING = 2; 分別為初始化; 解碼; 解碼完畢移除
        decodeState = STATE_CALLING_CHILD_DECODE;
        try {
            // 具體的解碼邏輯(netty提供的解碼器或自定義解碼器中重寫的decode方法)
            decode(ctx, in, out);
        } finally {
            // 此時decodeState為正在解碼中 值為1,返回false
            boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
            // 在設定為初始化等待解碼
            decodeState = STATE_INIT;
            // 解碼完成移除當前ChannelHandler標記為不處理
            // 可以看看handlerRemoved原始碼。如果緩衝區還有資料直接傳遞給下一個handler
            if (removePending) {
                handlerRemoved(ctx);
            }
        }
    }
5. 執行channelReadComplete
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    // 讀取次數重置
    numReads = 0;
    // 重置讀寫index
    discardSomeReadBytes();
    // 在channelRead meth中定義賦值 decodeWasNull = !out.insertSinceRecycled();
    // out指的是解碼集合List<Object> out; 咱們可以點進
    if (decodeWasNull) {
        decodeWasNull = false;
        if (!ctx.channel().config().isAutoRead()) {
            ctx.read();
        }
    }
    // fire掉readComplete傳遞到下一個handler的readComplete
    ctx.fireChannelReadComplete();
}

/**
*  然後我們可以搜尋下insertSinceRecucled在什麼地方被賦值了
* Returns {@code true} if any elements where added or set. This will be reset once {@link #recycle()} was called.
*/
boolean insertSinceRecycled() {
    return insertSinceRecycled;
}


// 搜尋下insert的呼叫我們可以看到是CodecOutputList類即為channelRead中的out集合,眾所周知在    decode完之後,解碼資料就會被呼叫add方法,此時insertSinceRecycled被設定為true
private void insert(int index, Object element) {
    array[index] = element;
    insertSinceRecycled = true;
}


/**
* 清空回收陣列內部的所有元素和儲存空間
* Recycle the array which will clear it and null out all entries in the internal storage.
*/
// 搜尋recycle的呼叫我麼可以知道在channelRead的finally邏輯中 呼叫了out.recycle();此時        insertSinceRecycled被設定為false
void recycle() {
    for (int i = 0 ; i < size; i ++) {
        array[i] = null;
    }
    clear();
    insertSinceRecycled = false;
    handle.recycle(this);
}

至此ByteToMessageDecoder解碼類應該差不多比較清晰了!!!

MessageToByteEncoder: 編碼超類,將訊息轉成位元組進行編碼發出

何謂編碼,就是將傳送資料轉化為客戶端和服務端約束好的資料結構和格式進行傳輸,我們可以在編碼過程中將訊息體body的長度和一些頭部資訊有序的設定到ByteBuf位元組緩衝區中;方便解碼方靈活的運用來判斷(是否完整的包等)和處理業務;解碼是繼承入站資料,反之編碼應該繼承出站的資料;接下來我們看看編碼類是怎麼進行編碼的;
MessageToByteEncoder類圖如下

原始碼分析

既然是繼承出站類,我們直接看看write方法是怎麼樣的

/**
* 通過write方法獲取到出站的資料即要傳送出去的資料
* ctx channelHandler上下文
* msg 傳送的資料 Object可以通過繼承類指定的泛型來指定
* promise channelPromise非同步監聽,類似ChannelFuture,只不過promise可以設定監聽的結果,future只能通過獲取監聽的成功失敗結果;可以去了解下promise和future的區別
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        // 檢測傳送資料的型別 通過TypeParameterMatcher型別匹配器
        if (acceptOutboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            // 分配位元組緩衝區 preferDirect預設為true
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                // 進行編碼
                encode(ctx, cast, buf);
            } finally {
                // 完成編碼後釋放物件的引用
                ReferenceCountUtil.release(cast);
            }
            // 如果緩衝區有資料則通過ctx傳送出去,promise可以監聽資料傳輸並設定是否完成
            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                // 如果沒有資料則釋放位元組緩衝區的引用併發送一個empty的空包
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            // 非TypeParameterMatcher型別匹配器匹配的型別直接傳送出去
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
}

// 初始化設定preferDirect為true
protected MessageToByteEncoder() {
    this(true);
}
protected MessageToByteEncoder(boolean preferDirect) {
    matcher = TypeParameterMatcher.find(this, MessageToByteEncoder.class, "I");
    this.preferDirect = preferDirect;
}

編碼: 重寫encode方法,根據實際業務來進行資料編碼

// 此處就是我們需要重寫的編碼方法了,我們和根據約束好的或者自己定義好想要的資料格式傳送給對方

// 下面是我自己寫的demo的編碼方法;頭部設定好body的長度,服務端可以根據長度來判斷是否是完整的包,僅僅自學寫的簡單的demo非正常線上運營專案的邏輯
public class MyClientEncode extends MessageToByteEncoder<String> {

    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
        if (null != msg) {
            byte[] request = msg.getBytes(Charset.forName("UTF-8"));
            out.writeInt(request.length);
            out.writeBytes(request);
        }
    }
}

編碼類相對要簡單很多,因為只需要將傳送的資料序列化,按照一定的格式進行傳送資料!!!

專案實戰

專案主要簡單的實現下自定義編解碼器的運用及LengthFieldBasedFrameDecoder的使用

  • 專案結構如下
    │  hetangyuese-netty-06.iml
    │  pom.xml
    │
    ├─src
    │  ├─main
    │  │  ├─java
    │  │  │  └─com
    │  │  │      └─hetangyuese
    │  │  │          └─netty
    │  │  │              ├─client
    │  │  │              │      MyClient06.java
    │  │  │              │      MyClientChannelInitializer.java
    │  │  │              │      MyClientDecoder.java
    │  │  │              │      MyClientEncode.java
    │  │  │              │      MyClientHandler.java
    │  │  │              │      MyMessage.java
    │  │  │              │
    │  │  │              └─server
    │  │  │                      MyChannelInitializer.java
    │  │  │                      MyServer06.java
    │  │  │                      MyServerDecoder.java
    │  │  │                      MyServerDecoderLength.java
    │  │  │                      MyServerEncoder.java
    │  │  │                      MyServerHandler.java
    │  │  │
    │  │  └─resources
    │  └─test
    │      └─java
    
  • 服務端

    Serverhandler: 只是簡單的將解碼的內容輸出

    public class MyServerHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客戶端連線成功 time: " + new Date().toLocaleString());
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客戶端斷開連線 time: " + new Date().toLocaleString());
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("content:" + body);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 出現異常關閉通道
            cause.printStackTrace();
            ctx.close();
        }
    }

    解碼器

    public class MyServerDecoder extends ByteToMessageDecoder {
    
        // 此處我頭部只塞了長度欄位佔4個位元組,別問為啥我知道,這是要客戶端和服務端約束好的
        private static int min_head_length = 4;
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
            // 解碼的位元組長度
            int size = in.readableBytes();
            if(size < min_head_length) {
                System.out.println("解析的資料長度小於頭部長度欄位的長度");
                return ;
            }
            // 讀取的時候指標已經移位到長度欄位的尾端
            int length = in.readInt();
            if (size < length) {
                System.out.println("解析的資料長度與長度不符合");
                return ;
            }
    
            // 上面已經讀取到了長度欄位,後面的長度就是body
            ByteBuf decoderArr = in.readBytes(length);
            byte[] request = new byte[decoderArr.readableBytes()];
            // 將資料寫入空陣列
            decoderArr.readBytes(request);
            String body = new String(request, Charset.forName("UTF-8"));
            out.add(body);
        }
    }

    將解碼器加入到channelHandler中:記得加到業務handler的前面否則無效

    public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
    //                .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0))
    //                .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0))
                    .addLast(new MyServerDecoder())
                    .addLast(new MyServerHandler())
            ;
        }
    }
  • 客戶端

    ClientHandler

    public class MyClientHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("與服務端連線成功");
            for (int i = 0; i<10; i++) {
                ctx.writeAndFlush("hhhhh" + i);
            }
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("與服務端斷開連線");
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("收到服務端訊息:" +msg+ " time: " + new Date().toLocaleString());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }

    編碼器

    public class MyClientEncode extends MessageToByteEncoder<String> {
    
        @Override
        protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
            if (null != msg) {
                byte[] request = msg.getBytes(Charset.forName("UTF-8"));
                out.writeInt(request.length);
                out.writeBytes(request);
            }
        }
    }

    將編碼器加到ClientHandler的前面

    public class MyClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
                    .addLast(new MyClientDecoder())
                    .addLast(new MyClientEncode())
                    .addLast(new MyClientHandler())
            ;
    
        }
    }
  • 服務端執行結果
    MyServer06 is start ...................
    客戶端連線成功 time: 2019-11-19 16:35:47
    content:hhhhh0
    content:hhhhh1
    content:hhhhh2
    content:hhhhh3
    content:hhhhh4
    content:hhhhh5
    content:hhhhh6
    content:hhhhh7
    content:hhhhh8
    content:hhhhh9
  • 如果不用自定義的解碼器怎麼獲取到body內容呢

    將自定義編碼器換成LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)

    public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
    //                .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0))
                    .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0))
    //                .addLast(new MyServerDecoder())
                    .addLast(new MyServerHandler())
            ;
        }
    }
    
    // 怕忘記的各個引數的含義在這在說明一次,自己不斷的修改每個值觀察結果就可以更加深刻的理解
    /**
    * maxFrameLength:訊息體的最大長度,好像預設最大值為1024*1024
    * lengthFieldOffset 長度欄位所在位元組陣列的下標 (我這是第一個write的所以下標是0)
    * lengthFieldLength 長度欄位的位元組長度(int型別佔4個位元組)
    * lengthAdjustment 長度欄位補償的數值 (lengthAdjustment =  資料包長度 - lengthFieldOffset - lengthFieldLength - 長度域的值),解析需要減去對應的數值
    * initialBytesToStrip 是否去掉長度欄位(0不去除,對應長度域位元組長度)
    */
    public LengthFieldBasedFrameDecoder(
                int maxFrameLength,
                int lengthFieldOffset, int lengthFieldLength,
                int lengthAdjustment, int initialBytesToStrip)
    結果: 前都帶上了長度
    MyServer06 is start ...................
    客戶端連線成功 time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh0, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh1, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh2, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh3, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh4, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh5, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh6, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh7, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh8, time: 2019-11-19 17:53:42
    收到客戶端發來的訊息:   hhhhh9, time: 2019-11-19 17:53:42

    如果我們在客戶端的長度域中做手腳 LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)

    舊: out.writeInt(request.length);
    新: out.writeInt(request.length + 1);
    // 看結果就不正常,0後面多了一個0;但是不知道為啥只解碼了一次??? 求解答
    MyServer06 is start ...................
    客戶端連線成功 time: 2019-11-19 17:56:55
    收到客戶端發來的訊息:   hhhhh0 , time: 2019-11-19 17:56:55
    
    // 正確修改為 LengthFieldBasedFrameDecoder(10240, 0, 4, -1, 0)
    // 結果:
    MyServer06 is start ...................
    客戶端連線成功 time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh0, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh1, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh2, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh3, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh4, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh5, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh6, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh7, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh8, time: 2019-11-19 18:02:18
    收到客戶端發來的訊息:   hhhhh9, time: 2019-11-19 18:02:18

    捨棄長度域 :LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 4)

    // 結果
    MyServer06 is start ...................
    客戶端連線成功 time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh0, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh1, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh2, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh3, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh4, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh5, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh6, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh7, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh8, time: 2019-11-19 18:03:44
    收到客戶端發來的訊息:hhhhh9, time: 2019-11-19 18:03:44
    分析原始碼示例中的 lengthAdjustment = 訊息位元組長度 - lengthFieldOffset-lengthFieldLength-長度域中的值
  • 原始碼中的示例
     * <pre>
     * lengthFieldOffset   =  0
     * lengthFieldLength   =  2
     * <b>lengthAdjustment</b>    = <b>-2</b> (= the length of the Length field)
     * initialBytesToStrip =  0
     *
     * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
     * +--------+----------------+      +--------+----------------+
     * | Length | Actual Content |----->| Length | Actual Content |
     * | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
     * +--------+----------------+      +--------+----------------+
     * </pre>
    長度域中0x000E為16進位制,轉換成10進位制是14,說明訊息體長度為14;根據公式:14-0-2-14 = -2
    * <pre>
     * lengthFieldOffset   = 0
     * lengthFieldLength   = 3
     * <b>lengthAdjustment</b>    = <b>2</b> (= the length of Header 1)
     * initialBytesToStrip = 0
     *
     * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
     * +----------+----------+----------------+      +----------+----------+----------------+
     * |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
     * | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
     * +----------+----------+----------------+      +----------+----------+----------------+
     * </pre>
    從上的例子可以知道;lengthAdjustment(2) = 17- 12(00000C)-lengthFieldOffset(0) - lengthFieldLength(3);

    .......等等