netty原始碼分析之服務端啟動全解析

閃電俠發表於2018-10-19

background

netty 是一個非同步事件驅動的網路通訊層框架,其官方文件的解釋為

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

我們在新美大訊息推送系統sailfish(日均推送訊息量50億),新美大移動端代理優化系統shark(日均吞吐量30億)中,均選擇了netty作為底層網路通訊框架。

既然兩大如此重要的系統底層都使用到了netty,所以必然要對netty的機制,甚至原始碼瞭若指掌,於是,便催生了netty原始碼系列文章。後面,我會通過一系列的主題把我從netty原始碼裡所學到的毫無保留地介紹給你,原始碼基於4.1.6.Final

why netty

netty底層基於jdk的NIO,我們為什麼不直接基於jdk的nio或者其他nio框架?下面是我總結出來的原因

1.使用jdk自帶的nio需要了解太多的概念,程式設計複雜 2.netty底層IO模型隨意切換,而這一切只需要做微小的改動 3.netty自帶的拆包解包,異常檢測等機制讓你從nio的繁重細節中脫離出來,讓你只需要關心業務邏輯 4.netty解決了jdk的很多包括空輪訓在內的bug 5.netty底層對執行緒,selector做了很多細小的優化,精心設計的reactor執行緒做到非常高效的併發處理 6.自帶各種協議棧讓你處理任何一種通用協議都幾乎不用親自動手 7.netty社群活躍,遇到問題隨時郵件列表或者issue 8.netty已經歷各大rpc框架,訊息中介軟體,分散式通訊中介軟體線上的廣泛驗證,健壯性無比強大

dive into netty

瞭解了這麼多,今天我們就從一個例子出來,開始我們的netty原始碼之旅。

本篇主要講述的是netty是如何繫結埠,啟動服務。啟動服務的過程中,你將會了解到netty各大核心元件,我先不會細講這些元件,而是會告訴你各大元件是怎麼串起來組成netty的核心

example

下面是一個非常簡單的服務端啟動程式碼

public final class SimpleServer {

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new SimpleServerHandler())
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                        }
                    });

            ChannelFuture f = b.bind(8888).sync();

            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private static class SimpleServerHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("channelActive");
        }

        @Override
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            System.out.println("channelRegistered");
        }

        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            System.out.println("handlerAdded");
        }
    }
}
複製程式碼

簡單的幾行程式碼就能開啟一個服務端,埠繫結在8888,使用nio模式,下面講下每一個步驟的處理細節

EventLoopGroup 已經在我的其他文章中詳細剖析過,說白了,就是一個死迴圈,不停地檢測IO事件,處理IO事件,執行任務

ServerBootstrap 是服務端的一個啟動輔助類,通過給他設定一系列引數來繫結埠啟動服務

group(bossGroup, workerGroup) 我們需要兩種型別的人幹活,一個是老闆,一個是工人,老闆負責從外面接活,接到的活分配給工人幹,放到這裡,bossGroup的作用就是不斷地accept到新的連線,將新的連線丟給workerGroup來處理

.channel(NioServerSocketChannel.class) 表示服務端啟動的是nio相關的channel,channel在netty裡面是一大核心概念,可以理解為一條channel就是一個連線或者一個服務端bind動作,後面會細說

.handler(new SimpleServerHandler() 表示伺服器啟動過程中,需要經過哪些流程,這裡SimpleServerHandler最終的頂層介面為ChannelHander,是netty的一大核心概念,表示資料流經過的處理器,可以理解為流水線上的每一道關卡

childHandler(new ChannelInitializer<SocketChannel>)...表示一條新的連線進來之後,該怎麼處理,也就是上面所說的,老闆如何給工人配活

ChannelFuture f = b.bind(8888).sync(); 這裡就是真正的啟動過程了,繫結8888埠,等待伺服器啟動完畢,才會進入下行程式碼

f.channel().closeFuture().sync(); 等待服務端關閉socket

bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); 關閉兩組死迴圈

上述程式碼可以很輕鬆地再本地跑起來,最終控制檯的輸出為:

handlerAdded
channelRegistered
channelActive
複製程式碼

關於為什麼會順序輸出這些,深入分析之後其實很easy

深入細節

ServerBootstrap 一系列的引數配置其實沒啥好講的,無非就是使用method chaining的方式將啟動伺服器需要的引數儲存到filed。我們的重點落入到下面這段程式碼

b.bind(8888).sync();
複製程式碼

這裡說一句:我們剛開始看原始碼,對細節沒那麼清楚的情況下可以藉助IDE的debug功能,step by step,one step one test或者二分test的方式,來確定哪行程式碼是最終啟動服務的入口,在這裡,我們已經確定了bind方法是入口,我們跟進去,分析

public ChannelFuture bind(int inetPort) {
    return bind(new InetSocketAddress(inetPort));
} 
複製程式碼

通過埠號建立一個 InetSocketAddress,然後繼續bind

public ChannelFuture bind(SocketAddress localAddress) {
    validate();
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    return doBind(localAddress);
}
複製程式碼

validate() 驗證服務啟動需要的必要引數,然後呼叫doBind()

private ChannelFuture doBind(final SocketAddress localAddress) {
    //...
    final ChannelFuture regFuture = initAndRegister();
    //...
    final Channel channel = regFuture.channel();
    //...
    doBind0(regFuture, channel, localAddress, promise);
    //...
    return promise;
}
複製程式碼

這裡,我去掉了細枝末節,讓我們專注於核心方法,其實就兩大核心一個是 initAndRegister(),以及doBind0()

其實,從方法名上面我們已經可以略窺一二,init->初始化,register->註冊,那麼到底要註冊到什麼呢?聯絡到nio裡面輪詢器的註冊,可能是把某個東西初始化好了之後註冊到selector上面去,最後bind,像是在本地繫結埠號,帶著這些猜測,我們深入下去

initAndRegister()

final ChannelFuture initAndRegister() {
    Channel channel = null;
    // ...
    channel = channelFactory.newChannel();
    //...
    init(channel);
    //...
    ChannelFuture regFuture = config().group().register(channel);
    //...
    return regFuture;
}
複製程式碼

我們還是專注於核心程式碼,拋開邊角料,我們看到 initAndRegister() 做了幾件事情 1.new一個channel 2.init這個channel 3.將這個channel register到某個物件

我們逐步分析這三件事情

1.new一個channel

我們首先要搞懂channel的定義,netty官方對channel的描述如下

A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind

這裡的channel,由於是在服務啟動的時候建立,我們可以和普通Socket程式設計中的ServerSocket對應上,表示服務端繫結的時候經過的一條流水線

我們發現這條channel是通過一個 channelFactory new出來的,channelFactory 的介面很簡單

public interface ChannelFactory<T extends Channel> extends io.netty.bootstrap.ChannelFactory<T> {
    /**
     * Creates a new channel.
     */
    @Override
    T newChannel();
}
複製程式碼

就一個方法,我們檢視channelFactory被賦值的地方

AbstractBootstrap.java

public B channelFactory(ChannelFactory<? extends C> channelFactory) {
    if (channelFactory == null) {
        throw new NullPointerException("channelFactory");
    }
    if (this.channelFactory != null) {
        throw new IllegalStateException("channelFactory set already");
    }

    this.channelFactory = channelFactory;
    return (B) this;
}
複製程式碼

在這裡被賦值,我們層層回溯,檢視該函式被呼叫的地方,發現最終是在這個函式中,ChannelFactory被new出

public B channel(Class<? extends C> channelClass) {
    if (channelClass == null) {
        throw new NullPointerException("channelClass");
    }
    return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
複製程式碼

這裡,我們的demo程式呼叫channel(channelClass)方法的時候,將channelClass作為ReflectiveChannelFactory的建構函式建立出一個ReflectiveChannelFactory

demo端的程式碼如下:

.channel(NioServerSocketChannel.class);
複製程式碼

然後回到本節最開始

channelFactory.newChannel();
複製程式碼

我們就可以推斷出,最終是呼叫到 ReflectiveChannelFactory.newChannel() 方法,跟進

public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {

    private final Class<? extends T> clazz;

    public ReflectiveChannelFactory(Class<? extends T> clazz) {
        if (clazz == null) {
            throw new NullPointerException("clazz");
        }
        this.clazz = clazz;
    }

    @Override
    public T newChannel() {
        try {
            return clazz.newInstance();
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class " + clazz, t);
        }
    }
}
複製程式碼

看到clazz.newInstance();,我們明白了,原來是通過反射的方式來建立一個物件,而這個class就是我們在ServerBootstrap中傳入的NioServerSocketChannel.class

結果,繞了一圈,最終建立channel相當於呼叫預設建構函式new出一個 NioServerSocketChannel物件

這裡提一下,讀原始碼細節,有兩種讀的方式,一種是回溯,比如用到某個物件的時候可以逐層追溯,一定會找到該物件的最開始被建立的程式碼區塊,還有一種方式就是自頂向下,逐層分析,一般用在分析某個具體的方法,庖丁解牛,最後拼接出完整的流程

接下來我們就可以將重心放到 NioServerSocketChannel的預設建構函式

private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
複製程式碼

private static ServerSocketChannel newSocket(SelectorProvider provider) {
    //...
    return provider.openServerSocketChannel();
}
複製程式碼

通過SelectorProvider.openServerSocketChannel()建立一條server端channel,然後進入到以下方法

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
複製程式碼

這裡第一行程式碼就跑到父類裡面去了,第二行,new出來一個 NioServerSocketChannelConfig,其頂層介面為 ChannelConfig,netty官方的描述如下

A set of configuration properties of a Channel.

基本可以判定,ChannelConfig 也是netty裡面的一大核心模組,初次看原始碼,看到這裡,我們大可不必深挖這個物件,而是在用到的時候再回來深究,只要記住,這個物件在建立NioServerSocketChannel物件的時候被建立即可

我們繼續追蹤到 NioServerSocketChannel 的父類

AbstractNioMessageChannel.java

protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent, ch, readInterestOp);
}
複製程式碼

繼續往上追

AbstractNioChannel.java

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = readInterestOp;
    //...
    ch.configureBlocking(false);
    //...
}
複製程式碼

這裡,簡單地將前面 provider.openServerSocketChannel(); 建立出來的 ServerSocketChannel 儲存到成員變數,然後呼叫ch.configureBlocking(false);設定該channel為非阻塞模式,標準的jdk nio程式設計的玩法

這裡的 readInterestOp 即前面層層傳入的 SelectionKey.OP_ACCEPT,接下來重點分析 super(parent);(這裡的parent其實是null,由前面寫死傳入)

AbstractChannel.java

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}
複製程式碼

到了這裡,又new出來三大元件,賦值到成員變數,分別為

id = newId();
protected ChannelId newId() {
    return DefaultChannelId.newInstance();
}
複製程式碼

id是netty中每條channel的唯一標識,這裡不細展開,接著

unsafe = newUnsafe();
protected abstract AbstractUnsafe newUnsafe();
複製程式碼

檢視Unsafe的定義

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

成功捕捉netty的又一大元件,我們可以先不用管TA是幹嘛的,只需要知道這裡的 newUnsafe方法最終屬於類NioServerSocketChannel

最後

pipeline = newChannelPipeline();

protected DefaultChannelPipeline newChannelPipeline() {
    return new DefaultChannelPipeline(this);
}

protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
}

複製程式碼

初次看這段程式碼,可能並不知道 DefaultChannelPipeline 是幹嘛用的,我們仍然使用上面的方式,檢視頂層介面ChannelPipeline的定義

A list of ChannelHandlers which handles or intercepts inbound events and outbound operations of a Channel

從該類的文件中可以看出,該介面基本上又是netty的一大核心模組

到了這裡,我們總算把一個服務端channel建立完畢了,將這些細節串起來的時候,我們順帶提取出netty的幾大基本元件,先總結如下

  • Channel
  • ChannelConfig
  • ChannelId
  • Unsafe
  • Pipeline
  • ChannelHander

初次看程式碼的時候,我們的目標是跟到伺服器啟動的那一行程式碼,我們先把以上這幾個元件記下來,等程式碼跟完,我們就可以自頂向下,逐層分析,我會放到後面原始碼系列中去深入到每個元件

總結一下,使用者呼叫方法 Bootstrap.bind(port) 第一步就是通過反射的方式new一個NioServerSocketChannel物件,並且在new的過程中建立了一系列的核心元件,僅此而已,並無他,真正的啟動我們還需要繼續跟

2.init這個channel

到了這裡,你最好跳到文章最開始的地方回憶一下,第一步newChannel完畢,這裡就對這個channel做init,init方法具體幹啥,我們深入

@Override
void init(Channel channel) throws Exception {
    final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        channel.config().setOptions(options);
    }

    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            @SuppressWarnings("unchecked")
            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
            channel.attr(key).set(e.getValue());
        }
    }

    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
    synchronized (childOptions) {
        currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
    }
    synchronized (childAttrs) {
        currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
    }

    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
        }
    });
}

複製程式碼

初次看到這個方法,可能會覺得,哇塞,老長了,這可這麼看?還記得我們前面所說的嗎,庖丁解牛,逐步拆解,最後歸一,下面是我的拆解步驟

1.設定option和attr

final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        channel.config().setOptions(options);
    }

    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            @SuppressWarnings("unchecked")
            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
            channel.attr(key).set(e.getValue());
        }
    }
複製程式碼

通過這裡我們可以看到,這裡先呼叫options0()以及attrs0(),然後將得到的options和attrs注入到channelConfig或者channel中,關於option和attr是幹嘛用的,其實你現在不用瞭解得那麼深入,只需要檢視最頂層介面ChannelOption以及檢視一下channel的具體繼承關係,就可以瞭解,我把這兩個也放到後面的原始碼分析系列再講

2.設定新接入channel的option和attr

final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
    currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
    currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}
複製程式碼

這裡,和上面類似,只不過不是設定當前channel的這兩個屬性,而是對應到新進來連線對應的channel,由於我們這篇文章只關心到server如何啟動,接入連線放到下一篇文章中詳細剖析

3.加入新連線處理器

p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
        }
    });
複製程式碼

到了最後一步,p.addLast()向serverChannel的流水線處理器中加入了一個 ServerBootstrapAcceptor,從名字上就可以看出來,這是一個接入器,專門接受新請求,把新的請求扔給某個事件迴圈器,我們先不做過多分析

來,我們總結一下,我們發現其實init也沒有啟動服務,只是初始化了一些基本的配置和屬性,以及在pipeline上加入了一個接入器,用來專門接受新連線,我們還得繼續往下跟

3.將這個channel register到某個物件

這一步,我們是分析如下方法

ChannelFuture regFuture = config().group().register(channel);
複製程式碼

呼叫到 NioEventLoop 中的register

@Override
public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}
複製程式碼
@Override
public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    promise.channel().unsafe().register(this, promise);
    return promise;
}
複製程式碼

好了,到了這一步,還記得這裡的unsafe()返回的應該是什麼物件嗎?不記得的話可以看下前面關於unsafe的描述,或者最快的方式就是debug到這邊,跟到register方法裡面,看看是哪種型別的unsafe

我們跟進去之後發現是

AbstractUnsafe.java

@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // ...
    AbstractChannel.this.eventLoop = eventLoop;
    // ...
    register0(promise);
}
複製程式碼

這裡我們依然只需要focus重點,先將EventLoop事件迴圈器繫結到該NioServerSocketChannel上,然後呼叫 register0()

private void register0(ChannelPromise promise) {
    try {
        boolean firstRegistration = neverRegistered;
        doRegister();
        neverRegistered = false;
        registered = true;

        pipeline.invokeHandlerAddedIfNeeded();

        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                beginRead();
            }
        }
    } catch (Throwable t) {
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}
複製程式碼

這一段其實也很清晰,先呼叫 doRegister();,具體幹啥待會再講,然後呼叫invokeHandlerAddedIfNeeded(), 於是乎,控制檯第一行列印出來的就是

handlerAdded
複製程式碼

關於最終是如何呼叫到的,我們後面詳細剖析pipeline的時候再講

然後呼叫 pipeline.fireChannelRegistered(); 呼叫之後,控制檯的顯示為

handlerAdded
channelRegistered
複製程式碼

繼續往下跟

if (isActive()) {
    if (firstRegistration) {
        pipeline.fireChannelActive();
    } else if (config().isAutoRead()) {
        beginRead();
    }
}
複製程式碼

讀到這,你可能會想當然地以為,控制檯最後一行

pipeline.fireChannelActive();
複製程式碼

由這行程式碼輸出,我們不妨先看一下 isActive() 方法

@Override
public boolean isActive() {
    return javaChannel().socket().isBound();
}
複製程式碼

最終呼叫到jdk中

ServerSocket.java

    /**
     * Returns the binding state of the ServerSocket.
     *
     * @return true if the ServerSocket succesfuly bound to an address
     * @since 1.4
     */
    public boolean isBound() {
        // Before 1.3 ServerSockets were always bound during creation
        return bound || oldImpl;
    }
複製程式碼

這裡isBound()返回false,但是從目前我們跟下來的流程看,我們並沒有將一個ServerSocket繫結到一個address,所以 isActive() 返回false,我們沒有成功進入到pipeline.fireChannelActive();方法,那麼最後一行到底是誰輸出的呢,我們有點抓狂,其實,只要熟練運用IDE,要定位函式呼叫棧,無比簡單

下面是我用intellij定位函式呼叫的具體方法

Intellij函式呼叫定位

我們先在最終輸出文字的這一行程式碼處打一個斷點,然後debug,執行到這一行,intellij自動給我們拉起了呼叫棧,我們唯一要做的事,就是移動方向鍵,就能看到函式的完整的呼叫鏈

如果你看到方法的最近的發起端是一個執行緒Runnable的run方法,那麼就在提交Runnable物件方法的地方打一個斷點,去掉其他斷點,重新debug,比如我們首次debug發現呼叫棧中的最近的一個Runnable如下

if (!wasActive && isActive()) {
    invokeLater(new Runnable() {
        @Override
        public void run() {
            pipeline.fireChannelActive();
        }
    });
}
複製程式碼

我們停在了這一行pipeline.fireChannelActive();, 我們想看最初始的呼叫,就得跳出來,斷點打到 if (!wasActive && isActive()),因為netty裡面很多工執行都是非同步執行緒即reactor執行緒呼叫的(具體可以看reactor執行緒三部曲中的最後一曲),如果我們要檢視最先發起的方法呼叫,我們必須得檢視Runnable被提交的地方,逐次遞迴下去,就能找到那行"消失的程式碼"

最終,通過這種方式,終於找到了 pipeline.fireChannelActive(); 的發起呼叫的程式碼,不巧,剛好就是下面的doBind0()方法

doBind0()

private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }
複製程式碼

我們發現,在呼叫doBind0(...)方法的時候,是通過包裝一個Runnable進行非同步化的,關於非同步化task,可以看下我前面的文章,netty原始碼分析之揭開reactor執行緒的面紗(三)

好,接下來我們進入到channel.bind()方法

AbstractChannel.java

@Override
public ChannelFuture bind(SocketAddress localAddress) {
    return pipeline.bind(localAddress);
}
複製程式碼

發現是呼叫pipeline的bind方法

@Override
public final ChannelFuture bind(SocketAddress localAddress) {
    return tail.bind(localAddress);
}
複製程式碼

相信你對tail是什麼不是很瞭解,可以翻到最開始,tail在建立pipeline的時候出現過,關於pipeline和tail對應的類,我後面原始碼系列會詳細解說,這裡,你要想知道接下來程式碼的走向,唯一一個比較好的方式就是debug 單步進入,篇幅原因,我就不詳細展開

最後,我們來到了如下區域

HeadContext.java

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

這裡的unsafe就是前面提到的 AbstractUnsafe, 準確點,應該是 NioMessageUnsafe

我們進入到它的bind方法

@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    // ...
    boolean wasActive = isActive();
    // ...
    doBind(localAddress);

    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }
    safeSetSuccess(promise);
}
複製程式碼

顯然按照正常流程,我們前面已經分析到 isActive(); 方法返回false,進入到 doBind()之後,如果channel被啟用了,就發起pipeline.fireChannelActive();呼叫,最終呼叫到使用者方法,在控制檯列印出了最後一行,所以到了這裡,你應該清楚為什麼最終會在控制檯按順序列印出那三行字了吧

doBind()方法也很簡單

protected void doBind(SocketAddress localAddress) throws Exception {
    if (PlatformDependent.javaVersion() >= 7) {
        //noinspection Since15
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}
複製程式碼

最終調到了jdk裡面的bind方法,這行程式碼過後,正常情況下,就真正進行了埠的繫結。

另外,通過自頂向下的方式分析,在呼叫pipeline.fireChannelActive();方法的時候,會呼叫到如下方法

HeadContext.java

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

    readIfIsAutoRead();
}
複製程式碼

進入 readIfIsAutoRead

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

分析isAutoRead方法

private volatile int autoRead = 1;
public boolean isAutoRead() {
    return autoRead == 1;
}
複製程式碼

由此可見,isAutoRead方法預設返回true,於是進入到以下方法

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

最終呼叫到

AbstractNioUnsafe.java

protected void doBeginRead() throws Exception {
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

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

這裡的this.selectionKey就是我們在前面register步驟返回的物件,前面我們在register的時候,註冊測ops是0

回憶一下注冊

AbstractNioChannel

selectionKey = javaChannel().register(eventLoop().selector, 0, this)
複製程式碼

這裡相當於把註冊過的ops取出來,通過了if條件,然後呼叫

selectionKey.interestOps(interestOps | readInterestOp);
複製程式碼

而這裡的 readInterestOp 就是前面newChannel的時候傳入的SelectionKey.OP_ACCEPT,又是標準的jdk nio的玩法,到此,你需要了解的細節基本已經差不多了,就這樣結束吧!

summary

最後,我們來做下總結,netty啟動一個服務所經過的流程 1.設定啟動類引數,最重要的就是設定channel 2.建立server對應的channel,建立各大元件,包括ChannelConfig,ChannelId,ChannelPipeline,ChannelHandler,Unsafe等 3.初始化server對應的channel,設定一些attr,option,以及設定子channel的attr,option,給server的channel新增新channel接入器,並出發addHandler,register等事件 4.呼叫到jdk底層做埠繫結,並觸發active事件,active觸發的時候,真正做服務埠繫結

另外,文章中閱讀原始碼的思路詳細或許也可以給你帶來一些幫助。

如果你想系統地學Netty,我的小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》可以幫助你,如果你想系統學習Netty原理,那麼你一定不要錯過我的Netty原始碼分析系列視訊:coding.imooc.com/class/230.h…

相關文章