Netty之揭开BootStrap 的神秘面纱

客户端BootStrap:

  Bootstrap 是Netty 提供的一个便利的工厂类, 我们可以通过它来完成Netty 的客户端或服务器端的Netty 初始化。下面我先来看一个例子, 从客户端和服务器端分别分析一下Netty 的程序是如何启动的。首先,让我们从客户端的代码片段开始:

EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                            pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast("handler", new MyClient());
                        }
                    });
                ChannelFuture f = bootstrap.connect("127.0.0.1", 6666).sync();
                f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
}

1.EventLoop 的初始化:

  最开始的 Client 用户代码中,我们在一开始就实例化了一个NioEventLoopGroup 对象,因此我们就从它的构造器中追踪一下EventLoop 的初始化过程。首先来看一下NioEventLoopGroup 的类继承层次:

  NioEventLoop 有几个重载的构造器,不过内容都没有太大的区别,从new NioEventLoopGroup()开始,其中确定了线程数,selectorProvider ,选择器策略工厂DefaultSelectStrategyFactory.INSTANCE 最终都是调用的父类MultithreadEventLoopGroup的构造器:

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }

  其中有个有意思的地方是,如果我们传入的线程数nThreads 是0,那么Netty 会为我们设置默认的线程数DEFAULT_EVENT_LOOP_THREADS,而这个默认的线程数是怎么确定的呢?其实很简单,在静态代码块中,会首先确定DEFAULT_EVENT_LOOP_THREADS 的值:

static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
}

  Netty 首先会从系统属性中获取"io.netty.eventLoopThreads"的值,如果我们没有设置的话,那么就返回默认值:即处理器核心数* 2。回到MultithreadEventLoopGroup 构造器中会继续调用父类MultithreadEventExecutorGroup 的构造器:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
        this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
    }

  其中 DefaultEventExecutorChooserFactory 就是事件执行器选择策略的创建工厂,继续调用重载方法:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
     。。。。。。
        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            。。。。。。
          children[i] = newChild(executor, args);
     } 。。。。。。
chooser = chooserFactory.newChooser(children);      //去掉以下代码 }

  这里其实就是创建了一个大小为 nThreads 的事件执行器数组,第二步是初始化该数组,紧接着通过选择策略工厂 EventExecutorChooserFactory 创建选择策略,首先看一下他的初始化,调用newChhild()方法初始化children 数组。根据上面的代码,我们也能知道:MultithreadEventExecutorGroup 内部维护了一个EventExecutor 数组,而Netty 的EventLoopGroup 的实现机制其实就建立在MultithreadEventExecutorGroup 之上。每当Netty 需要一个EventLoop 时,会调用next()方法获取一个可用的EventLoop。newChild()方法是一个抽象方法,它的任务是实例化EventLoop 对象。我们跟踪一下它的代码。可以发现。这个方法在NioEventLoopGroup 类中有实现,其内容很简单:

protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
                ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
    }

  在此构造方法中调用其父类 SingleThreadEventLoop 的构造方法,再调用 SingleThreadEventExecutor的构造方法。数组初始化结束,紧接着是初始化选择器,我们可以继续跟踪到newChooser 方法里面看看其实现逻辑,具体代码如下:

public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {

    public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();

    private DefaultEventExecutorChooserFactory() { }

    @SuppressWarnings("unchecked")
    @Override
    public EventExecutorChooser newChooser(EventExecutor[] executors) {
//判断是否二次幂
if (isPowerOfTwo(executors.length)) { return new PowerOfTwoEventExecutorChooser(executors); } else { return new GenericEventExecutorChooser(executors); } } private static boolean isPowerOfTwo(int val) { return (val & -val) == val; } private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; PowerOfTwoEventExecutorChooser(EventExecutor[] executors) { this.executors = executors; } @Override public EventExecutor next() {
// 索引自增 & 执行器数组长度-1
return executors[idx.getAndIncrement() & executors.length - 1]; } } private static final class GenericEventExecutorChooser implements EventExecutorChooser { private final AtomicInteger idx = new AtomicInteger(); private final EventExecutor[] executors; GenericEventExecutorChooser(EventExecutor[] executors) { this.executors = executors; } @Override public EventExecutor next() {
// 索引自增 % 执行器数组长度 后取绝对值
return executors[Math.abs(idx.getAndIncrement() % executors.length)]; } } }

  上面的代码逻辑主要表达的意思是:即如果nThreads 是2 的幂,则使用PowerOfTwoEventExecutorChooser,否则使用GenericEventExecutorChooser。这两个Chooser 都重写next()方法。next()方法的主要功能就是将数组索引循环位移,如下图所示:

  当索引移动最后一个位置时,再调用next()方法就会将索引位置重新指向0。

  这个运算逻辑其实很简单,就是每次让索引自增后和数组长度取模:idx.getAndIncrement() % executors.length。但是就连一个非常简单的数组索引运算,Netty 都帮我们做了优化。因为在计算机底层,&与比%运算效率更高。好了,分析到这里我们应该已经非常清楚MultithreadEventExecutorGroup 中的处理逻辑,最后总结一下整个EventLoopGroup 的初始化过程:

 

  1. EventLoopGroup(其实是MultithreadEventExecutorGroup)内部维护一个类型为EventExecutor children 数组,是大小为nThreads的SingleThreadEventExecutor,这样就构成了一个线程池。
  2. 如果我们在实例化NioEventLoopGroup 时,如果指定线程池大小,则nThreads 就是指定的值,反之是处理器核心数* 2。
  3. MultithreadEventExecutorGroup 中会调用newChild()抽象方法来初始化children 数组。
  4. 抽象方法newChild()是在NioEventLoopGroup 中实现的,它返回一个NioEventLoop 实例。
  5. NioEventLoop 属性赋值:
    1. provider:在NioEventLoopGroup 构造器中通过SelectorProvider.provider()获取一个SelectorProvider。
    2. selector:在NioEventLoop 构造器中通过调用通过provider.openSelector()方法获取一个selector 对象。
  6. 根据nThreads 的大小,创建不同的Chooser,即如果nThreads 是2 的幂,则使用PowerOfTwoEventExecutorChooser,反之使用GenericEventExecutorChooser。不论使用哪个Chooser,它们的功能都是一样的,即从children 数组中选出一个合适的EventExecutor 实例。

2.Channel 简介:

  在Netty 中,Channel 是一个Socket 的抽象,它为用户提供了关于Socket 状态(是否是连接还是断开)以及对Socket的读写等操作。每当Netty 建立了一个连接后, 都创建一个对应的Channel 实例。除了TCP 协议以外,Netty 还支持很多其他的连接协议, 并且每种协议还有NIO(非阻塞IO)和OIO(Old-IO, 即传统的阻塞IO)版本的区别。不同协议不同的阻塞类型的连接都有不同的Channel 类型与之对应下面是一些常用的Channel类型:

  • NioSocketChannel :异步非阻塞的客户端TCP Socket 连接。
  • NioServerSocketChannel :异步非阻塞的服务器端TCP Socket 连接
  • NioDatagramChannel :异步非阻塞的UDP 连接。
  • NioSctpChannel :异步的客户端Sctp(Stream Control Transmission Protocol,流控制传输协议)连接。
  • NioSctpServerChannel :异步的Sctp 服务器端连接。
  • OioSocketChannel :同步阻塞的客户端TCP Socket 连接。
  • OioServerSocketChannel :同步阻塞的服务器端TCP Socket 连接。
  • OioDatagramChannel :同步阻塞的UDP 连接。
  • OioSctpChannel :同步的Sctp 服务器端连接。
  • OioSctpServerChannel: 同步的客户端TCP Socket 连接。

  下面我们来看一下Channel 的总体类图:

NioSocketChannel 的创建:

  从上面的客户端代码虽然简单, 但是却展示了Netty 客户端初始化时所需的所有内容:

  1. EventLoopGroup:不论是服务器端还是客户端, 都必须指定EventLoopGroup。在这个例子中, 指定了NioEventLoopGroup, 表示一个NIO 的EventLoopGroup。
  2. ChannelType: 指定Channel 的类型。因为是客户端,因此使用了NioSocketChannel。
  3. Handler: 设置处理数据的Handler。

  下面我们继续深入代码,看一下客户端通过Bootstrap 启动后,都做了哪些工作?我们看一下NioSocketChannel 的类层次结构如下:

  回到我们在客户端连接代码的初始化Bootstrap 中调用了一个channel()方法,传入的参数是NioSocketChannel.class,在这个方法中其实就是初始化了一个ReflectiveChannelFactory 的对象:

public B channel(Class<? extends C> channelClass) {
        return channelFactory(new ReflectiveChannelFactory<C>(
                ObjectUtil.checkNotNull(channelClass, "channelClass")
        ));
    }

  而ReflectiveChannelFactory 实现了ChannelFactory 接口, 它提供了唯一的方法, 即newChannel()方法,ChannelFactory, 顾名思义, 就是创建Channel 的工厂类。进入到ReflectiveChannelFactory 的newChannel()方法中,我们看到其实现代码如下:

public T newChannel() {
  return constructor.newInstance();
}

  根据上面代码的提示,我们就可以得出:

  1. Bootstrap 中的ChannelFactory 实现类是ReflectiveChannelFactory。
  2. 通过channel()方法创建的Channel 具体类型是NioSocketChannel。

  Channel 的实例化过程其实就是调用ChannelFactory 的newChannel()方法,而实例化的Channel 具体类型又是和初始化Bootstrap 时传入的channel()方法的参数相关。因此对于客户端的Bootstrap 而言,创建的Channel 实例就是NioSocketChannel。

 

客户端Channel 的初始化:

  前面我们已经知道了如何设置一个Channel 的类型,并且了解到Channel 是通过ChannelFactory 的newChannel()方法来实例化的, 那么ChannelFactory 的newChannel()方法在哪里调用呢?继续跟踪, 我们发现其调用链如下:

  在AbstractBootstrap 的initAndRegister()中调用了ChannelFactory()的newChannel()来创建一个NioSocketChannel的实例,其源码如下:

final ChannelFuture initAndRegister() {
        channel = channelFactory.newChannel();
        init(channel);
        ChannelFuture regFuture = config().group().register(channel);
}

  在newChannel()方法中,利用反射机制调用类对象的newInstance()方法来创建一个新的Channel 实例,相当于调用NioSocketChannel 的默认构造器。NioSocketChannel 的默认构造器代码如下:

public NioSocketChannel() {
        this(DEFAULT_SELECTOR_PROVIDER);
}

  这里的代码比较关键,我们看到,在这个构造器中会调用newSocket()来打开一个新的Java NIO 的SocketChannel:

private static SocketChannel newSocket(SelectorProvider provider) {
       
  return provider.openSocketChannel(); }

  接着会调用父类, 即AbstractNioByteChannel 的构造器,并传入参数parent 为null, ch 为刚才调用newSocket()创建的Java NIO 的SocketChannel 对象, 因此新创建的NioSocketChannel 对象中parent 暂时是null。并且这里会传入一个感兴趣事件是读事件:

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
        super(parent, ch, SelectionKey.OP_READ);
}

  然后继续调用父类AbstractChannel 的构造器:

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

  至此, NioSocketChannel 就初始化就完成了, 我们可以稍微总结一下NioSocketChannel 初始化所做的工作内容:

  1. 调用NioSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)打开一个新的Java NIOSocketChannel。
  2. AbstractChannel(Channel parent)中需要初始化的属性:
    1. id:每个Channel 都拥有一个唯一的id。
    2. parent:属性置为null。
    3. unsafe:通过newUnsafe()实例化一个unsafe 对象,它的类型是AbstractNioByteChannel.NioByteUnsafe 内部类。
    4. pipeline:是new DefaultChannelPipeline(this)新创建的实例。
  3. AbstractNioChannel 中的属性:
    1. ch:赋值为Java SocketChannel,即NioSocketChannel 的newSocket()方法返回的Java NIO SocketChannel。
    2. readInterestOp:赋值为SelectionKey.OP_READ
    3. ch:被配置为非阻塞,即调用ch.configureBlocking(false)。
  4. NioSocketChannel 中的属性:
    1. config = new NioSocketChannelConfig(this, socket.socket())

Unsafe 字段的初始化:

  在实例化NioSocketChannel 的过程中,会在父类AbstractChannel 的构造方法中调用newUnsafe()来获取一个unsafe 实例。那么unsafe 是怎么初始化的呢? 它的作用是什么?其实unsafe 特别关键,它封装了对Java 底层Socket 的操作,因此实际上是沟通Netty 上层和Java 底层的重要的桥梁。那么我们下面看一下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 deregister(ChannelPromise promise);
        void beginRead();
        void write(Object msg, ChannelPromise promise);
        void flush();
        ChannelPromise voidPromise();
        ChannelOutboundBuffer outboundBuffer();
    }

  从源码中可以看出, 这些方法其实都是对应到相关的Java 底层的Socket 的操作。继续回到AbstractChannel 的构造方法中,在这里调用了newUnsafe()获取一个新的unsafe 对象,而newUnsafe()方法在NioSocketChannel 中被重写了。来看代码:

protected AbstractNioUnsafe newUnsafe() {
        return new NioSocketChannelUnsafe();
    }

  NioSocketChannel 的newUnsafe()方法会返回一个NioSocketChannelUnsafe 实例。从这里我们就可以确定了,在实例化的NioSocketChannel 中的unsafe 字段,其实是一个NioSocketChannelUnsafe 的实例。

Pipeline 的初始化:

  上面我们分析了NioSocketChannel 的大体初始化过程, 但是我们漏掉了一个关键的部分,即ChannelPipeline 的初始化。在Pipeline 的注释说明中写到“Each channel has its own pipeline and it is created automatically when a newchannel is created.”,我们知道,在实例化一个Channel 时,必然都要实例化一个ChannelPipeline。而我们确实在AbstractChannel 的构造器看到了pipeline 字段被初始化为DefaultChannelPipeline 的实例。那么我们就来看一下,:DefaultChannelPipeline 构造器做了哪些工作。

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 的构造器需要传入一个channel,而这个channel 其实就是我们实例化的NioSocketChannel,DefaultChannelPipeline 会将这个NioSocketChannel 对象保存在channel 字段中。DefaultChannelPipeline 中还有两个特殊的字段,即head 和tail,这两个字段是双向链表的头和尾。其实在DefaultChannelPipeline 中,维护了一个以AbstractChannelHandlerContext 为节点元素的双向链表,这个链表是Netty 实现Pipeline 机制的关键。关于DefaultChannelPipeline 中的双向链表以及它所起的作用,我们暂不深入,后续再做深入分析。先看看HeadContext跟TailContext的类继承层次结构如下所示:

  再来看看二者的构造器:

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

  我们可以看到,链表中head 实现了ChannelOutboundHandler跟ChannelInboundHandler,它调用了父类AbstractChannelHandlerContext 的构造器,并传入参数inbound = false,outbound = true。而而tail 则是一个ChannelInboundHandler。TailContext 的构造器与HeadContext 的相反,它调用了父类AbstractChannelHandlerContext 的构造器,并传入参数inbound = true,outbound = false。即header 是一个OutBoundHandler,而tail 是一个InBoundHandler。

Channel 注册到Selector:

  在前面的分析中,我们提到Channel 会在Bootstrap 的initAndRegister()中进行初始化,但是这个方法还会将初始化好的Channe 注册到NioEventLoop 的selector 中。接下来我们来分析一下Channel 注册的过程。再回顾一下AbstractBootstrap 的initAndRegister()方法:

final ChannelFuture initAndRegister() {
        channel = channelFactory.newChannel();
        init(channel);
        ChannelFuture regFuture = config().group().register(channel);
}

  当Channel 初始化后,紧接着会调用group().register()方法来向selector 注册Channel。我们继续跟踪的话,会发现其调用链如下:

  通过跟踪调用链, 最终我们发现是调用到了unsafe 的register 方法,那么接下来我们就仔细看一下AbstractChannel$AbstractUnsafe.register()方法中到底做了什么?

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    AbstractChannel.this.eventLoop = eventLoop;
    register0(promise);
}

  首先,将eventLoop 赋值给Channel 的eventLoop 属性,而我们知道这个eventLoop 对象其实是MultithreadEventLoopGroup 的next()方法获取的,根据我们前面的分析,我们可以确定next()方法返回的eventLoop对象是NioEventLoop 实例。register()方法接着调用了register0()方法,继而调用 doRegister() 方法:

protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {//javaChannel()是channel初始化的时候设置的值
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
        }
}

  看到javaChannel()这个方法在前面我们已经知道了,它返回的是一个Java NIO 的SocketChannel 对象,这里我们将这个SocketChannel 注册到与eventLoop 关联的selector 上了。我们总结一下Channel 的注册过程:

  1. 首先在AbstractBootstrap 的initAndRegister()方法中, 通过group().register(channel),调用MultithreadEventLoopGroup 的register()方法。
  2. 在MultithreadEventLoopGroup 的register()中,调用next()方法获取一个可用的SingleThreadEventLoop, 然后调用它的register()方法。
  3. 在SingleThreadEventLoop 的register()方法中,调用channel.unsafe().register(this, promise)方法来获取channel 的unsafe()底层操作对象,然后调用unsafe 的register()。
  4. 在AbstractUnsafe 的register()方法中, 调用register0()方法注册Channel 对象。
  5. 在AbstractUnsafe 的register0()方法中,调用AbstractNioChannel 的doRegister()方法。
  6. AbstractNioChannel 的doRegister()方法通过javaChannel().register(eventLoop().selector, 0, this)将Channel对应的Java NIO 的SocketChannel 注册到一个eventLoop 的selector 中,并且将当前Channel 作为attachment 与SocketChannel 关联。

  总的来说,Channel 注册过程所做的工作就是将Channel 与对应的EventLoop 关联,因此这也体现了,在Netty中,每个Channel 都会关联一个特定的EventLoop,并且这个Channel 中的所有IO 操作都是在这个EventLoop 中执行的;当关联好Channel 和EventLoop 后,会继续调用底层Java NIO 的SocketChannel 对象的register()方法,将底层Java NIO 的SocketChannel 注册到指定的selector 中。通过这两步,就完成了Netty 对Channel 的注册过程。

Handler 的添加过程:

  Netty 有一个强大和灵活之处就是基于Pipeline 的自定义handler 机制。基于此,我们可以像添加插件一样自由组合各种各样的handler 来完成业务逻辑。例如我们需要处理HTTP 数据,那么就可以在pipeline 前添加一个针对HTTP编、解码的Handler,然后接着添加我们自己的业务逻辑的handler,这样网络上的数据流就向通过一个管道一样, 从不同的handler 中流过并进行编、解码,最终在到达我们自定义的handler 中。说到这里,有些小伙伴肯定会好奇,既然这个pipeline 机制是这么的强大,那么它是怎么实现的呢? 我们从简单的入手,先体验一下自定义的handler 是如何以及何时添加到ChannelPipeline中的。后续我回深入分析首先我们看一下如下的用户代码片段:

.handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                            pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast("handler", new MyClient());
                        }
                    })

  这个代码片段就是实现了handler 的添加功能。我们看到,Bootstrap 的handler()方法接收一个ChannelHandler,而我们传的参数是一个派生于抽象类ChannelInitializer 的匿名类,它当然也实现了ChannelHandler 接口。我们来看一下,ChannelInitializer 类内到底有什么玄机:

public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {
  protected abstract void initChannel(C ch) throws Exception;
    @Override
    @SuppressWarnings("unchecked")
    public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        if (initChannel(ctx)) {
            ctx.pipeline().fireChannelRegistered();
            removeState(ctx);
        } else {
            ctx.fireChannelRegistered();
        }
    }private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
        if (initMap.add(ctx)) { // Guard against re-entrance.
            try {
                initChannel((C) ctx.channel());
            } catch (Throwable cause) {
                exceptionCaught(ctx, cause);
            } finally {
                ChannelPipeline pipeline = ctx.pipeline();
                if (pipeline.context(this) != null) {
                    pipeline.remove(this);
                }
            }
            return true;
        }
        return false;
    }
}

  ChannelInitializer 是一个抽象类,它有一个抽象的方法initChannel(),我们看到的匿名类正是实现了这个方法,并在这个方法中添加的自定义的handler 的。那么initChannel()是哪里被调用的呢?其实是在ChannelInitializer 的channelRegistered()方法中。接下来关注一下channelRegistered()方法。从上面的源码中,我们可以看到,在channelRegistered()方法中,会调用this.init(channel);方法,将自定义的handler 添加到ChannelPipeline 中,然后调用ctx.pipeline().remove(this)方法将自己从ChannelPipeline 中删除。上面的分析过程,如下图片所示:一开始,ChannelPipeline 中只有三个handler,分别是:head、tail 和我们添加的ChannelInitializer。

  接着initChannel()方法调用后,添加了自定义的handler:

  最后将ChannelInitializer 删除:

  分析到这里,我们已经简单了解了自定义的handler 是如何添加到ChannelPipeline 中的,后续我们再进行深入的探讨。

客户端发起连接请求:

  经过上面的各种分析后,我们大致已经了解Netty 客户端初始化时,所做的工作,那么接下来我们就直奔主题,分析一下客户端是如何发起TCP 连接的?首先,客户端通过调用Bootstrap 的connect()方法进行连接。在connect()方法中,会进行一些参数检查后,最终调用的是doConnect()方法,其代码实现如下:

private static void doConnect(
            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
        final Channel channel = connectPromise.channel();
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (localAddress == null) {
                    channel.connect(remoteAddress, connectPromise);
                } else {
                    channel.connect(remoteAddress, localAddress, connectPromise);
                }
                connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            }
        });
    }

   在doConnect()方法中,eventLoop 线程中调用Channel 的connect()方法,而这个Channel 的具体类型实际就是NioSocketChannel,前面已经分析过了。继续跟踪到channel.connect()方法中,我们发现它调用的是DefaultChannelPipeline 的connect()方法,pipeline 的connect()方法代码如下:

public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }

  tail 我们已经分析过, 是一个TailContext 的实例,而TailContext 又是AbstractChannelHandlerContext 的子类,并且没有实现connect()方法,因此这里调用的其实是AbstractChannelHandlerContext 的connect()方法,我们看一下这个方法的实现:

public ChannelFuture connect(
            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
     。。。。。
        final AbstractChannelHandlerContext next = findContextOutbound(MASK_CONNECT);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeConnect(remoteAddress, localAddress, promise);
        } else {
            safeExecute(executor, new Runnable() {
                @Override
                public void run() {
                    next.invokeConnect(remoteAddress, localAddress, promise);
                }
            }, promise, null);
        }
        return promise;
    }

  上面的代码中有一个关键的地方,即final AbstractChannelHandlerContext next = findContextOutbound(),这里调用findContextOutbound()方法,从DefaultChannelPipeline 内的双向链表的tail 开始,不断向前找到第一个outbound为true 的AbstractChannelHandlerContext,然后调用它的invokeConnect()方法,其代码如下:

private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        if (invokeHandler()) {
            try {
                ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
            } catch (Throwable t) {
                notifyOutboundHandlerException(t, promise);
            }
        } else {
            connect(remoteAddress, localAddress, promise);
        }
    }

  前面我们有提到,在DefaultChannelPipeline 的构造器中,实例化了两个对象:head 和tail,并形成了双向链表的头和尾。head 是HeadContext 的实例,它实现了ChannelOutboundHandler 接口,并且它的outbound 设置为true。因此在findContextOutbound()方法中,找到的AbstractChannelHandlerContext 对象其实就是head。进而在invokeConnect()方法中, 我们向上转换为ChannelOutboundHandler 就是没问题的了。而又因为HeadContext 重写了connect()方法,因此实际上调用的是HeadContext 的connect()方法。我们接着跟踪到HeadContext 的connect()方法,看其代码如下:

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

  这个connect()方法很简单,只是调用了unsafe 的connect()方法。回顾一下HeadContext 的构造器, 我们发现这个unsafe 其实就是pipeline.channel().unsafe()返回的Channel 的unsafe 字段。到这里为止,我们应该已经知道, 其实是AbstractNioByteChannel.NioByteUnsafe 内部类兜了一大圈。最后,我们找到创建Socket 连接的关键代码继续跟踪,其实调用的就是AbstractNioUnsafe 的connect()方法:

public final void connect(
                final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {if (connectPromise != null) {
                    // Already a connect in process.
                    throw new ConnectionPendingException();
                }
                boolean wasActive = isActive();
                if (doConnect(remoteAddress, localAddress)) {
                    fulfillConnectPromise(promise, wasActive);
                } else {
                   。。。
                }
}

  在这个connect()方法中,又调用了doConnect()方法。注意:这个方法并不是AbstractNioUnsafe 的方法,而是AbstractNioChannel 的抽象方法。doConnect()方法是在NioSocketChannel 中实现的,因此进入到NioSocketChannel的doConnect()方法中:

protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
}

  我们终于看到的最关键的部分了,首先是获取Java NIO 的SocketChannel,获取NioSocketChannel 的newSocket()返回的SocketChannel 对象;然后调用SocketChannel 的connect()方法完成JavaNIO 底层的Socket 的连接。最后总结一下,客户端BootStrap 发起连接请求的流程可以用如下时序图直观地展示:

服务端ServerBootStrap:

  在分析客户端的代码时,我们已经对Bootstrap 启动Netty 有了一个大致的认识,那么接下来分析服务器端时,就会相对简单一些了。首先还是来看一下服务器端的启动代码:

EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
          
        try {  
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
  
                        @Override  
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //自定义协议解码器
                            /** 入参有5个,分别解释如下
                             (1) maxFrameLength - 发送的数据包最大长度;
                             (2) lengthFieldOffset - 长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
                             (3) lengthFieldLength - 长度域的自己的字节数长度。
                             (4) lengthAdjustment – 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,
                                    还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
                             (5) initialBytesToStrip – 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。
                             */
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
                            //自定义协议编码器
                            pipeline.addLast(new LengthFieldPrepender(4));
                            //对象参数类型编码器
                            pipeline.addLast("encoder",new ObjectEncoder());
                            //对象参数类型解码器
                            pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
                            pipeline.addLast(new RegistryHandler());
                        }  
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            ChannelFuture future = b.bind(port).sync();
            System.out.println("Wuzz RPC Registry start listen at " + port );
            future.channel().closeFuture().sync();    
        } catch (Exception e) {  
             bossGroup.shutdownGracefully();    
             workerGroup.shutdownGracefully();  
        }  

  服务端基本写法和客户端的代码相比,没有很大的差别,基本上也是进行了如下几个部分的初始化:

  1. EventLoopGroup:不论是服务器端还是客户端,都必须指定EventLoopGroup。在上面的代码中,指定了NioEventLoopGroup,表示一个NIO 的EventLoopGroup,不过服务器端需要指定两个EventLoopGroup,一个是bossGroup,用于处理客户端的连接请求;另一个是workerGroup,用于处理与各个客户端连接的IO 操作。
  2. ChannelType: 指定Channel 的类型。因为是服务器端,因此使用了NioServerSocketChannel。
  3. Handler:设置数据处理器。

bossGroup 与workerGroup:

  在客户端的时候,我们初始化了一个EventLoopGroup 对象,而在服务端的初始化时,我们设置了两个EventLoopGroup,一个是bossGroup,另一个是workerGroup。那么这两个EventLoopGroup 都是干什么用的呢? 接下来我们详细探究一下。其实,bossGroup 只用于服务端的accept,也就是用于处理客户端新连接接入请求。我们可以把Netty 比作一个餐馆,bossGroup 就像一个大堂经理,当客户来到餐馆吃时,大堂经理就会引导顾客就坐,为顾客端茶送水等。而workerGroup 就是实际上干活的厨师,它们负责客户端连接通道的IO 操作:当大堂经历接待顾客后,顾客可以稍做休息, 而此时后厨里的厨师们(workerGroup)就开始忙碌地准备饭菜了。关于bossGroup 与workerGroup 的关系,我们可以用如下图来展示:

  首先,服务端的bossGroup 不断地监听是否有客户端的连接,当发现有一个新的客户端连接到来时,bossGroup 就会为此连接初始化各项资源,然后从workerGroup 中选出一个EventLoop 绑定到此客户端连接中。那么接下来的服务器与客户端的交互过程就全部在此分配的EventLoop 中完成。口说无凭,我们还是以源码说话吧。首先在ServerBootstrap 初始化时,调用了b.group(bossGroup, workerGroup)设置了两个EventLoopGroup,我们跟踪进去以后会看到:

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);this.childGroup = childGroup;
        return this;
}

  显然,这个方法初始化了两个字段,一个是group = parentGroup。它是在super.group(parentGroup)中完成初始化的,另一个是childGroup = childGroup。接着从应用程序的启动代码来看调用了b.bind()方法来监听一个本地端口。bind()方法会触发如下调用链:AbstractBootstrap.bind() -> AbstractBootstrap.doBind() -> AbstractBootstrap.initAndRegister()源码看到到这里为止,我们发现AbstractBootstrap 的initAndRegister()方法已经是我们的老朋友了,我们在分析客户端程序时和它打过很多交道,现在再来回顾一下这个方法吧:

final ChannelFuture initAndRegister() {
        channel = channelFactory.newChannel();
        init(channel);
        ChannelFuture regFuture = config().group().register(channel);
}

  这里group()方法返回的是上面我们提到的bossGroup,而这里的channel 其实就是NioServerSocketChannel 的实例,因此我们可以猜测group().register(channel)将bossGroup 和NioServerSocketChannel 应该就关联起来了。那么workerGroup 具体是在哪里与NioServerSocketChannel 关联的呢?我们继续往下看init(channel)方法:

void init(Channel channel) throws Exception {
        ChannelPipeline p = channel.pipeline();
        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
     // .......
        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final 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(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

  实际上init()方法在ServerBootstrap 中被重写了,从上面的代码片段中我们看到,它为pipeline 中添加了一个ChannelInitializer,而这个ChannelInitializer 中添加了一个非常关键的ServerBootstrapAcceptor 的handler。关于handler 的添加与初始化的过程,我们留到之再详细分析。现在,我们来关注一下ServerBootstrapAcceptor类。在ServerBootstrapAcceptor 中重写了channelRead()方法,其主要代码如下:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);

            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }

            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
}

  ServerBootstrapAcceptor 中的childGroup 是构造此对象是传入的currentChildGroup,也就是workerGroup 对象。而这里的Channel 是NioSocketChannel 的实例,因此这里的childGroup 的register()方法就是将workerGroup 中的某个EventLoop 和NioSocketChannel 关联上了。既然如此,那么现在的问题是ServerBootstrapAcceptor 的channelRead()方法是在哪里被调用的呢? 其实当一个client 连接到server 时,Java 底层NIO 的ServerSocketChannel就会有一个SelectionKey.OP_ACCEPT 的事件就绪,接着就会调用到NioServerSocketChannel 的doReadMessages()方法:

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

  在doReadMessages()方法中,通过调用javaChannel().accept()方法获取到客户端新连接的SocketChannel 对象,紧接着就实例化一个NioSocketChannel,并且传入NioServerSocketChannel 对象(即this)。由此可知,我们创建的这个NioSocketChannel 的父类Channel 就是NioServerSocketChannel 实例。接下来就经由Netty 的ChannelPipeline 机制,将读取事件逐级发送到各个handler 中,于是就会触发前面我们提到的ServerBootstrapAcceptor 的channelRead()方法。

服务端Selector 事件轮询:

  再回到服务端ServerBootStrap 的启动代码,是从bind()方法开始的。ServerBootStrapt 的bind()方法实际上就是其父类AbstractBootstrap 的bind()方法,来看代码:

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()方法中,调用的是EventLoop 的execute()方法,我们继续跟进去:

public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
} else {
boolean inEventLoop = this.inEventLoop();
if (inEventLoop) {
this.addTask(task);
} else {
this.startThread();
this.addTask(task);
      // .......
}
}

   在execute()主要就是创建线程,将线程添加到EventLoop 的无锁化串行任务队列。我们重点关注startThread()方法,继续看源代码:

private void startThread() {
    doStartThread();
}
private void doStartThread() {
executor.execute(new Runnable() {
     .....
SingleThreadEventExecutor.this.run();
    .......
  });
}
 

  我们发现startThread()最终调用的是SingleThreadEventExecutor.this.run()方法,这个this 就是NioEventLoop 对象:

protected void run() {
        for (;;) {
            try {
                try {
                    switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.BUSY_WAIT:
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));

                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                    }
                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        runAllTasks();
                    }
          ...... }
}

  终于看到似曾相识的代码。上面代码主要就是用一个死循环,在不断地轮询SelectionKey.select()方法,主要用来解决JDK 空轮训Bug,而processSelectedKeys()就是针对不同的轮询事件进行处理。如果客户端有数据写入,最终也会调用AbstractNioMessageChannel 的doReadMessages()方法。那么我们先来看一下是哪里调用的,通过追踪processSelectedKeys()方法,最后会调用到NioEventLoop的processSelectedKey 方法:

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
            final EventLoop eventLoop;

            eventLoop = ch.eventLoop();
int readyOps = k.readyOps();

            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {

                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }

            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                ch.unsafe().forceFlush();
            }

            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

  这里可以看到熟悉的代码,当有链接进来的时候,便会走 unsafe.read():这个里面就调用了doReadMessages

  1. Connect, 即连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT.int值为16.
  2. Accept, 即确认事件, 对应于SelectionKey.OP_ACCEPT.int值为8.
  3. Read, 即读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读.int值为1
  4. Write, 即写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写.int值为4
public void read() {
   int localRead = doReadMessages(readBuf);
}

总结一下:

  1. Netty 中Selector 事件轮询是从EventLoop 的execute()方法开始的。
  2. 在EventLoop 的execute()方法中,会为每一个任务创建一个独立的线程,并保存到无锁化串行任务队列。
  3. 线程任务队列的每个任务实际调用的是NioEventLoop 的run()方法。
  4. 在run 方法中调用processSelectedKeys()处理轮询事件。

Netty 解决JDK 空轮训Bug:

  各位应该早有耳闻臭名昭著的Java NIO epoll 的bug,它会导致Selector 空轮询,最终导致CPU 100%。官方声称在JDK1.6 版本的update18 修复了该问题,但是直到JDK1.7 版本该问题仍旧存在,只不过该BUG 发生概率降低了一些而已,它并没有被根本解决。出现此Bug 是因为当Selector 的轮询结果为空,也没有wakeup 或新消息处理,则发生空轮询,CPU 使用率达到100%。我们来看下这个问题在issue 中的原始描述:

This is an issue with poll (and epoll) on Linux. If a file descriptor for a connected socket is polled with a request
event mask of 0, and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and
maybe POLLERR) bit set in the returned event set. The implication of this behaviour is that Selector will wakeup and
as the interest set for the SocketChannel is 0 it means there aren't any selected events and the select method
returns 0.

  具体解释为:在部分Linux 的2.6 的kernel 中,poll 和epoll 对于突然中断的连接socket 会对返回的eventSet 事件集合置为POLLHUP,也可能是POLLERR,eventSet 事件集合发生了变化,这就可能导致Selector 会被唤醒。这是与操作系统机制有关系的,JDK 虽然仅仅是一个兼容各个操作系统平台的软件,但很遗憾在JDK5 和JDK6 最初的版本中(严格意义上来将,JDK 部分版本都是),这个问题并没有解决,而将这个帽子抛给了操作系统方,这也就是这个bug 最终一直到2013 年才最终修复的原因。在Netty 中最终的解决办法是:创建一个新的Selector,将可用事件重新注册到新的Selector 中来终止空轮训。前面我们有提到select()方法解决了JDK 空轮训的Bug,它到底是如何解决的呢?下面我们来一探究竟,进入select()方法的源码:

private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

            for (;;) {
          // .......
int selectedKeys = selector.select(timeoutMillis); selectCnt ++; if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) { break; } if (Thread.interrupted()) {             //..... selectCnt = 1; break; } long time = System.nanoTime(); if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) { // timeoutMillis elapsed without anything selected. selectCnt = 1; } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { selector = selectRebuildSelector(selectCnt); selectCnt = 1; break; } currentTimeNanos = time; } }

  从上面的代码中可以看出,Selector 每一次轮询都计数selectCnt++,开始轮询会计时赋值给timeoutMillis,轮询完成会计时赋值给time,这两个时间差会有一个时间差,而这个时间差就是每次轮询所消耗的时间。从上面的的逻辑看出,如果每次轮询消耗的时间为0,且重复次数超过512 次,则调用rebuildSelector()方法,即重构Selector。我们跟进到源码中就会发现:

private void rebuildSelector0() {
        final Selector oldSelector = selector;
        final SelectorTuple newSelectorTuple;

        newSelectorTuple = openSelector();
      // Register all channels to the new Selector.
        int nChannels = 0;
        for (SelectionKey key: oldSelector.keys()) {
            Object a = key.attachment();
            try {
                if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                    continue;
                }

                int interestOps = key.interestOps();
                key.cancel();
                SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                
    }

   在rebuildSelector()方法中,主要做了三件事情:

  1. 创建一个新的Selector。
  2. 将原来Selector 中注册的事件全部取消。
  3. 将可用事件重新注册到新的Selector 中,并激活。

NioServerSocktChannel 的创建:

  我们在分析客户端Channel 初始化过程时已经提到,Channel 是对Java 底层Socket 连接的抽象,并且知道了客户端Channel 的具体类型是NioSocketChannel,那么,自然的服务端的Channel 类型就是NioServerSocketChannel 了。通过前面的分析, 我们已经知道了,在客户端中,Channel 类型的指定是在初始化时,通过Bootstrap 的channel()方法设置的,服务端也是同样的方式。再看服务端代码,我们调用了ServerBootstarap 的channel(NioServerSocketChannel.class)方法,传的参数是NioServerSocketChannel.class 对象。如此,按照客户端代码同样的流程,我们可以确定NioServerSocketChannel 的实例化也是通过ReflectiveChannelFactory 工厂类来完成的,而ReflectiveChannelFactory 中的clazz 字段被赋值为NioServerSocketChannel.class,因此当调ReflectiveChannelFactory 的newChannel()方法,就能获取到一个NioServerSocketChannel 的实例。

  最后我们也来总结一下:

  1. ServerBootstrap 中的ChannelFactory 的实现类是ReflectiveChannelFactory 类。
  2. 创建的Channel 具体类型是NioServerSocketChannel。

Channel 的实例化过程:

  其实就是调用ChannelFactory 的newChannel()方法,而实例化的Channel 具体类型就是初始化ServerBootstrap 时传给channel()方法的实参。因此,上面代码案例中的服务端ServerBootstrap, 创建的Channel实例就是NioServerSocketChannel 的实例。

服务端Channel 的初始化:

  我们来分析NioServerSocketChannel 的实例化过程,先看一下NioServerSocketChannel 的类层次结构图:

  首先,我们来跟踪一下NioServerSocketChannel 的默认构造,和NioSocketChannel 类似,构造器都是调用newSocket()来打开一个Java 的NIO Socket。不过需要注意的是, 客户端的newSocket()方法调用的是openSocketChannel(),而服务端的newSocket()调用的是openServerSocketChannel()。顾名思义,一个是客户端的Java SocketChannel,一个是服务器端的Java ServerSocketChannel,来看代码:

private static ServerSocketChannel newSocket(SelectorProvider provider) {
      return provider.openServerSocketChannel();
}

  接下来会调用重载构造方法:

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

  这个构造方法中,调用父类构造方法时传入的参数是SelectionKey.OP_ACCEPT。作为对比,我们回顾一下,在客户端的Channel 初始化时,传入的参数是SelectionKey.OP_READ。在服务启动后需要监听客户端的连接请求,因此在这里我们设置SelectionKey.OP_ACCEPT,也就是通知selector 我们对客户端的连接请求感兴趣。接着和客户端对比分析一下,会逐级地调用父类的构造器NioServerSocketChannel -> AbstractNioMessageChannel-> AbstractNioChannel -> AbstractChannel。同样的,在AbstractChannel 中实例化一个unsafe 和pipeline:

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

  不过,在这里需要注意的是,客户端的unsafe 是AbstractNioByteChannel#NioByteUnsafe 的实例,而服务端的unsafe是AbstractNioMessageChannel.AbstractNioUnsafe 的实例。因为AbstractNioMessageChannel 重写了newUnsafe()方法,其源代码如下:

protected AbstractNioUnsafe newUnsafe() {
        return new NioMessageUnsafe();
    }

  最后总结一下, 在NioServerSocketChannel 实例化过程中的执行逻辑:

  1. 调用NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)方法打开一个新的Java NIOServerSocketChannel
  2. AbstractChannel 初始化被赋值是属性:parent:设置为nullunsafe:通过newUnsafe()实例化一个unsafe 对象, 类型是AbstractNioMessageChannel#AbstractNioUnsafepipeline:创建实例DefaultChannelPipeline 实例
  3. AbstractNioChannel 中被赋值的属性:ch:赋值为Java NIO 的ServerSocketChannel,调用NioServerSocketChannel 的newSocket()方法获取。readInterestOp:默认赋值为SelectionKey.OP_ACCEPT。ch 设置为非阻塞,调用ch.configureBlocking(false)方法。
  4. NioServerSocketChannel 中被赋值的属性:config = new NioServerSocketChannelConfig(this, javaChannel().socket())

ChannelPipeline 初始化:

  服务端ChannelPipeline 的初始化和客户端一致,因此就不再单独分析了。

服务端Channel 注册到Selector:

  服务端Channel 的注册过程和客户端一致,也不再单独分析了。

Handler 的添加过程:

  服务端handler 的添加过程和客户端的有点区别,跟EventLoopGroup 一样服务端的handler 也有两个:一个是通过handler()方法设置的handler,另一个是通过childHandler()方法设置的childHandler。通过前面的bossGroup 和workerGroup 的分析,其实我们在这里可以大胆地猜测:handler 与accept 过程有关。即handler 负责处理客户端新连接接入的请求;而childHandler 就是负责和客户端连接的IO 交互。那么实际上是不是这样的呢?我们继续用代码来证明。在前面章节我们已经了解ServerBootstrap 重写了init()方法,在这个方法中也添加了handler:

void init(Channel channel) throws Exception {
        ChannelPipeline p = channel.pipeline();
        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
     // ......
        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final 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(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

  在上面代码的initChannel()方法中,首先通过handler()方法获取一个handler,如果获取的handler 不为空,则添加到pipeline 中。然后接着,添加了一个ServerBootstrapAcceptor 的实例。那么这里的handler()方法返回的是哪个对象呢? 其实它返回的是handler 字段,而这个字段就是我们在服务器端的启动代码中设置的:那么这个时候, pipeline 中的handler 情况如下:

  根据我们原来客户端代码的分析来,我们指定channel 绑定到eventLoop(在这里是指NioServerSocketChannel 绑定到bossGroup)后,会在pipeline 中触发fireChannelRegistered 事件,接着就会触发对ChannelInitializer 的initChannel()方法的调用。因此在绑定完成后,此时的pipeline 的内如下:

  在前面我们分析bossGroup 和workerGroup 时,已经知道了ServerBootstrapAcceptor 的channelRead()方法会为新建的Channel 设置handler 并注册到一个eventLoop 中,即:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);

            childGroup.register(child).addListener(new ChannelFutureListener() {

        }

  而这里的childHandler 就是我们在服务器端启动代码中设置的handler。后续的步骤我们基本上已经清楚了,当客户端连接Channel 注册后,就会触发ChannelInitializer 的initChannel()方法的调用。最后我们来总结一下服务端handler 与childHandler 的区别与联系:

  1. 在服务器NioServerSocketChannel 的pipeline 中添加的是handler 与ServerBootstrapAcceptor。
  2. 当有新的客户端连接请求时,调用ServerBootstrapAcceptor 的channelRead()方法创建此连接的NioSocketChannel 并添加childHandler 到NioSocketChannel 对应的pipeline 中,并将此channel 绑定到workerGroup 中的某个eventLoop 中。
  3. handler 是在accept 阶段起作用,它处理客户端的连接请求。
  4. childHandler 是在客户端连接建立以后起作用,它负责客户端连接的IO 交互。

  最后来看一张图,加深理解。下图描述了服务端调用流程:

 

posted @ 2019-07-19 21:57  吴振照  阅读(1678)  评论(0编辑  收藏  举报