詳盡Netty(三):Channel

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

概念

Channel 是java nio的一個基本構造。

​ 它代表一個到實體(如一個硬件設備,一個文件、一個網絡套接字或者一個能夠之行一個或者多個不同的I/O操作的程序組件)的開放鏈接,如讀操作和寫操作。

可以把Channel 看做是傳送(入站)或者傳出(出站)數據的載體。可以被打開或者被關閉,鏈接或者斷開連接。

其UML圖:
在這裏插入圖片描述

分類

Channel:是對網絡Socket的封裝,抽象了網絡I/O的讀、寫、連接與綁定。

AbstractChannel:實現了Channel接口的大部分功能,一次連接用到的Channel、ChannelId、eventLoop、pipelIne、unsafe都會保存在這裏。

AbstractNioChannel:通過select的方式對讀寫事件進行監聽。

客戶端Channel:主要註冊read與write事件,關注於具體數據的讀寫。

服務端Channel:主要註冊accept事件,關注於具體連接的接入,這也是與客戶端Channel的read事件最主要的區別。

所有方法,如圖:

在這裏插入圖片描述

Channel重要方法和參數

eventLoop: 返回分配給Channel 的EventLoop
pipeline: 返回分配給Channel 的ChannelPipeline
isActive: 如果Channel 是活動的,則返回true。活動的意義可能依賴於底層的傳輸。例如,一個Socket 傳輸一旦連接到了遠程節點便是活動的,而一個Datagram 傳輸一旦被打開便是活動的。
localAddress: 返回本地的SokcetAddress
remoteAddress: 返回遠程的SocketAddress
write: 將數據寫到遠程節點。這個數據將被傳遞給ChannelPipeline,並且排隊直到它被沖刷
flush: 將之前已寫的數據沖刷到底層傳輸,如一個Socket
writeAndFlush: 一個簡便的方法,等同於調用write()並接着調用flush()

生命週期狀態

ChannelUnregistered :Channel 已經被創建,但還未註冊到EventLoop
ChannelRegistered :Channel 已經被註冊到了EventLoop
ChannelActive :Channel 處於活動狀態(已經連接到它的遠程節點)。它現在可以接收和發送數據了
ChannelInactive :Channel 沒有連接到遠程節點
當這些狀態發生改變時,將會生成對應的事件。這些事件將會被轉發給ChannelPipeline 中的ChannelHandler,其可以隨後對它們做出響應。

狀態的切換:

在這裏插入圖片描述

相關源碼

1. AbstractChannel

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
    // 父 Channel(NioServerSocketChannel 是沒有父channel的)
    private final Channel parent;
    // Channel 唯一ID
    private final ChannelId id;
    // Unsafe 對象,封裝 ByteBuf 的讀寫操作
    private final Unsafe unsafe;
    // 關聯的 Pipeline 對象
    private final DefaultChannelPipeline pipeline;
    
    private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);
    private final CloseFuture closeFuture = new CloseFuture(this);
    // 本地地址
    private volatile SocketAddress localAddress;
    //遠端地址
    private volatile SocketAddress remoteAddress;
    // EventLoop 封裝的 Selector,channel所註冊的eventLoop
    private volatile EventLoop eventLoop;
    // 是否註冊
    private volatile boolean registered;
    private boolean closeInitiated;

    /** Cache for the string representation of this channel */
    private boolean strValActive;
    private String strVal;

構造函數

protected AbstractChannel(Channel parent, ChannelId id) {
    this.parent = parent;
    this.id = id;
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}

// Unsafe 實現交給子類實現
protected abstract AbstractUnsafe newUnsafe();

// 創建 DefaultChannelPipeline 對象
protected DefaultChannelPipeline newChannelPipeline() {
    return new DefaultChannelPipeline(this);
}

說明

1.Unsafe類裏實現了具體的連接與寫數據。比如:網絡的讀,寫,鏈路關閉,發起連接等。之所以命名爲unsafe是不希望外部使用,並非是不安全的。

2.DefaultChannelPipeline 只是一個 Handler 的容器,也可以理解爲一個Handler鏈,具體的邏輯由Handler處理,而每個Handler都會分配一個EventLoop,最終的請求還是要EventLoop來執行,而EventLoop中又調用Channel中的內部類Unsafe對應的方法。
新建一個channel會自動創建一個ChannelPipeline。

3.這裏創建 DefaultChannelPipeline,構造中傳入當前的 Channel,而讀寫數據都是在 ChannelPipeline 中進行的,ChannelPipeline 進行讀寫數據又委託給 Channel 中的 Unsafe 進行操作。

2.AbstractNioChannel

public abstract class AbstractNioChannel extends AbstractChannel {

    // 抽象了 SocketChannel 和 ServerSocketChannel 的公共的父類
    //Socketchannle和ServerSocketChannel的公共操作類,用來設置SelectableChannel相關參數和IO操作
    private final SelectableChannel ch;
    // SelectionKey.OP_READ 讀事件
    protected final int readInterestOp;
    // 註冊到 selector 上返回的 selectorKey
    volatile SelectionKey selectionKey;
    // 是否還有未讀的數據
    boolean readPending;
    private final Runnable clearReadPendingRunnable = new Runnable() {
        @Override
        public void run() {
            clearReadPending0();
        }
    };

    /**
     * The future of the current connection attempt.  If not null, subsequent
     * connection attempts will fail.
     */
    // 連接操作的結果
    private ChannelPromise connectPromise;
    // 連接超時定時任務
    private ScheduledFuture<?> connectTimeoutFuture;
    // 客戶端地址
    private SocketAddress requestedRemoteAddress;
   
    ....

核心方法:

  //核心操作,註冊操作

 //1) 如果當前註冊返回的selectionKey已經被取消,則拋出CancelledKeyException異常,捕獲該異常進行處理。
//2) 如果是第一次處理該異常,調用多路複用器的selectNow()方法將已經取消的selectionKey從多路複用器中刪除掉。操作成功之後,將selected置爲true, 說明之前失效的selectionKey已經被刪除掉。繼續發起下一次註冊操作,如果成功則退出,
//3) 如果仍然發生CancelledKeyException異常,說明我們無法刪除已經被取消的selectionKey,按照JDK的API說明,這種意外不應該發生。如果發生這種問題,則說明可能NIO的相關類庫存在不可恢復的BUG,直接拋出CancelledKeyException異常到上層進行統一處理。

    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
                if (!selected) {
                    // Force the Selector to select now as the "canceled" SelectionKey may still be
                    // cached and not removed because no Select.select(..) operation was called yet.
                    eventLoop().selectNow();
                    selected = true;
                } else {
                    // We forced a select operation on the selector before but the SelectionKey is still cached
                    // for whatever reason. JDK bug ?
                    throw e;
                }
            }
        }
    }

}

SelectionKey 常量值

public abstract class SelectionKey {
    
	public static final int OP_READ = 1 << 0;   //讀操作位
	public static final int OP_WRITE = 1 << 2;  //寫操作位
	public static final int OP_CONNECT = 1 << 3;  //客戶端連接操作位
	public static final int OP_ACCEPT = 1 << 4;  //服務端接受連接操作位

	//如果註冊的操作位爲0表示只是完成註冊功能,說明對任何事件都不感興趣

doBeginRead() 讀之前的準備

  @Override
    protected void doBeginRead() throws Exception {
        // Channel.read() or ChannelHandlerContext.read() was called
        final SelectionKey selectionKey = this.selectionKey;
        if (!selectionKey.isValid()) {
            return;   //key無效的話直接返回
        }

        readPending = true;  //表示讀pending中

        final int interestOps = selectionKey.interestOps();
        if ((interestOps & readInterestOp) == 0) {  //表示當前沒有讀操作位
            selectionKey.interestOps(interestOps | readInterestOp);  //設置讀操作位
        }

    
    }

  //SelectionKey中定義的是否可讀操作
  public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }

3.AbstractNioByteChannel

doWrite操作

配置中設置循環次數是避免半包中數據量過大,IO線程一直嘗試寫操作,此時IO線程無法處理其他IO操作或者定時任務,比如新的消息或者定時任務,如果網絡IO慢或者對方讀取慢等造成IO線程假死的狀態.

    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        int writeSpinCount = -1; //        寫自選次數

        boolean setOpWrite = false;  //寫操作位爲0
        for (;;) {
            Object msg = in.current();
            if (msg == null) {  //從環形數組ChannelOutboundBuffer彈出一條消息,如果爲null,表示消息已經發送完成,
                // Wrote all messages.
                clearOpWrite();  //清除寫標誌位,退出循環
                // Directly return here so incompleteWrite(...) is not called.
                return;
            }

            if (msg instanceof ByteBuf) {
                ByteBuf buf = (ByteBuf) msg;
                int readableBytes = buf.readableBytes();
                if (readableBytes == 0) { //如果可讀字節爲0,則丟棄該消息,循環處理其他消息
                    in.remove();
                    continue;
                }

                boolean done = false;    //消息是否全部發送完畢表示
                long flushedAmount = 0;  //發送的字節數量
                if (writeSpinCount == -1) {
                    //如果爲-1的時候從配置中獲取寫循環次數
                    writeSpinCount = config().getWriteSpinCount();
                }
                for (int i = writeSpinCount - 1; i >= 0; i --) {
                    int localFlushedAmount = doWriteBytes(buf);  //由子類實現寫
                    if (localFlushedAmount == 0) {  //這裏表示本次發送字節爲0,發送TCP緩衝區滿了,所以此時爲了避免空循環一直髮送,這裏就將半包寫表示設置爲true並退出循環
                        setOpWrite = true;
                        break;
                    }
                    //發送成功就對發送的字節計數
                    flushedAmount += localFlushedAmount;
                    if (!buf.isReadable()) { //如果沒有可讀字節,表示已經發送完畢
                        done = true; //表示發送完成,並退出循環
                        break;
                    }
                }
                //通知promise當前寫的進度
                in.progress(flushedAmount); 

                if (done) {  //如果發送完成,移除緩衝的數據
                    in.remove();
                } else {
                    如果沒有完成會調用incompleteWrite方法
                    // Break the loop and so incompleteWrite(...) is called.
                    break;
                }
            } else if (msg instanceof FileRegion) {  //這個是文件傳輸和上面類似
                FileRegion region = (FileRegion) msg;
                boolean done = region.transferred() >= region.count();

                if (!done) {
                    long flushedAmount = 0;
                    if (writeSpinCount == -1) {
                        writeSpinCount = config().getWriteSpinCount();
                    }

                    for (int i = writeSpinCount - 1; i >= 0; i--) {
                        long localFlushedAmount = doWriteFileRegion(region);
                        if (localFlushedAmount == 0) {
                            setOpWrite = true;
                            break;
                        }

                        flushedAmount += localFlushedAmount;
                        if (region.transferred() >= region.count()) {
                            done = true;
                            break;
                        }
                    }

                    in.progress(flushedAmount);
                }

                if (done) {
                    in.remove();
                } else {
                    // Break the loop and so incompleteWrite(...) is called.
                    break;
                }
            } else {
                // Should not reach here.
                throw new Error();
            }
        }
        //如果沒有完成寫看看需要做的事情
        incompleteWrite(setOpWrite);
    }

//未完成寫操作,看看操作
  protected final void incompleteWrite(boolean setOpWrite) {
        // Did not write completely.
        if (setOpWrite) {  //如果當前的寫操作位true,那麼當前多路複用器繼續輪詢處理
            setOpWrite();
        } else {  //否則重新新建一個task任務,讓eventLoop後面點執行flush操作,這樣其他任務才能夠執行
            // Schedule flush again later so other tasks can be picked up in the meantime
            Runnable flushTask = this.flushTask;
            if (flushTask == null) {
                flushTask = this.flushTask = new Runnable() {
                    @Override
                    public void run() {
                        flush();
                    }
                };
            }
            eventLoop().execute(flushTask);
        }
    }

4.AbstractNioMessageChannel

doWrite操作

    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        final SelectionKey key = selectionKey();
        final int interestOps = key.interestOps();

        for (;;) {
            Object msg = in.current();
            if (msg == null) {
                // Wrote all messages.
                if ((interestOps & SelectionKey.OP_WRITE) != 0) {
                    key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
                }
                break;
            }
            try {
                boolean done = false;
                for (int i = config().getWriteSpinCount() - 1; i >= 0; i--) {
                    if (doWriteMessage(msg, in)) {
                        done = true;
                        break;
                    }
                }

                if (done) {
                    in.remove();
                } else {
                    // Did not write all messages.
                    if ((interestOps & SelectionKey.OP_WRITE) == 0) {
                        key.interestOps(interestOps | SelectionKey.OP_WRITE);
                    }
                    break;
                }
            } catch (Exception e) {
                if (continueOnWriteError()) {
                    in.remove(e);
                } else {
                    throw e;
                }
            }
        }
    }

AbstractNioMessageChannel 和AbstractNioByteChannel的消息發送實現比較相似,

不同之處在於:一個發送的是ByteBuf或者FileRegion,它們可以直接被髮送;另一個發送的則是POJO對象。

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章