Netty-Channel架構體系原始碼解讀

賜我白日夢發表於2019-07-17

全文圍繞下圖,Netty-Channel的簡化版架構體系圖展開,從頂層Channel介面開始入手,往下遞進,閒言少敘,直接開擼

概述: 從圖中可以看到,從頂級介面Channel開始,在介面中定義了一套方法當作規範,緊接著的是來兩個抽象的介面實現類,在這個抽象類中對介面中的方法,進行了部分實現,然後開始根據不同的功能分支,分成服務端的Channel和客戶端的Channel

1

回顧

Channel的分類

根據服務端和客戶端,Channel可以分成兩類(這兩大類的分支見上圖):

  • 服務端: NioServerSocketChannel
  • 客戶端: NioSocketChannel

什麼是Channel?

channel是一個管道,用於連線位元組緩衝區Buf和另一端的實體,這個例項可以是Socket,也可以是File, 在Nio網路程式設計模型中, 服務端和客戶端進行IO資料互動(得到彼此推送的資訊)的媒介就是Channel

Netty對Jdk原生的ServerSocketChannel進行了封裝和增強封裝成了NioXXXChannel, 相對於原生的JdkChannel, Netty的Channel增加了如下的元件

  • id 標識唯一身份資訊
  • 可能存在的parent Channel
  • 管道 pepiline
  • 用於資料讀寫的unsafe內部類
  • 關聯上相伴終生的NioEventLoop

本篇部落格,會追溯上圖中的體系關係,找出NioXXXChannel的相對於jdk原生channel在哪裡新增的上面的新元件

原始碼開始-Channel

2

現在來到上圖的Channel部分, 他是一個介面, netty用它規定了一個Channel是該具有的功能,在它的文件對Channel的是什麼,以及對各個元件進行了描述

  • 闡述了channel是什麼,有啥用
  • Channel通過ChannelPipeline中的多個Handler處理器,Channel使用它處理IO資料
  • Channel中的所有Io操作都是非同步的,一經呼叫就馬上返回,於是Netty基於Jdk原生的Future進行了封裝, ChannelFuture, 讀寫操作會返回這個物件,實現自動通知IO操作已完成
  • Channel是可以有parent的, 如下
// 建立客戶端channel時,會把服務端的Channel設定成自己的parent
// 於是就像下面:
  服務端的channel = 客戶端的channel.parent();
  服務的channel.parent()==null;

此外,Channel還定義了大量的抽象方法, 如下:

/**
 * todo 返回一個僅供內部使用的unsafe物件, Chanel上 IO資料的讀寫都是藉助這個類完成的
 */
Unsafe unsafe();
// 返回Channel的管道
ChannelPipeline pipeline();

ByteBufAllocator alloc();

@Override // todo 進入第一個實現 , 讀取Channel中的 IO資料
Channel read();

// 返回Channel id
ChannelId id();  

// todo 返回channel所註冊的 eventLoop
EventLoop eventLoop();

// 返回當前Channel的父channel
Channel parent();

// todo 描述了 關於channel的 一些列配置資訊
ChannelConfig config();

// 檢查channel是否開啟
boolean isOpen();

// 檢查channel是否註冊
boolean isRegistered();

 // todo 什麼是active 他說的是channel狀態, 什麼狀態呢? 當前channel 若和Selector正常的通訊就說明 active
boolean isActive();

// 返回channel的後設資料
ChannelMetadata metadata();

//  伺服器的ip地址
SocketAddress localAddress();

// remoteAddress 客戶端的ip地址
SocketAddress remoteAddress();

ChannelFuture closeFuture();
boolean isWritable();
long bytesBeforeUnwritable();
long bytesBeforeWritable();
@Override
Channel flush();

Channel重要的內部介面 unsafe

Netty中,真正幫助Channel完成IO讀寫操作的是它的內部類unsafe, 原始碼如下, 很多重要的功能在這個介面中定義, 下面列舉的常用的方法

interface Unsafe {
//  把channel註冊進EventLoop
void register(EventLoop eventLoop, ChannelPromise promise);
 
 // todo 給channel繫結一個 adress,
void bind(SocketAddress localAddress, ChannelPromise promise);

// 把channel註冊進Selector
void deregister(ChannelPromise promise);

// 從channel中讀取IO資料
void beginRead();

// 往channe寫入資料
void write(Object msg, ChannelPromise promise);
...
...

AbstractChanel

3

接著往下看,下面來到Channel介面的直接實現類,AbstractChannel 他是個抽象類, AbstractChannel重寫部分Channel介面預定義的方法, 它的抽象內部類AbstractUnsafe實現了Channel的內部介面unsafe

我們現在是從上往下看,但是當我們建立物件使用的時候其實是使用的特化的物件,建立特化的物件就難免會調層層往上呼叫父類的構造方法, 所以我們看看AbstractChannel的構造方法幹了什麼活? 原始碼如下:

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    // todo channelId 代表Chanel唯一的身份標誌
    id = newId();
    // todo 建立一個unsafe物件
    unsafe = newUnsafe();
    // todo 在這裡初始化了每一個channel都會有的pipeline元件
    pipeline = newChannelPipeline();
}

我們看,AbstractChannel建構函式, 接受的子類傳遞進來的引數只有一個parent CHannel,而且,還不有可能為空, 所以在AbstractChannel是沒有維護jdk底層的Channel的, 相反他會維護著Channel關聯的EventLoop,我是怎麼知道的呢? 首先,它的屬性中存在這個欄位,而且,將channel註冊進selector的Register()方法是AbastractChannel重寫的,Selector在哪呢? 在EventLoop裡面,它怎麼得到的呢? 它的子類傳遞了給了它

終於看出來點眉目,構造方法做了四件事

  • 設定parent
    • 如果當前建立的channel是客戶端的channel,把parent初始化為他對應的parent
    • 如果為服務端的channel,這就是null
  • 建立唯一的id
  • 建立針對channel進行io讀寫的unsafe
  • 建立channel的處理器handler鏈 channelPipeline

AbstractChannel中維護著EventLoop

AbstractChanel的重要抽象內部類AbstractUnsafe 繼承了Channel的內部介面Unsafe

他的原始碼如下,我貼出來了兩個重要的方法, 關於這兩個方法的解析,我寫在程式碼的下面


protected abstract class AbstractUnsafe implements Unsafe {

@Override
// todo 入參 eventLoop == SingleThreadEventLoop   promise == NioServerSocketChannel + Executor
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    if (!isCompatible(eventLoop)) {
        promise.setFailure(
                new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }
    // todo 賦值給自己的 事件迴圈, 把當前的eventLoop賦值給當前的Channel上  作用是標記後續的所有註冊的操作都得交給我這個eventLoop處理, 正好對應著下面的判斷
    // todo 保證了 即便是在多執行緒的環境下一條channel 也只能註冊關聯上唯一的eventLoop,唯一的執行緒
    AbstractChannel.this.eventLoop = eventLoop;

    // todo 下面的分支判斷裡面執行的程式碼是一樣的!!, 為什麼? 這是netty的重點, 它大量的使用執行緒, 執行緒之間就會產生同步和併發的問題
    // todo 下面的分支,目的就是把執行緒可能帶來的問題降到最低限度
    // todo 進入inEventLoop() --> 判斷當前執行這行程式碼的執行緒是否就是 SingleThreadEventExecutor裡面維護的那條唯一的執行緒
    // todo 解釋下面分支的必要性, 一個eventLoop可以註冊多個channel, 但是channel的整個生命週期中所有的IO事件,僅僅和它關聯上的thread有關係
    // todo 而且,一個eventLoop在他的整個生命週期中,只和唯一的執行緒進行繫結,
    //
    // todo 當我們註冊channel的時候就得確保給他專屬它的thread,
    // todo 如果是新的連線到了,
    if (eventLoop.inEventLoop()) {
        // todo 進入regist0()
        register0(promise);
    } else {
        try {
            // todo 如果不是,它以一個任務的形式提交  事件迴圈 , 新的任務在新的執行緒開始,  規避了多執行緒的併發
            // todo 他是SimpleThreadEventExucutor中execute()實現的,把任務新增到執行佇列執行
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // todo 進入這個方法doRegister()
        // todo 它把系統建立的ServerSocketChannel 註冊進了選擇器
        doRegister();
        neverRegistered = false;
        registered = true;

        // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
        // user may already fire events through the pipeline in the ChannelFutureListener.
        // todo 確保在 notify the promise前呼叫 handlerAdded(...)
        // todo 這是必需的,因為使用者可能已經通過ChannelFutureListener中的管道觸發了事件。
        // todo 如果需要的話,執行HandlerAdded()方法
        // todo 正是這個方法, 回撥了前面我們新增 Initializer 中新增 Accpter的重要方法
        pipeline.invokeHandlerAddedIfNeeded();

        // todo  !!!!!!!  觀察者模式!!!!!!  通知觀察者,誰是觀察者?  暫時理解ChannelHandler 是觀察者
        safeSetSuccess(promise);

        // todo 傳播行為, 傳播什麼行為呢?   在head---> ServerBootStraptAccptor ---> tail傳播事件ChannelRegistered  , 也就是挨個呼叫它們的ChannelRegisted函式
        pipeline.fireChannelRegistered();
        // Only fire a channelActive if the channel has never been registered. This prevents firing
        // multiple channel actives if the channel is deregistered and re-registered.
        // todo 對於服務端:  javaChannel().socket().isBound(); 即  當Channel繫結上了埠   isActive()才會返回true
        // todo 對於客戶端的連線 ch.isOpen() && ch.isConnected(); 返回true , 就是說, Channel是open的 開啟狀態的就是true
        if (isActive()) {
            if (firstRegistration) {
                // todo 在pipeline中傳播ChannelActive的行為,跟進去
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                // This channel was registered before and autoRead() is set. This means we need to begin read
                // again so that we process inbound data.
                //
                // See https://github.com/netty/netty/issues/4805
                // todo 可以接受客戶端的資料了
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();

    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    // See: https://github.com/netty/netty/issues/576
    if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
        localAddress instanceof InetSocketAddress &&
        !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
        !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
        // Warn a user about the fact that a non-root user can't receive a
        // broadcast packet on *nix if the socket is bound on non-wildcard address.
        logger.warn(
                "A non-root user can't receive a broadcast packet if the socket " +
                "is not bound to a wildcard address; binding to a non-wildcard " +
                "address (" + localAddress + ") anyway as requested.");
    }

    boolean wasActive = isActive();
    // todo 由於埠的繫結未完成,所以 wasActive是 false

    try {
        // todo 繫結埠, 進去就是NIO原生JDK繫結埠的程式碼
        doBind(localAddress);
        // todo 埠繫結完成  isActive()是true
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }
    // todo 根據上面的邏輯判斷, 結果為 true
    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            // todo 來到這裡很重要, 向下傳遞事件行為, 傳播行為的時候, 從管道的第一個節點開始傳播, 第一個節點被封裝成 HeadContext的物件
           // todo 進入方法, 去 HeadContext裡面檢視做了哪些事情
            // todo 她會觸發channel的read, 最終重新為 已經註冊進selector 的 chanel, 二次註冊新增上感性趣的accept事件
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    // todo 觀察者模式, 設定改變狀態, 通知觀察者的方法回撥
    safeSetSuccess(promise);
}

AbstractChannel抽象內部類的register(EventLoop,channelPromise)方法

這個方法,是將channel註冊進EventLoop的Selector, 它的呼叫順序如下:

本類方法 regist()--> 本類方法 register0() --> 本類抽象方法doRegister()

doRegister() 在這裡設計成抽象方法,等著子類去具體的實現, 為啥這樣做呢?

剛才說了,AbstractChannel本身就是個模板,而且它僅僅維護了EventLoop,沒有拿到channel引用的它根本不可能進行註冊的邏輯,那誰有jdk原生channel的引用呢? 它的直接子類AbstractNioChannel下面是AbstractNioChannel的構造方法, 它自己維護jdk原生的Channel,所以由他重寫doRegister(),

*/ // todo 無論是服務端的channel 還是客戶端的channel都會使用這個方法進行初始化
// // TODO: 2019/6/23                null        ServerSocketChannel       accept
// todo  如果是在建立NioSocketChannel  parent==NioServerSocketChannel  ch == SocketChanel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);// todo  繼續向上跟,建立基本的元件
    // todo 如果是建立NioSocketChannel   這就是在儲存原生的jdkchannel
    // todo 如果是建立NioServerSocketChannel   這就是在儲存ServerSocketChannel
    this.ch = ch;
    // todo 設定上感興趣的事件
    this.readInterestOp = readInterestOp;
    try {
        // todo 作為服務端, ServerSocketChannel 設定為非阻塞的
        // todo 作為客戶端   SocketChannel 設定為非阻塞的
        ch.configureBlocking(false);
    } catch (IOException e) {

AbstractChannel抽象內部類的bind()方法

bind()方法的呼叫順序, 本類方法 bind()--> 本類的抽象方法 dobind()

方法的目的是給Channel繫結上屬於它的埠,同樣有一個抽象方法,等著子類去實現,因為我們已經知道了AbstractChannel不維護channel的引用,於是我就去找dobind()這個抽象函式的實現, 結果發現,AbstractChannel的直接子類AbstractNioChannel中根本不沒有他的實現,這是被允許的,因為AbstractNioChannel本身也是抽象類, 到底是誰實現呢? 如下圖:在NioServerSocketChannel中獲取出 Jdk原生的channel, 客戶端和服務端的channel又不同,所以繫結埠這中特化的任務,交給他們自己實現

4

AbstractChannel的beginRead()()方法

上面完成註冊之後,就去繫結埠,當埠繫結完成,就會channel處於active狀態,下一步就是執行beginRead() ,執行的流程如下

本類抽象方法 beginRead() --> 本類抽象方法doBeginRead()

這個read() 就是從已經繫結好埠的channel中讀取IO資料,和上面的方法一樣,對應沒有channel應用的AbstractChannel來說,netty把它設計成抽象方法,交給擁有jdk 原生channel引用的AbstractNioChannel實現

小結:

AbstractChannel作為Channel的直接實現類,本身又是抽象類,於是它實現了Channel的預留的一些抽象方法, 初始化了channel的四個元件 id pipeline unsafe parent, 更為重要的是它的抽象內部類 實現了 關於nettyChannel的註冊,繫結,讀取資料的邏輯,而且以抽象類的方法,挖好了填空題等待子類的特化實現


遞進AbstractNioChannel

5

跟進構造方法

依然是來到AbstractNioChannel的構造方法,發現它做了如下的構造工作:

  • 把parent傳遞給了AbstractChannel
  • 把子類傳遞過來的Channel要告訴Selector的感興趣的選項儲存
  • 設定channel為非阻塞
// todo 無論是服務端的channel 還是客戶端的channel都會使用這個方法進行初始化
// // TODO: 2019/6/23                null        ServerSocketChannel       accept
// todo  如果是在建立NioSocketChannel  parent==NioServerSocketChannel  ch == SocketChanel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);// todo  繼續向上跟,建立基本的元件
    // todo 如果是建立NioSocketChannel   這就是在儲存原生的jdkchannel
    // todo 如果是建立NioServerSocketChannel   這就是在儲存ServerSocketChannel
    this.ch = ch;
    // todo 設定上感興趣的事件
    this.readInterestOp = readInterestOp;
    try {
        // todo 作為服務端, ServerSocketChannel 設定為非阻塞的
        // todo 作為客戶端   SocketChannel 設定為非阻塞的
        ch.configureBlocking(false);
    } catch (IOException e) {

重寫了它父類的doRegister()

AbstractNioChannel維護channel的引用,真正的實現把 jdk 原生的 channel註冊進 Selector中

@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
    // todo  javaChannel() -- 返回SelectableChanel 可選擇的Channel,換句話說,可以和Selector搭配使用,他是channel體系的頂級抽象類, 實際的型別是 ServerSocketChannel
    // todo  eventLoop().unwrappedSelector(), -- >  獲取選擇器, 現在在AbstractNioChannel中 獲取到的eventLoop是BossGroup裡面的
    // todo  到目前看, 他是把ServerSocketChannel(系統建立的) 註冊進了 EventLoop的選擇器
    // todo 這裡的 最後一個引數是  this是當前的channel , 意思是把當前的Channel當成是一個 attachment(附件) 繫結到selector上 作用???
    // todo  現在知道了attachment的作用了
     //    todo 1. 當channel在這裡註冊進 selector中返回一個selectionKey, 這個key告訴selector 這個channel是自己的
     //    todo 2. 當selector輪詢到 有channel出現了自己的感興趣的事件時, 需要從成百上千的channel精確的匹配出 出現Io事件的channel,
    //     todo     於是seleor就在這裡提前把channel存放入 attachment中, 後來使用
    // todo 最後一個 this 引數, 如果是服務啟動時, 他就是NioServerSocketChannel   如果是客戶端他就是 NioSocketChannel
    // todo 到目前為止, 雖然註冊上了,但是它不關心任何事件
    selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
    return;
} catch (CancelledKeyException e) {

新增內部介面

AbstractNioChannel新新增了一個內部介面,作為原Channel的擴充套件,原始碼如下, 我們著重關心的就是這個新介面的 read()方法, 它的作用是從channel去讀取IO資料,作為介面的抽象方法,它規範服務端和客戶端根據自己需求去不同的實現這個read()

怎麼特化實現這個read方法呢? 若是服務端,它read的結果就是一個新的客戶端的連線, 如果是客戶端,它read的結果就是 客戶端傳送過來的資料,所以這個read()很有必要去特化

/**
 * Read from underlying {@link SelectableChannel}
 */
// todo 兩個實現類, NioByteUnsafe , 處理關於客戶端發來的資訊
// todo NioMessageUnsafe   處理客戶端新進來的連線
void read();


/**
* Special {@link Unsafe} sub-type which allows to access the underlying {@link SelectableChannel}
*/
public interface NioUnsafe extends Unsafe {
/**
 * Return underlying {@link SelectableChannel}
 */
SelectableChannel ch();

/**
 * Finish connect
 */
void finishConnect();


void forceFlush();
}

AbstractNioChannel抽象內部內同時繼承了它父類的AbstractUnsafe實現了當前的NioUnsafe, 再往後看, 問題來了, 服務端和客戶端在的針對read的特化實現在哪裡呢? 想想看肯定在它子類的unsafe內部類中,如下圖,紫框框

6

一會再具體看這兩個 內部類是如何特化read的 注意啊,不再是抽象的了

再進一步 AbstractNioMessageChannel

它的建構函式如下, 只是呼叫父類的建構函式,傳遞引數

protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    // todo 在進去
    // todo  null  ServerSocketChannel   accept
    super(parent, ch, readInterestOp);
}

AbstractNioMessageChannelMessageNioUnsaferead()特化實現

在read方法中,我們可以看到,他呼叫是本類的抽象方法doReadMessages(List<Object> buf), 方法的實現類是繼承體系的最底層的NioServerSocketChannel, 因為他就是那個特化的服務端channel

當然如果我們一開始跟進read()時,來到的客戶端的AbstractNioByteChannel,現在我們找到的doReadMessage()就是由 客戶端的channelNioSocketChannel完成的doReadBytes()

// todo 用於處理新連結進來的內部類
private final class NioMessageUnsafe extends AbstractNioUnsafe {

// todo 這個容器用於存放臨時讀到的連線
private final List<Object> readBuf = new ArrayList<Object>();

// todo 接受新連結的 read來到這裡
@Override
public void read() {
    ...
    doBeginRead(buf);
    ...
}

// todo 處理新的連線 是在 NioServerSocketChannel中實現的, 進入檢視
protected abstract int doReadMessages(List<Object> buf) throws Exception;

最終,特化的channel實現

現在我們就來到了最底層,整張繼承圖就全部展現在眼前了,下面就去看看,特化的服務端Channel NioServetSocketChannelNioSocketChanneldoReadMessages()doReadBytes()的各自實現

服務端, 我們看到了,它的特化read()是在建立新的 Jdk遠端channel, 因為它在建立新的連線chanel

 @Override
protected int doReadMessages(List<Object> buf) throws Exception {
    // todo java Nio底層在這裡 建立jdk底層的 原生channel
    SocketChannel ch = SocketUtils.accept(javaChannel());

    try {
        if (ch != null) {
            // todo  把java原生的channel, 封裝成 Netty自定義的封裝的channel , 這裡的buf是list集合物件,由上一層傳遞過來的
            // todo  this  --  NioServerSocketChannel
            // todo  ch --     SocketChnnel
            buf.add(new NioSocketChannel(this, ch));
            return 1;
        }
        ...

客戶端, 讀取客戶端傳送過來的IO資料

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

小結:

Netty的channel繼承體系,到現在就完成了, 相信,當我們現在再正著從 NioServerEventLoop入手,看他的初始化過程應該很簡單了, 其中我希望自己可以牢記幾個點

  • AbstractChannel維護NioChannelEventLoop
  • AbstractNioChannel維護jdk原生 channel
  • AbstractChannel中的AbstractUnsafe主要是定義了一套模板,給子類提供了填空題,下面的三個填空
    • 註冊 把chanel註冊進Selector
    • 繫結 把chanel繫結上埠
    • 新增感興趣的事件, 給建立出來的channel二次註冊上netty可以處理的感興趣的事件
  • channel的io操作是unsafe內部類完成的
    • 服務端從channel,讀取出新連線NioMessageUnsafe
    • 客戶端從channel,讀取出資料NioByteUnsafe

相關文章