netty原始碼分析之pipeline(二)

閃電俠發表於2019-03-03

前言

netty原始碼分析之pipeline(一)中,我們已經瞭解了pipeline在netty中所處的角色,像是一條流水線,控制著位元組流的讀寫,本文,我們在這個基礎上繼續深挖pipeline在事件傳播,異常傳播等方面的細節

主要內容

接下來,本文分以下幾個部分進行

  1. netty中的Unsafe到底是幹什麼的
  2. pipeline中的head
  3. pipeline中的inBound事件傳播
  4. pipeline中的tail
  5. pipeline中的outBound事件傳播
  6. pipeline 中異常的傳播

##Unsafe到底是幹什麼的
之所以Unsafe放到pipeline中講,是因為unsafe和pipeline密切相關,pipeline中的有關io的操作最終都是落地到unsafe,所以,有必要先講講unsafe

初識Unsafe

顧名思義,unsafe是不安全的意思,就是告訴你不要在應用程式裡面直接使用Unsafe以及他的衍生類物件。

netty官方的解釋如下

Unsafe operations that should never be called from user-code. These methods are only provided to implement the actual transport, and must be invoked from an I/O thread

Unsafe 在Channel定義,屬於Channel的內部類,表明Unsafe和Channel密切相關

下面是unsafe介面的所有方法

interface Unsafe {
   RecvByteBufAllocator.Handle recvBufAllocHandle();
   
   SocketAddress localAddress();
   SocketAddress remoteAddress();

   void register(EventLoop eventLoop, ChannelPromise promise);
   void bind(SocketAddress localAddress, ChannelPromise promise);
   void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
   void disconnect(ChannelPromise promise);
   void close(ChannelPromise promise);
   void closeForcibly();
   void beginRead();
   void write(Object msg, ChannelPromise promise);
   void flush();
   
   ChannelPromise voidPromise();
   ChannelOutboundBuffer outboundBuffer();
}
複製程式碼

按功能可以分為分配記憶體,Socket四元組資訊,註冊事件迴圈,繫結網路卡埠,Socket的連線和關閉,Socket的讀寫,看的出來,這些操作都是和jdk底層相關

Unsafe 繼承結構

Unsafe 繼承結構

NioUnsafeUnsafe基礎上增加了以下幾個介面

public interface NioUnsafe extends Unsafe {
    SelectableChannel ch();
    void finishConnect();
    void read();
    void forceFlush();
}
複製程式碼

從增加的介面以及類名上來看,NioUnsafe 增加了可以訪問底層jdk的SelectableChannel的功能,定義了從SelectableChannel讀取資料的read方法

AbstractUnsafe 實現了大部分Unsafe的功能

AbstractNioUnsafe 主要是通過代理到其外部類AbstractNioChannel拿到了與jdk nio相關的一些資訊,比如SelectableChannelSelectionKey等等

NioSocketChannelUnsafeNioByteUnsafe放到一起講,其實現了IO的基本操作,讀,和寫,這些操作都與jdk底層相關

NioMessageUnsafeNioByteUnsafe 是處在同一層次的抽象,netty將一個新連線的建立也當作一個io操作來處理,這裡的Message的含義我們可以當作是一個SelectableChannel,讀的意思就是accept一個SelectableChannel,寫的意思是針對一些無連線的協議,比如UDP來操作的,我們先不用關注

Unsafe的分類

從以上繼承結構來看,我們可以總結出兩種型別的Unsafe分類,一個是與連線的位元組資料讀寫相關的NioByteUnsafe,一個是與新連線建立操作相關的NioMessageUnsafe

NioByteUnsafe中的讀:委託到外部類NioSocketChannel

protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
複製程式碼

最後一行已經與jdk底層以及netty中的ByteBuf相關,將jdk的 SelectableChannel的位元組資料讀取到netty的ByteBuf

NioMessageUnsafe中的讀:委託到外部類NioSocketChannel

protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = javaChannel().accept();

    if (ch != null) {
        buf.add(new NioSocketChannel(this, ch));
        return 1;
    }
    return 0;
}
複製程式碼

NioMessageUnsafe 的讀操作很簡單,就是呼叫jdk的accept()方法,新建立一條連線

NioByteUnsafe中的寫:委託到外部類NioSocketChannel

@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
    final int expectedWrittenBytes = buf.readableBytes();
    return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
複製程式碼

最後一行已經與jdk底層以及netty中的ByteBuf相關,將netty的ByteBuf中的位元組資料寫到jdk的 SelectableChannel

NioMessageUnsafe 的寫,在tcp協議層面我們基本不會涉及,暫時忽略,udp協議的讀者可以自己去研究一番~

關於Unsafe我們就先了解這麼多

##pipeline中的head
netty原始碼分析之pipeline(一)中,我們瞭解到head節點在pipeline中第一個處理IO事件,新連線接入和讀事件在reactor執行緒的第二個步驟被檢測到

NioEventLoop

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
     final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
     //新連線的已準備接入或者已存在的連線有資料可讀
     if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
         unsafe.read();
     }
}
複製程式碼

讀操作直接依賴到unsafe來進行,新連線的接入在netty原始碼分析之新連線接入全解析中已詳細闡述,這裡不再描述,下面將重點放到連線位元組資料流的讀寫

NioByteUnsafe

@Override
public final void read() {
    final ChannelConfig config = config();
    final ChannelPipeline pipeline = pipeline();
    // 建立ByteBuf分配器
    final ByteBufAllocator allocator = config.getAllocator();
    final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
    allocHandle.reset(config);

    ByteBuf byteBuf = null;
    do {
        // 分配一個ByteBuf
        byteBuf = allocHandle.allocate(allocator);
        // 將資料讀取到分配的ByteBuf中去
        allocHandle.lastBytesRead(doReadBytes(byteBuf));
        if (allocHandle.lastBytesRead() <= 0) {
            byteBuf.release();
            byteBuf = null;
            close = allocHandle.lastBytesRead() < 0;
            break;
        }

        // 觸發事件,將會引發pipeline的讀事件傳播
        pipeline.fireChannelRead(byteBuf);
        byteBuf = null;
    } while (allocHandle.continueReading());
    pipeline.fireChannelReadComplete();
}
複製程式碼

同樣,我抽出了核心程式碼,細枝末節先剪去,NioByteUnsafe 要做的事情可以簡單地分為以下幾個步驟

  1. 拿到Channel的config之後拿到ByteBuf分配器,用分配器來分配一個ByteBuf,ByteBuf是netty裡面的位元組資料載體,後面讀取的資料都讀到這個物件裡面
  2. 將Channel中的資料讀取到ByteBuf
  3. 資料讀完之後,呼叫 pipeline.fireChannelRead(byteBuf); 從head節點開始傳播至整個pipeline

這裡,我們的重點其實就是 pipeline.fireChannelRead(byteBuf);

DefaultChannelPipeline

final AbstractChannelHandlerContext head;
//...
head = new HeadContext(this);

public final ChannelPipeline fireChannelRead(Object msg) {
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}
複製程式碼

結合這幅圖

pipeline

可以看到,資料從head節點開始流入,在進行下一步之前,我們先把head節點的功能過一遍

HeadContext

final class HeadContext extends AbstractChannelHandlerContext
        implements ChannelOutboundHandler, ChannelInboundHandler {

    private final Unsafe unsafe;

    HeadContext(DefaultChannelPipeline pipeline) {
        super(pipeline, null, HEAD_NAME, false, true);
        unsafe = pipeline.channel().unsafe();
        setAddComplete();
    }

    @Override
    public ChannelHandler handler() {
        return this;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // NOOP
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // NOOP
    }

    @Override
    public void bind(
            ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
            throws Exception {
        unsafe.bind(localAddress, promise);
    }

    @Override
    public void connect(
            ChannelHandlerContext ctx,
            SocketAddress remoteAddress, SocketAddress localAddress,
            ChannelPromise promise) throws Exception {
        unsafe.connect(remoteAddress, localAddress, promise);
    }

    @Override
    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        unsafe.disconnect(promise);
    }

    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        unsafe.close(promise);
    }

    @Override
    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        unsafe.deregister(promise);
    }

    @Override
    public void read(ChannelHandlerContext ctx) {
        unsafe.beginRead();
    }

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

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

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

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        invokeHandlerAddedIfNeeded();
        ctx.fireChannelRegistered();
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelUnregistered();

        // Remove all handlers sequentially if channel is closed and unregistered.
        if (!channel.isOpen()) {
            destroy();
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelActive();

        readIfIsAutoRead();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelInactive();
    }

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

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

        readIfIsAutoRead();
    }

    private void readIfIsAutoRead() {
        if (channel.config().isAutoRead()) {
            channel.read();
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelWritabilityChanged();
    }
}
複製程式碼

從head節點繼承的兩個介面看,TA既是一個ChannelHandlerContext,同時又屬於inBound(最新程式碼已經加上這一介面)和outBound Handler

在傳播讀寫事件的時候,head的功能只是簡單地將事件傳播下去,如ctx.fireChannelRead(msg);

在真正執行讀寫操作的時候,例如在呼叫writeAndFlush()等方法的時候,最終都會委託到unsafe執行,而當一次資料讀完,channelReadComplete方法首先被呼叫,TA要做的事情除了將事件繼續傳播下去之外,還得繼續向reactor執行緒註冊讀事件,即呼叫readIfIsAutoRead(), 我們簡單跟一下

HeadContext

private void readIfIsAutoRead() {
    if (channel.config().isAutoRead()) {
        channel.read();
    }
}
複製程式碼

AbstractChannel

@Override
public Channel read() {
    pipeline.read();
    return this;
}
複製程式碼

預設情況下,Channel都是預設開啟自動讀取模式的,即只要Channel是active的,讀完一波資料之後就繼續向selector註冊讀事件,這樣就可以連續不斷得讀取資料,最終,通過pipeline,還是傳遞到head節點

HeadContext

@Override
public void read(ChannelHandlerContext ctx) {
    unsafe.beginRead();
}
複製程式碼

委託到了 NioByteUnsafe

NioByteUnsafe

@Override
public final void beginRead() {
    doBeginRead();
} 
複製程式碼

AbstractNioChannel

@Override
protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}
複製程式碼

doBeginRead() 做的事情很簡單,拿到處理過的selectionKey,然後如果發現該selectionKey若在某個地方被移除了readInterestOp操作,這裡給他加上,事實上,標準的netty程式是不會走到這一行的,只有在三次握手成功之後,如下方法被呼叫

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ctx.fireChannelActive();

    readIfIsAutoRead();
}
複製程式碼

才會將readInterestOp註冊到SelectionKey上,可結合 netty原始碼分析之新連線接入全解析 來看

總結一點,head節點的作用就是作為pipeline的頭節點開始傳遞讀寫事件,呼叫unsafe進行實際的讀寫操作,下面,進入pipeline中非常重要的一環,inbound事件的傳播

##pipeline中的inBound事件傳播

netty原始碼分析之新連線接入全解析 一文中,我們沒有詳細描述為啥pipeline.fireChannelActive();最終會呼叫到AbstractNioChannel.doBeginRead(),瞭解pipeline中的事件傳播機制,你會發現相當簡單

DefaultChannelPipeline

public final ChannelPipeline fireChannelActive() {
    AbstractChannelHandlerContext.invokeChannelActive(head);
    return this;
}
複製程式碼

三次握手成功之後,pipeline.fireChannelActive();被呼叫,然後以head節點為引數,直接一個靜態呼叫

AbstractChannelHandlerContext

static void invokeChannelActive(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelActive();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelActive();
            }
        });
    }
}
複製程式碼

首先,netty為了確保執行緒的安全性,將確保該操作在reactor執行緒中被執行,這裡直接呼叫 HeadContext.fireChannelActive()方法

HeadContext

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ctx.fireChannelActive();

    readIfIsAutoRead();
}
複製程式碼

我們先看 ctx.fireChannelActive();,跟進去之前我們先看下當前pipeline的情況
[2.png]

AbstractChannelHandlerContext

public ChannelHandlerContext fireChannelActive() {
    final AbstractChannelHandlerContext next = findContextInbound();
    invokeChannelActive(next);
    return this;
}
複製程式碼

首先,呼叫 findContextInbound() 找到下一個inbound節點,由於當前pipeline的雙向連結串列結構中既有inbound節點,又有outbound節點,讓我們看看netty是怎麼找到下一個inBound節點的

AbstractChannelHandlerContext

private AbstractChannelHandlerContext findContextInbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.next;
    } while (!ctx.inbound);
    return ctx;
}
複製程式碼

這段程式碼很清楚地表明,netty尋找下一個inBound節點的過程是一個線性搜尋的過程,他會遍歷雙向連結串列的下一個節點,直到下一個節點為inBound(關於inBound和outBound,netty原始碼分析之pipeline(一)已有說明,這裡不再詳細分析)

找到下一個節點之後,執行 invokeChannelActive(next);,一個遞迴呼叫,直到最後一個inBound節點——tail節點

TailContext

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { }
複製程式碼

Tail節點的該方法為空,結束呼叫,同理,可以分析所有的inBound事件的傳播,正常情況下,即使用者如果不覆蓋每個節點的事件傳播操作,幾乎所有的事件最後都落到tail節點,所以,我們有必要研究一下tail節點所具有的功能

##pipeline中的tail

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {

    TailContext(DefaultChannelPipeline pipeline) {
        super(pipeline, null, TAIL_NAME, true, false);
        setAddComplete();
    }

    @Override
    public ChannelHandler handler() {
        return this;
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // This may not be a configuration error and so don`t log anything.
        // The event may be superfluous for the current pipeline configuration.
        ReferenceCountUtil.release(evt);
    }

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

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

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { }
}
複製程式碼

正如我們前面所提到的,tail節點的大部分作用即終止事件的傳播(方法體為空),除此之外,有兩個重要的方法我們必須提一下,exceptionCaught()channelRead()

exceptionCaught

protected void onUnhandledInboundException(Throwable cause) {
    try {
        logger.warn(
                "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
                        "It usually means the last handler in the pipeline did not handle the exception.",
                cause);
    } finally {
        ReferenceCountUtil.release(cause);
    }
}
複製程式碼

異常傳播的機制和inBound事件傳播的機制一樣,最終如果使用者自定義節點沒有處理的話,會落到tail節點,tail節點可不會簡單地吞下這個異常,而是向你發出警告,相信使用netty的同學對這段警告不陌生吧?

channelRead

protected void onUnhandledInboundMessage(Object msg) {
    try {
        logger.debug(
                "Discarded inbound message {} that reached at the tail of the pipeline. " +
                        "Please check your pipeline configuration.", msg);
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
複製程式碼

另外,tail節點在發現位元組資料(ByteBuf)或者decoder之後的業務物件在pipeline流轉過程中沒有被消費,落到tail節點,tail節點就會給你發出一個警告,告訴你,我已經將你未處理的資料給丟掉了

總結一下,tail節點的作用就是結束事件傳播,並且對一些重要的事件做一些善意提醒

pipeline中的outBound事件傳播

上一節中,我們在闡述tail節點的功能時,忽略了其父類AbstractChannelHandlerContext所具有的功能,這一節中,我們以最常見的writeAndFlush操作來看下pipeline中的outBound事件是如何向外傳播的

典型的訊息推送系統中,會有類似下面的一段程式碼

Channel channel = getChannel(userInfo);
channel.writeAndFlush(pushInfo);
複製程式碼

這段程式碼的含義就是根據使用者資訊拿到對應的Channel,然後給使用者推送訊息,跟進 channel.writeAndFlush

NioSocketChannel

public ChannelFuture writeAndFlush(Object msg) {
    return pipeline.writeAndFlush(msg);
}
複製程式碼

從pipeline開始往外傳播

public final ChannelFuture writeAndFlush(Object msg) {
    return tail.writeAndFlush(msg);
}
複製程式碼

Channel 中大部分outBound事件都是從tail開始往外傳播, writeAndFlush()方法是tail繼承而來的方法,我們跟進去

AbstractChannelHandlerContext

public ChannelFuture writeAndFlush(Object msg) {
    return writeAndFlush(msg, newPromise());
}

public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
    write(msg, true, promise);

    return promise;
}
複製程式碼

這裡提前說一點,netty中很多io操作都是非同步操作,返回一個ChannelFuture給呼叫方,呼叫方拿到這個future可以在適當的時機拿到操作的結果,或者註冊回撥,後面的原始碼系列會深挖,這裡就帶過了,我們繼續

AbstractChannelHandlerContext

private void write(Object msg, boolean flush, ChannelPromise promise) {
    AbstractChannelHandlerContext next = findContextOutbound();
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        AbstractWriteTask task;
        if (flush) {
            task = WriteAndFlushTask.newInstance(next, m, promise);
        }  else {
            task = WriteTask.newInstance(next, m, promise);
        }
        safeExecute(executor, task, promise, m);
    }
}
複製程式碼

netty為了保證程式的高效執行,所有的核心的操作都在reactor執行緒中處理,如果業務執行緒呼叫Channel的讀寫方法,netty會將該操作封裝成一個task,隨後在reactor執行緒中執行,參考 netty原始碼分析之揭開reactor執行緒的面紗(三),非同步task的執行

這裡我們為了不跑偏,假設是在reactor執行緒中(上面的這段例子其實是在業務執行緒中),先呼叫findContextOutbound()方法找到下一個outBound()節點

AbstractChannelHandlerContext

private AbstractChannelHandlerContext findContextOutbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.prev;
    } while (!ctx.outbound);
    return ctx;
}
複製程式碼

找outBound節點的過程和找inBound節點類似,反方向遍歷pipeline中的雙向連結串列,直到第一個outBound節點next,然後呼叫next.invokeWriteAndFlush(m, promise)

AbstractChannelHandlerContext

private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
        invokeFlush0();
    } else {
        writeAndFlush(msg, promise);
    }
}
複製程式碼

呼叫該節點的ChannelHandler的write方法,flush方法我們暫且忽略,後面會專門講writeAndFlush的完整流程

AbstractChannelHandlerContext

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}
複製程式碼

我們在使用outBound型別的ChannelHandler中,一般會繼承 ChannelOutboundHandlerAdapter,所以,我們需要看看他的 write方法是怎麼處理outBound事件傳播的

ChannelOutboundHandlerAdapter

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg, promise);
}
複製程式碼

很簡單,他除了遞迴呼叫 ctx.write(msg, promise);之外,啥事也沒幹,在netty原始碼分析之pipeline(一)我們已經知道,pipeline的雙向連結串列結構中,最後一個outBound節點是head節點,因此資料最終會落地到他的write方法

HeadContext

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    unsafe.write(msg, promise);
}
複製程式碼

這裡,加深了我們對head節點的理解,即所有的資料寫出都會經過head節點,我們在下一節會深挖,這裡暫且到此為止

實際情況下,outBound類的節點中會有一種特殊型別的節點叫encoder,它的作用是根據自定義編碼規則將業務物件轉換成ByteBuf,而這類encoder 一般繼承自 MessageToByteEncoder

下面是一段

public abstract class DataPacketEncoder extends MessageToByteEncoder<DatePacket> {

    @Override
    protected void encode(ChannelHandlerContext ctx, DatePacket msg, ByteBuf out) throws Exception {
        // 這裡拿到業務物件msg的資料,然後呼叫 out.writeXXX()系列方法編碼
    }
}
複製程式碼

為什麼業務程式碼只需要覆蓋這裡的encod方法,就可以將業務物件轉換成位元組流寫出去呢?通過前面的呼叫鏈條,我們需要檢視一下其父類MessageToByteEncoder的write方法是怎麼處理業務物件的

MessageToByteEncoder

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        // 需要判斷當前編碼器能否處理這類物件
        if (acceptOutboundMessage(msg)) {
            I cast = (I) msg;
            // 分配記憶體
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                encode(ctx, cast, buf);
            } finally {
                ReferenceCountUtil.release(cast);
            }
            // buf到這裡已經裝載著資料,於是把該buf往前丟,知道head節點
            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            // 如果不能處理,就將outBound事件繼續往前面傳播
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
複製程式碼

先呼叫 acceptOutboundMessage 方法判斷,該encoder是否可以處理msg對應的類的物件(暫不展開),通過之後,就強制轉換,這裡的泛型I對應的是DataPacket,轉換之後,先開闢一段記憶體,呼叫encode(),即回到DataPacketEncoder中,將buf裝滿資料,最後,如果buf中被寫了資料(buf.isReadable()),就將該buf往前丟,一直傳遞到head節點,被head節點的unsafe消費掉

當然,如果當前encoder不能處理當前業務物件,就簡單地將該業務物件向前傳播,直到head節點,最後,都處理完之後,釋放buf,避免堆外記憶體洩漏

##pipeline 中異常的傳播

我們通常在業務程式碼中,會加入一個異常處理器,統一處理pipeline過程中的所有的異常,並且,一般該異常處理器需要載入自定義節點的最末尾,即

pipeline中異常的傳播

此類ExceptionHandler一般繼承自 ChannelDuplexHandler,標識該節點既是一個inBound節點又是一個outBound節點,我們分別分析一下inBound事件和outBound事件過程中,ExceptionHandler是如何才處理這些異常的

inBound異常的處理

我們以資料的讀取為例,看下netty是如何傳播在這個過程中發生的異常

我們前面已經知道,對於每一個節點的資料讀取都會呼叫AbstractChannelHandlerContext.invokeChannelRead()方法

AbstractChannelHandlerContext

private void invokeChannelRead(Object msg) {
    try {
        ((ChannelInboundHandler) handler()).channelRead(this, msg);
    } catch (Throwable t) {
        notifyHandlerException(t);
    }
}
複製程式碼

可以看到該節點最終委託到其內部的ChannelHandler處理channelRead,而在最外層catch整個Throwable,因此,我們在如下使用者程式碼中的異常會被捕獲

public class BusinessHandler extends ChannelInboundHandlerAdapter {
    @Override
    protected void channelRead(ChannelHandlerContext ctx, Object data) throws Exception {
       //...
          throw new BusinessException(...); 
       //...
    }

}
複製程式碼

上面這段業務程式碼中的 BusinessException 會被 BusinessHandler所在的節點捕獲,進入到 notifyHandlerException(t);往下傳播,我們看下它是如何傳播的

AbstractChannelHandlerContext

private void notifyHandlerException(Throwable cause) {
    // 略去了非關鍵程式碼,讀者可自行分析
    invokeExceptionCaught(cause);
}
複製程式碼
private void invokeExceptionCaught(final Throwable cause) {
    handler().exceptionCaught(this, cause);
}
複製程式碼

可以看到,此Hander中異常優先由此Handelr中的exceptionCaught方法來處理,預設情況下,如果不覆寫此Handler中的exceptionCaught方法,呼叫

ChannelInboundHandlerAdapter

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
        throws Exception {
    ctx.fireExceptionCaught(cause);
}
複製程式碼

AbstractChannelHandlerContext

public ChannelHandlerContext fireExceptionCaught(final Throwable cause) {
    invokeExceptionCaught(next, cause);
    return this;
}
複製程式碼

到了這裡,已經很清楚了,如果我們在自定義Handler中沒有處理異常,那麼預設情況下該異常將一直傳遞下去,遍歷每一個節點,直到最後一個自定義異常處理器ExceptionHandler來終結,收編異常

Exceptionhandler

public Exceptionhandler extends ChannelDuplexHandler {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        // 處理該異常,並終止異常的傳播
    }
}
複製程式碼

到了這裡,你應該知道為什麼異常處理器要加在pipeline的最後了吧?

outBound異常的處理

然而對於outBound事件傳播過程中所發生的異常,該Exceptionhandler照樣能完美處理,為什麼?

我們以前面提到的writeAndFlush方法為例,來看看outBound事件傳播過程中的異常最後是如何落到Exceptionhandler中去的

前面我們知道,channel.writeAndFlush()方法最終也會呼叫到節點的 invokeFlush0()方法(write機制比較複雜,我們留到後面的文章中將)

AbstractChannelHandlerContext

private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
        invokeFlush0();
    } else {
        writeAndFlush(msg, promise);
    }
}


private void invokeFlush0() {
    try {
        ((ChannelOutboundHandler) handler()).flush(this);
    } catch (Throwable t) {
        notifyHandlerException(t);
    }
}
複製程式碼

invokeFlush0()會委託其內部的ChannelHandler的flush方法,我們一般實現的即是ChannelHandler的flush方法

private void invokeFlush0() {
    try {
        ((ChannelOutboundHandler) handler()).flush(this);
    } catch (Throwable t) {
        notifyHandlerException(t);
    }
}
複製程式碼

好,假設在當前節點在flush的過程中發生了異常,都會被 notifyHandlerException(t);捕獲,該方法會和inBound事件傳播過程中的異常傳播方法一樣,也是輪流找下一個異常處理器,而如果異常處理器在pipeline最後面的話,一定會被執行到,這就是為什麼該異常處理器也能處理outBound異常的原因

關於為啥 ExceptionHandler 既能處理inBound,又能處理outBound型別的異常的原因,總結一點就是,在任何節點中發生的異常都會往下一個節點傳遞,最後終究會傳遞到異常處理器

總結

最後,老樣子,我們做下總結

  1. 一個Channel對應一個Unsafe,Unsafe處理底層操作,NioServerSocketChannel對應NioMessageUnsafe, NioSocketChannel對應NioByteUnsafe
  2. inBound事件從head節點傳播到tail節點,outBound事件從tail節點傳播到head節點
  3. 異常傳播只會往後傳播,而且不分inbound還是outbound節點,不像outBound事件一樣會往前傳播

如果你想系統地學Netty,我的小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》可以幫助你

image.png

如果你想系統學習Netty原理,那麼你一定不要錯過我的Netty原始碼分析系列視訊:Java 讀原始碼之 Netty 深入剖析

image.png
image.png
image.png
image.png

相關文章