netty系列之:channel,ServerChannel和netty中的實現

flydean發表於2022-02-23

簡介

我們知道channel是netty中用於溝通ByteBuf和Event的橋樑,在netty服務的建立過程中,不管是客戶端的Bootstrap還是伺服器端的ServerBootstrap,都需要呼叫channel方法來指定對應的channel型別。

那麼netty中channel到底有哪些型別呢?他們具體是如何工作的呢?一起來看看。

channel和ServerChannel

Channel在netty中是一個interface,在Channel中定義了很多非常有用的方法。通常來說如果是客戶端的話,對應的channel就是普通的channel。如果是伺服器端的話,對應的channel就應該是ServerChannel。

那麼客戶端channel和伺服器端channel有什麼區別呢?我們先來看下ServerChannel的定義:

public interface ServerChannel extends Channel {
    // This is a tag interface.
}

可以看到ServerChannel繼承自Channel,表示服務端的Channel也是Channel的一種。

但是很奇怪的是,你可以看到ServerChannel中並沒有新增任何新的方法。也就是說ServerChannel和Channel在定義上本質是一樣的。你可以把ServerChannel看做是一個tag interface而已。

那麼channel和ServerChannel有什麼聯絡呢?

我們知道在Channel中定義了一個parent方法:

Channel parent();

這個parent方法返回的是該channel的父channel。我們以最簡單的LocalChannel和LocalServerChannel為例,來檢視一下他們的父子關係到底是怎麼建立的。

首先parent的值是通過LocalChannel和LocalServerChannel的公共父類AbstractChannel來實現的:

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

對於LocalChannel來說,可以通過它的建構函式來設定parent channel:

    protected LocalChannel(LocalServerChannel parent, LocalChannel peer) {
        super(parent);
        config().setAllocator(new PreferHeapByteBufAllocator(config.getAllocator()));
        this.peer = peer;
        localAddress = parent.localAddress();
        remoteAddress = peer.localAddress();
    }

我們知道當client端想要連線到server端的時候,需要呼叫client channel的connect方法,對於LocalChannel來說,它的connect方法實際上呼叫的是pipeline的connect方法:

public ChannelFuture connect(SocketAddress remoteAddress) {
        return pipeline.connect(remoteAddress);
    }

最終會呼叫LocalChannel中的LocalUnsafe.connect方法。

而在LocalUnsafe.connect方法中又會呼叫serverChannel.serve方法。

serverChannel的newLocalChannel方法會建立新的LocalChannel並返回:

    protected LocalChannel newLocalChannel(LocalChannel peer) {
        return new LocalChannel(this, peer);
    }

這裡使用newLocalChannel方法建立的LocalChannel就是serverChannel的子channel。

最後返回的LocalChannel會作為client端LocalChannel的peer channel而存在。

netty中channel的實現

在netty中channel和Serverchannel有很多個實現類,用來完成不同的業務功能。

為了循序漸進一步步瞭解netty中channel的祕密,這裡我們先來探討一下netty中channel的基本實現LocalChannel和LocalServerChannel的工作原理。

下圖是LocalChannel和LocalServerChannel的主要繼承和依賴關係:

netty系列之:channel,ServerChannel和netty中的實現

從圖中可以看到,LocalChannel繼承自AbstractChannel而LocalServerChannel則繼承自AbstractServerChannel。

因為ServerChannel繼承自Channel,所以很自然的AbstractServerChannel又繼承自AbstractChannel。

接下來,我們通過對比分析AbstractChannel和AbstractServerChannel,LocalChannel和LocalServerChannel來一探netty中channel實現的底層原理。

AbstractChannel和AbstractServerChannel

AbstractChannel是對Channel的最基本的實現。先來看下AbstractChannel中都有那些功能。

首先AbstractChannel中定義了Channel介面中要返回的一些和channel相關的基本屬性,包括父channel,channel id,pipline,localAddress,remoteAddress,eventLoop等,如下所示:

    private final Channel parent;
    private final ChannelId id;
    private final DefaultChannelPipeline pipeline;
    private volatile SocketAddress localAddress;
    private volatile SocketAddress remoteAddress;
    private volatile EventLoop eventLoop;

    private final Unsafe unsafe;

要注意的是AbstractChannel中還有一個非常中要的Unsafe屬性。

Unsafe本身就是Channel介面中定義的一個內部介面,它的作用就是為各個不同型別的transport提供特定的實現。

從名字可以看出Unsafe是一個不安全的實現,它只是在netty的原始碼中使用,它是不能出現在使用者程式碼中的。或者你可以將Unsafe看做是底層的實現,而包裹他的AbstractChannel或者其他的Channel是對底層實現的封裝,對於普通使用者來說,他們只需要使用Channel就可以了,並不需要深入到更底層的內容。

另外,對於Unsafe來說,除了下面幾個方法之外,剩餘的方法必須從 I/O thread中呼叫:

localAddress()
remoteAddress()
closeForcibly()
register(EventLoop, ChannelPromise)
deregister(ChannelPromise)
voidPromise()

和一些基本的狀態相關的資料:

private volatile boolean registered;
private boolean closeInitiated;

除了基本的屬性設定和讀取之外,我們channel中最終要的方法主要有下面幾個:

  1. 用於建立伺服器端服務的bind方法:
public ChannelFuture bind(SocketAddress localAddress) {
        return pipeline.bind(localAddress);
    }
  1. 用於客戶端建立和伺服器端連線的connect方法:
public ChannelFuture connect(SocketAddress remoteAddress) {
        return pipeline.connect(remoteAddress);
    }
  1. 斷開連線的disconnect方法:
public ChannelFuture disconnect() {
        return pipeline.disconnect();
    }
  1. 關閉channel的close方法:
public ChannelFuture close() {
        return pipeline.close();
    }
  1. 取消註冊的deregister方法:
public ChannelFuture deregister() {
        return pipeline.deregister();
    }
  1. 重新整理資料的flush方法:
    public Channel flush() {
        pipeline.flush();
        return this;
    }
  1. 讀取資料的read方法:
    public Channel read() {
        pipeline.read();
        return this;
    }
  1. 寫入資料的方法:
    public ChannelFuture write(Object msg) {
        return pipeline.write(msg);
    }

可以看到這些channel中的讀寫和繫結工作都是由和channel相關的pipeline來執行的。

其實也很好理解,channel只是一個通道,和資料相關的操作,還是需要在管道中執行。

我們以bind方法為例子,看一下AbstractChannel中的pipline是怎麼實現的。

在AbstractChannel中,預設的pipeline是DefaultChannelPipeline,它的bind方法如下:

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

這裡的unsafe實際上就是AbstractChannel中的unsafe,unsafe中的bind方法最終會呼叫AbstractChannel中的dobind方法:

protected abstract void doBind(SocketAddress localAddress) throws Exception;

所以歸根到底,如果是基於AbstractChannel的各種實現,那麼只需要實現它的這些do*方法即可。

好了,AbstractChannel的介紹完畢了。 我們再來看一下AbstractServerChannel。AbstractServerChannel繼承自AbstractChannel並且實現了ServerChannel介面。

public abstract class AbstractServerChannel extends AbstractChannel implements ServerChannel 

我們知道ServerChannel和Channel實際上是相同的,所以AbstractServerChannel只是在AbstractChannel的實現上進行了一些調整。

在AbstractServerChannel中,我們一起來觀察一下AbstractServerChannel和AbstractChannel到底有什麼不同。

首先是AbstractServerChannel的建構函式:

protected AbstractServerChannel() {
        super(null);
    }

建構函式中,super的parent channel是null,表示ServerChannel本身並不存在父channel,這是ServerChannel和client channel
的第一個不同之處。因為server channel可以通過worker event loop來接受client channel,所以server channel是client channel的父channel。

另外,我們還觀察幾個方法的實現:

public SocketAddress remoteAddress() {
        return null;
    }

對於ServerChannel來說不需要主動連線到遠端的Server,所以並沒有remoteAddress。

另外,因為斷開連線是由client端主動呼叫的,所以server channel的doDisconnect會丟擲不支援該操作的異常:

    protected void doDisconnect() throws Exception {
        throw new UnsupportedOperationException();
    }

同時ServerChannel只是用來負責accept和client channel建立關聯關係,所以server channel本身並不支援向channel內進行的write操作,所以這個doWrite方法也是不支援的:

    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        throw new UnsupportedOperationException();
    }

最後ServerChannel只支援bind操作,所以DefaultServerUnsafe中的connect方法也會丟擲UnsupportedOperationException.

LocalChannel和LocalServerChannel

LocalChannel和LocalServerChannel是AbstractChannel和AbstractServerChannel的最基本的實現。從名字就可以看出來,這兩個Channel是本地channel,我們來看一下這兩個Channel的具體實現。

首先我們來看一下LocalChannel,LocalChannel有幾點對AbstractChannel的擴充套件。

第一個擴充套件點是LocalChannel中新增了channel的幾個狀態:

private enum State { OPEN, BOUND, CONNECTED, CLOSED }

通過不同的狀態,可以對channel進行更加細粒度的控制。

另外LocalChannel中新增了一個非常重要的屬性:

private volatile LocalChannel peer;

因為LocalChannel表示的是客戶端channel,所以這個peer表示的是client channel對等的server channel。接下來我們看一下具體的實現。

首先是LocalChannel的建構函式:

    protected LocalChannel(LocalServerChannel parent, LocalChannel peer) {
        super(parent);
        config().setAllocator(new PreferHeapByteBufAllocator(config.getAllocator()));
        this.peer = peer;
        localAddress = parent.localAddress();
        remoteAddress = peer.localAddress();
    }

LocalChannel可以接受一個LocalServerChannel作為它的parent,還有一個LocalChannel作為它的對等channel。

那麼這個peer是怎麼建立的呢?

我們來看一下LocalUnsafe中connect的邏輯。

            if (state != State.BOUND) {
                // Not bound yet and no localAddress specified - get one.
                if (localAddress == null) {
                    localAddress = new LocalAddress(LocalChannel.this);
                }
            }

            if (localAddress != null) {
                try {
                    doBind(localAddress);
                } catch (Throwable t) {
                    safeSetFailure(promise, t);
                    close(voidPromise());
                    return;
                }
            }

首先判斷當前channel的狀態,如果是非繫結狀態,那麼需要進行繫結操作。首先根據傳入的LocalChannel建立對應的LocalAddress。

這個LocalAddress只是LocalChannel的一種表現形式,並沒有什麼特別的功能。

我們來看一下這個doBind方法:

    protected void doBind(SocketAddress localAddress) throws Exception {
        this.localAddress =
                LocalChannelRegistry.register(this, this.localAddress,
                        localAddress);
        state = State.BOUND;
    }

LocalChannelRegistry中維護了一個static的map,這個map中存放的就是註冊過的Channel.

這裡註冊是為了在後面方便的拿到對應的channel。

註冊好localChannel之後,接下來就是根據註冊好的remoteAddress來獲取對應的LocalServerChannel,最後呼叫LocalServerChannel的serve方法建立一個新的peer channel:

Channel boundChannel = LocalChannelRegistry.get(remoteAddress);
            if (!(boundChannel instanceof LocalServerChannel)) {
                Exception cause = new ConnectException("connection refused: " + remoteAddress);
                safeSetFailure(promise, cause);
                close(voidPromise());
                return;
            }

            LocalServerChannel serverChannel = (LocalServerChannel) boundChannel;
            peer = serverChannel.serve(LocalChannel.this);

serve方法首先會建立一個新的LocalChannel:

    protected LocalChannel newLocalChannel(LocalChannel peer) {
        return new LocalChannel(this, peer);
    }

如果我們把之前的Localchannel稱為channelA,這裡建立的新的LocalChannel稱為channelB。那麼最後的結果就是channelA的peer是channelB,而channelB的parent是LocalServerChannel,channelB的peer是channelA。

這樣就構成了一個對等channel之間的關係。

接下來我們看下localChannel的read和write到底是怎麼工作的。

首先看一下LocalChannel的doWrite方法:

Object msg = in.current();
...
peer.inboundBuffer.add(ReferenceCountUtil.retain(msg));
in.remove();
...
finishPeerRead(peer);

首先從ChannelOutboundBuffer拿到要寫入的msg,將其加入peer的inboundBuffer中,最後呼叫finishPeerRead方法。

從方法名字可以看出finishPeerRead就是呼叫peer的read方法。

事實上該方法會呼叫peer的readInbound方法,從剛剛寫入的inboundBuffer中讀取訊息:

    private void readInbound() {
        RecvByteBufAllocator.Handle handle = unsafe().recvBufAllocHandle();
        handle.reset(config());
        ChannelPipeline pipeline = pipeline();
        do {
            Object received = inboundBuffer.poll();
            if (received == null) {
                break;
            }
            pipeline.fireChannelRead(received);
        } while (handle.continueReading());

        pipeline.fireChannelReadComplete();
    }

所以,對於localChannel來說,它的寫實際上寫入到peer的inboundBuffer中。然後再呼叫peer的讀方法,從inboundBuffer中讀取資料。

相較於localChannel來說,localServerChannel多了一個serve方法,用來建立peer channel,並呼叫readInbound開始從inboundBuffer中讀取資料。

總結

本章詳細介紹了channel和serverChannel的區別,和他們的最簡單的本地實現。希望大家對channel和serverChannel的工作原理有了最基本的瞭解。

本文已收錄於 http://www.flydean.com/04-2-netty-channel-vs-serverchannel-md/

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

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

相關文章