netty系列之:netty中的核心MessageToMessage編碼器

flydean發表於2022-04-18

簡介

在netty中我們需要傳遞各種型別的訊息,這些message可以是字串,可以是陣列,也可以是自定義的物件。不同的物件之間可能需要互相轉換,這樣就需要一個可以自由進行轉換的轉換器,為了統一編碼規則和方便使用者的擴充套件,netty提供了一套訊息之間進行轉換的框架。本文將會講解這個框架的具體實現。

框架簡介

netty為訊息和訊息之間的轉換提供了三個類,這三個類都是抽象類,分別是MessageToMessageDecoder,MessageToMessageEncoder和MessageToMessageCodec。

先來看下他們的定義:

public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerAdapter
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN> extends ChannelDuplexHandler 

MessageToMessageEncoder繼承自ChannelOutboundHandlerAdapter,負責向channel中寫訊息。

MessageToMessageDecoder繼承自ChannelInboundHandlerAdapter,負責從channel中讀取訊息。

MessageToMessageCodec繼承自ChannelDuplexHandler,它是一個雙向的handler,可以從channel中讀取訊息,也可以向channel中寫入訊息。

有了這三個抽象類,我們再看下這三個類的具體實現。

MessageToMessageEncoder

先看一下訊息的編碼器MessageToMessageEncoder,編碼器中最重要的方法就是write,看下write的實現:

    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        CodecOutputList out = null;
        try {
            if (acceptOutboundMessage(msg)) {
                out = CodecOutputList.newInstance();
                @SuppressWarnings("unchecked")
                I cast = (I) msg;
                try {
                    encode(ctx, cast, out);
                } finally {
                    ReferenceCountUtil.release(cast);
                }

                if (out.isEmpty()) {
                    throw new EncoderException(
                            StringUtil.simpleClassName(this) + " must produce at least one message.");
                }
            } else {
                ctx.write(msg, promise);
            }
        } catch (EncoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new EncoderException(t);
        } finally {
            if (out != null) {
                try {
                    final int sizeMinusOne = out.size() - 1;
                    if (sizeMinusOne == 0) {
                        ctx.write(out.getUnsafe(0), promise);
                    } else if (sizeMinusOne > 0) {
                        if (promise == ctx.voidPromise()) {
                            writeVoidPromise(ctx, out);
                        } else {
                            writePromiseCombiner(ctx, out, promise);
                        }
                    }
                } finally {
                    out.recycle();
                }
            }
        }
    }

write方法接受一個需要轉換的原始物件msg,和一個表示channel讀寫進度的ChannelPromise。

首先會對msg進行一個型別判斷,這個判斷方法是在acceptOutboundMessage中實現的。

    public boolean acceptOutboundMessage(Object msg) throws Exception {
        return matcher.match(msg);
    }

這裡的matcher是一個TypeParameterMatcher物件,它是一個在MessageToMessageEncoder建構函式中初始化的屬性:

    protected MessageToMessageEncoder() {
        matcher = TypeParameterMatcher.find(this, MessageToMessageEncoder.class, "I");
    }

這裡的I就是要匹配的msg型別。

如果不匹配,則繼續呼叫ctx.write(msg, promise); 將訊息不做任何轉換的寫入到channel中,供下一個handler呼叫。

如果匹配成功,則會呼叫核心的encode方法:encode(ctx, cast, out);

注意,encode方法在MessageToMessageEncoder中是一個抽象方法,需要使用者在繼承類中自行擴充套件。

encode方法實際上是將msg物件轉換成為要轉換的物件,然後新增到out中。這個out是一個list物件,具體而言是一個CodecOutputList物件,作為一個list,out是一個可以儲存多個物件的列表。

那麼out是什麼時候寫入到channel中去的呢?

別急,在write方法中最後有一個finally程式碼塊,在這個程式碼塊中,會將out寫入到channel裡面。

因為out是一個List,可能會出現out中的物件部分寫成功的情況,所以這裡需要特別處理。

首先判斷out中是否只有一個物件,如果是一個物件,那麼直接寫到channel中即可。如果out中多於一個物件,那麼又分成兩種情況,第一種情況是傳入的promise是一個voidPromise,那麼呼叫writeVoidPromise方法。

什麼是voidPromise呢?

我們知道Promise有多種狀態,可以通過promise的狀態變化了解到資料寫入的情況。對於voidPromise來說,它只關心一種失敗的狀態,其他的狀態都不關心。

如果使用者關心promise的其他狀態,則會呼叫writePromiseCombiner方法,將多個物件的狀態合併為一個promise返回。

事實上,在writeVoidPromise和writePromiseCombiner中,out中的物件都是一個一個的取出來,寫入到channel中的,所以才會生成多個promise和需要將promise進行合併的情況:

    private static void writeVoidPromise(ChannelHandlerContext ctx, CodecOutputList out) {
        final ChannelPromise voidPromise = ctx.voidPromise();
        for (int i = 0; i < out.size(); i++) {
            ctx.write(out.getUnsafe(i), voidPromise);
        }
    }

    private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) {
        final PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
        for (int i = 0; i < out.size(); i++) {
            combiner.add(ctx.write(out.getUnsafe(i)));
        }
        combiner.finish(promise);
    }

MessageToMessageDecoder

和encoder對應的就是decoder了,MessageToMessageDecoder的邏輯和MessageToMessageEncoder差不多。

首先也是需要判斷讀取的訊息型別,這裡也定義了一個TypeParameterMatcher物件,用來檢測傳入的訊息型別:

    protected MessageToMessageDecoder() {
        matcher = TypeParameterMatcher.find(this, MessageToMessageDecoder.class, "I");
    }

decoder中重要的方法是channelRead方法,我們看下它的實現:

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I cast = (I) msg;
                try {
                    decode(ctx, cast, out);
                } finally {
                    ReferenceCountUtil.release(cast);
                }
            } else {
                out.add(msg);
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            try {
                int size = out.size();
                for (int i = 0; i < size; i++) {
                    ctx.fireChannelRead(out.getUnsafe(i));
                }
            } finally {
                out.recycle();
            }
        }
    }

首先檢測msg的型別,只有接受的型別才進行decode處理,否則將msg加入到CodecOutputList中。

最後在finally程式碼塊中將out中的物件一個個取出來,呼叫ctx.fireChannelRead進行讀取。

訊息轉換的關鍵方法是decode,這個方法也是一個抽象方法,需要在繼承類中實現具體的功能。

MessageToMessageCodec

前面講解了一個編碼器和一個解碼器,他們都是單向的。最後要講解的codec叫做MessageToMessageCodec,這個codec是一個雙向的,即可以接收訊息,也可以傳送訊息。

先看下它的定義:

public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN> extends ChannelDuplexHandler

MessageToMessageCodec繼承自ChannelDuplexHandler,接收兩個泛型引數分別是INBOUND_IN和OUTBOUND_IN。

它定義了兩個TypeParameterMatcher,分別用來過濾inboundMsg和outboundMsg:

    protected MessageToMessageCodec() {
        inboundMsgMatcher = TypeParameterMatcher.find(this, MessageToMessageCodec.class, "INBOUND_IN");
        outboundMsgMatcher = TypeParameterMatcher.find(this, MessageToMessageCodec.class, "OUTBOUND_IN");
    }

分別實現了channelRead和write方法,用來讀寫訊息:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        decoder.channelRead(ctx, msg);
    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        encoder.write(ctx, msg, promise);
    }

這裡的decoder和encoder實際上就是前面我們講到的MessageToMessageDecoder和MessageToMessageEncoder:

    private final MessageToMessageEncoder<Object> encoder = new MessageToMessageEncoder<Object>() {

        @Override
        public boolean acceptOutboundMessage(Object msg) throws Exception {
            return MessageToMessageCodec.this.acceptOutboundMessage(msg);
        }

        @Override
        @SuppressWarnings("unchecked")
        protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
            MessageToMessageCodec.this.encode(ctx, (OUTBOUND_IN) msg, out);
        }
    };

    private final MessageToMessageDecoder<Object> decoder = new MessageToMessageDecoder<Object>() {

        @Override
        public boolean acceptInboundMessage(Object msg) throws Exception {
            return MessageToMessageCodec.this.acceptInboundMessage(msg);
        }

        @Override
        @SuppressWarnings("unchecked")
        protected void decode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
            MessageToMessageCodec.this.decode(ctx, (INBOUND_IN) msg, out);
        }
    };

可以看到MessageToMessageCodec實際上就是對MessageToMessageDecoder和MessageToMessageEncoder的封裝,如果需要對MessageToMessageCodec進行擴充套件的話,需要實現下面兩個方法:

    protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List<Object> out)
            throws Exception;

    protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List<Object> out)
            throws Exception;

總結

netty中提供的MessageToMessage的編碼框架是後面對編碼解碼器進行擴充套件的基礎。只有深入瞭解其中的原理,我們對於新的編碼解碼器運用起來才能得心應手。

本文已收錄於 http://www.flydean.com/14-0-1-netty-codec-msg-to-msg/

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

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

相關文章