• 0

  • 490

Netty系列五、Netty中的核心组件关系分析

黑猫

我不是黑客

6天前

引入

注意点: 源码分析采用的是4.1.15版本的Netty, 源码主要针对于服务器端进行分析(客户端其实类似)
大家在使用Netty的时候, 必然会涉及到自定义ChannelHandler来完成业务逻辑的处理, 与此同时, 会通过继承
ChannelInitializer这个类来将ChannelHandler注册到ChannelPipeline中, 并且在ChannelHandler的实现中
必然又会接触到ChannelHandlerContext这个类, 本文将会从源码的层次上对这些类之间的关系进行详细的分析,
从而让大家掌握Netty中类似于拦截器 / 过滤器模式对数据处理的整体架构

之后, 会引入Netty中对Nio的SocketChannel以及ServerSocketChannel封装类NioSocketChannel、
NioServerSocketChannel, 并带领大家捋清楚这些类与上面的ChannelPipeline之间的关系
再往后会引入Unsafe这个组件, 分析Unsafe组件与Channel之间的关系, 最后引入EventLoopGroup及EventLoop
复制代码

ChannelHandler

ChannelHandler是一个接口, 联想我们使用Netty的步骤, 会利用各种ChannelHandler来对数据进行处理, 对数据
流的编码解码都是基于ChannelHandler来完成的, 站在服务器的角度上, 各个不同类型的ChannelHandler构成了
服务器处理客户端消息的链条, 数据在ChannelHandler-A中处理完后传到ChannelHandler-B, 最后再回传回来,
ChannelHandler中定义了一个个的接口方法, 这些接口方法是整个数据处理流程生命周期的回调, 在不同的生命周
期时间点触发不同的回调方法

ChannelHandler通常会分为三类, 分别是入站处理器、出站处理器以及入站出站处理器, 入站处理器仅仅会在数据
开始处理到处理完毕起作用, 通常用于对数据的解码, 以及业务操作等, 出站处理器作用于数据处理完毕后回传给客
户端之前起作用, 一般用于对数据进行编码操作, 而入站出站处理器则是整个生命周期均起作用

相信大家如果使用过Netty的话对这些应该很熟悉了, 我们假设有入站出站处理器以下几个:
InboundHandler_1 -> OutboundHandler_1 -> InbondHandler_2 -> OutboundHandler_2
 

他们以链表的形式存放到了一起, 假设目前的有数据从客户端发送过来, 那么会经过InboundHandler_1将完成的字
节数据组织到一起, 其实就是解决TCP中的粘包半包问题, 确保我们能够拿到客户端发送的一条完整消息, 然后经过
InbondHandler_2对数据进行处理, 在该处理器中完成业务操作, 比如输出这条消息, 然后将消息通过
writeAndFlush回传给客户端, 当我们触发了该writeAndFlush方法后, 就会同时触发OutboundHandler的处理流
程, 先经过OutboundHandler_2往数据中增加一些信息, 比如数据的前缀等等(根据业务需求), 然后通过
OutboundHandler_1对数据进行编码处理, 这样客户端就能同样通过相同的流程获取到服务器发送的一条完整消息,
这就是ChannelHandler的大致处理流程, 相信这些大家都应该很熟悉了, 在此进行简单的回顾

可以看到, 服务器对数据的处理, 一定会先通过InbondHandler, 再通过OutbondHandler, 上面的链表组织形式中
InboundHandler_1处理完毕后会跳过OutboundHandler_1, 进而利用InbondHandler_2对数据继续进行处理, 其实
在整个处理的流程中, Netty会遍历这个链表中所有的ChannelHandler, 通过
ChannelHandler instanceof ChannelInboundHandler 或者 ChannelOutboundHandler来判断是何种类型的处
理器, 所以判断条件是很简单的

我们先不去分析ChannelHandler中的所有生命周期方法, 这个之后会有专门的文章进行分析
复制代码

ChannelHandlerContext

在上述对ChannelHandler的描述中, 我们得出了一个结论, 所有的ChannelHandler以链表的形式组织到了一起, 
那ChannelHandler仅仅是一个接口, 子类实现了这个接口后不可能还要去维护这样的链表关系吧, 于是就出现了
ChannelHandlerContext, 每一个ChannelHandlerContext中有其对应的ChannelHandler, 其大概的结构如下:

abstract class AbstractChannelHandlerContext implements ChannelHandlerContext{
    AbstractChannelHandlerContext next;
    AbstractChannelHandlerContext prev;
}

class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
    private ChannelHandler handler;

    public DefaultChannelHandlerContext (ChannelHandler handler) {
        this.handler = handler;
    }

    private static boolean isInbound(ChannelHandler handler) {
        return handler instanceof ChannelInboundHandler;
    }

    private static boolean isOutbound(ChannelHandler handler) {
        return handler instanceof ChannelOutboundHandler;
    }
}

分析:
    很清晰, 通过AbstractChannelHandlerContext的prev和next指针来完成了一个双向链表的关系维护, 通过
    子类来保存ChannelHandler的引用, 以及对出站处理器和入站处理器的判断, 抽象类中的其他属性以及相关
    方法我们先不进行分析, 这里仅仅是为了说明ChannelHandlerContext和ChannelHandler的关系而已, 换句
    话说, 我们添加的一个个的Handler在Netty中会以ChannelHandlerContext作为单位, 依次构成一条链表
复制代码

ChannelPipeline

public class DefaultChannelPipeline implements ChannelPipeline {
    AbstractChannelHandlerContext head;
    AbstractChannelHandlerContext tail;
    private final Channel channel;

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = channel;
        tail = new TailContext(this);
        head = new HeadContext(this);
        head.next = tail;
        tail.prev = head;
    }

    private void addLast0(AbstractChannelHandlerContext newCtx) {
        AbstractChannelHandlerContext prev = tail.prev;
        newCtx.prev = prev;
        newCtx.next = tail;
        prev.next = newCtx;
        tail.prev = newCtx;
    }
}

分析:
    上一小节我们了解到ChannelHandlerContext封装了ChannelHandler, 通过prev和next两个指针完成了该
    Handler双向链表的维护关系, 但是链表一定有一个头节点和一个尾节点, 而在ChannelPipeline中就保存了
    这两个头尾节点, 可以在构造方法中看到, 对头尾节点进行了初始化, 分别是TailContext以及HeadContext,
    我们先不用去看这两个节点的源码, 这两个节点是Netty内置的ChannelHandler, 在整个生命周期中扮演着及
    其重要的作用, 在之后我们分析服务器中处理客户端连接和数据的时候会详细分析, 所以说ChannelPipeline就
    是ChannelHandler组成的链表的入口点, 而添加Handler就是将这个Handler封装成ChannelHandlerContext
    然后拼接到pipeline中初始化好的链表中, 链表的头和尾是不会发生改变的, 添加的一个个Context始终会处于
    这两个Context之中
复制代码

小小的总结

ChannlePipeline中通过头尾节点提供了ChannelHandler链表的入口, 对一个Channel添加的ChannelHandler都会
放置在这个链表中, 通过ChannelPipeline组织起来, 我们在使用Netty的时候, 通常都会利用ChannelInitializer
往ChannelPipeline中添加ChannelHandler, 其实最终就是添加到了这个链表中, 由于ChannelHandler只用于对
数据的处理等, 所以利用ChannelHandlerContext对其进行了封装, 使得能够以双向链表的形式放入到
ChannelPipeline中, 换句话说, 我们在代码中通过pipeline.addLast(xxxxChannelHandler)其实就是往
ChannelPipeline中维护的双向链表中添加一个AbstractChannelHandlerContext封装的xxxxChannelHandler
而已, 如下图所示, 描述了上面三个组件之间的关系
复制代码

 

 

Channel的继承体系

如下图所示, 是Netty中Channel的继承体系结构类图(简单版本), DefaultAttributeMap在上一篇文章中我们已经
详细的描述过了, 是整个数据处理流程中一块共享区域, 这样方便在多个ChannelHandler中进行数据的传递, 在
Netty中对Nio中的ServerSocketChannel以及SocketChannel进一步进行了封装, 这个封装的意义在于提供更多的
功能, 以及将相应的各种读写操作放在了其中, 有点类似于上面我们说的ChannelHandlerContext以及
ChannelHandler的关系, 从类图中可以看到, Channel是一个公共接口, 其实现类AbstractChannel定义了所有
类型的Channel的公共数据, Netty中除了对Nio进行了封装, 同时对Bio也进行了封装, 但是对不同类型的I/O来说,
整个处理流程应该都是相同的, 都是通过ChannelPipeline中的一个个ChannelHandler进行处理, 所以对于不同的
I/O类型的公共操作就定义在了AbstractChannel中, 而NIO又有Server端和Client端, 对于这两个类型的Nio也有
公共操作, 比如说都有感兴趣的事件以及Selectionkey, 这些公共的东西就定义在了AbstractNioChannel中, 到
了最后, 才是NioServerSocketChannel(里面保存了一个JavaNio中的ServerSocketChannel)以及
NioSocketChannel(里面保存了一个JavaNio中的SocketChannel), 当然, 在其上面实际还有更多的继承类, 我们
暂时不进行引入, 之后在分析启动流程的时候自然就会遇到它们了
复制代码

 

 

Channel和ChannelPipeline的关系

Netty中, Channel和ChannelPipeline是一对一的关系, 一个Channel对应一个ChannelPipeline, 如下代码:

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
    private final ChannelId id;
    private final Unsafe unsafe;
    private final DefaultChannelPipeline pipeline;
    
    protected AbstractChannel(Channel parent) {
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }

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

    protected abstract AbstractUnsafe newUnsafe();
}



可以看到, 当一个Channel被创建的时候, 一定会伴随着一个Unsafe对象和ChannelPipeline的创建, Unsafe对象
之后我们再说, ChannelId是一个Channel的唯一标识, 这里为了给大家证明的是, 当我们创建了
NioSocketChannel以及NioServerSocketChannel的时候, 其父类必然会创建一个与之对应的ChannelPipeline对
象, 这个ChannelPipeline对象中保存了一个个被ChannelHandlerContext封装的ChannelHandler, 这样我们的
流程大概就清晰了

当客户端连接上了服务器后, 服务器会为之创建一个NioSocketChannel, 这个NioSocketChannel中保存了一个
ChannelPipeline对象, ChannelPipeline在初始化的时候会创建一个头尾节点, 用于链表的入口, 我们添加的一
个个的ChannelHandler就被放入到了这个链表中, 每个Channel都会维护自己的ChannelHandler链表, 数据的处理
也是用各自的ChannelPipeline来进行传递的, 于是我们的Channel的实际表现形式就变成了下图所示了
复制代码

 

 

Unsafe接口及其继承结构

看到这个类, 熟悉Java中AQS的同学应该会比较亲切了, Java也提供了一个Unsafe, Java提供的这个类功能非常的
强大, 可以直接在堆外分配内存、回收堆外内存、调用monitorEnter锁等等, Unsafe故名思意为不安全的意思, 确
实如此, 这些操作都是不安全的, 如果使用不当极可能导致内存泄漏, 而Netty中也有一个类似的实现, 但是Netty
的这个实现是提供给Socket数据传输用的

interface Unsafe {
    void register(EventLoop eventLoop, ChannelPromise promise);
    void bind(SocketAddress localAddress, ChannelPromise promise);
    void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
    void close(ChannelPromise promise);
    void write(Object msg, ChannelPromise promise);
    void beginRead();
}

可以看到, Unsafe提供的方法都是跟Socket相关的, 即端口的绑定、数据的读写以及将Channel注册到Selector中
但是也没那么简单, 通常其不同的子类对这些方法会有不同的实现方式, 这里提前透露一下, 服务器在accept客户
端的连接的时候, 在Netty中确是以read方法进行处理的, 正常我们的思想中, read方法就是读取数据的, 然而在
Netty中对客户端的连接竟然也是以read方法来处理accept的, 之后我们在分析客户端连接服务器的流程的时候会
更加详细的分析, 在这里, 仅仅是为了跟大家说明, Unsafe提供了公共的接口, 不同的子类根据场景的不同会有不
同的实现, 接下来我们以Unsafe的register方法来聊聊Netty中对于Nio的Channel注册到Selector是如何与
Unsafe进行关联的

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
    private final Unsafe unsafe;
    private final DefaultChannelPipeline pipeline;

    protected abstract class AbstractUnsafe implements Unsafe {
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            .................
            register0(promise);
            .................
        }

        protected void doRegister() throws Exception {}

        private void register0(ChannelPromise promise) {
            boolean firstRegistration = neverRegistered;
            doRegister();
            pipeline.invokeHandlerAddedIfNeeded();

            pipeline.fireChannelRegistered();

            if (isActive()) {
                if (firstRegistration) {
                    pipeline.fireChannelActive();
                } else if (config().isAutoRead()) {
                    beginRead();
                }
            }
        }
    }
}

在Unsafe的抽象类AbstractUnsafe中, 对Unsafe接口的register方法进行了实现, 实现中最终调用了register0
方法, 在register0方法中, 我们可以看到调用了一个doRegister方法, 与此同时还调用了ChannelPipeline的生
命周期方法进行回调处理, 那我们可以看到, doRegister方法是AbstractChannel中的方法, 在该类中是一个空实
现, 也就是说子类会进行实现, 我们再来看看子类的实现:

public abstract class AbstractNioChannel extends AbstractChannel {
    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
                if (!selected) {
                    eventLoop().selectNow();
                    selected = true;
                } else {
                    throw e;
                }
            }
        }
    }
}

很清晰, 就是调用Nio中的Channel的register方法, 注册到Selector中, 我们先不用理会这个Selector是哪来的
之后的文章会进行分析, 以上是为了给大家总结出一个结论, Unsafe接口定义了所有的I/O操作方法, 在其抽象类
AbstractUnsafe中对这些方法进行了实现, AbstractUnsafe位于AbstractChannel中, 两者通过模板方法模式进
行了关联, AbstracteUnsafe中实现的各个I/O操作, 最终都会调用AbstractChannel中的doXXX方法, 这些方法基
本都是空实现, 由子类进行实现, 比如AbstractNioChannel就 对doRegister方法进行了实现, 完成了Nio中两种
类型的Channel将自身注册到Selector中的操作

------> 小小的提示, 在AbstractNioChannel的doRegister方法中, 感兴趣的事件是0, 表示对任何事件都不感兴
趣, 这是因为Netty将注册和绑定感兴趣事件两个操作进行了分离, 之后更详细的源码分析中我们可以清晰的看到
复制代码

EventLoopGroup及EventLoop

Netty服务端启动类代码

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

    try {
        ChannelFuture channelFuture = new ServerBootstrap().group(boss, child)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast( new LengthFieldPrepender( 4, 0, false ) );
                            pipeline.addLast( new LengthFieldBasedFrameDecoder( 
                                Integer.MAX_VALUE, 0, 4, 0, 4 ) );
                            pipeline.addLast( new MyInboudHander() )
                        }
                    }).bind(6666).sync();

        channelFuture.channel().closeFuture().sync();
    } finally {
        boss.shutdownGracefully();
        child.shutdownGracefully();
    }
}

这一段代码大家应该都不会感到陌生, 算是Netty服务端的标准启动代码了, 可以看到, 我们一开始创建了两个
NioEventLoopGroup, 分别命名为bossGroup和childGroup, 结合之前我们对Reactor编程模型的分析, 为了能够
使得服务端接收用户的连接和服务端去处理客户端法来的请求互不阻塞, 引入了一主多从的模型, 于是, 服务端接收
连接独立一个Selector, 所有的客户端连接公用指定个数的Selector, 而这两个Group就是一主多从的体现模式

首先我们看到, bossGroup的构造方法中传入了1这个参数, 表示只创建一个Selector, 而childGroup的构造方法
中没有传入参数, 那么在里面就会创建默认个数的Selector, 结合我们之前一主多从的例子, 每个Selector都会在
一个独立的线程中进行处理, 而EventLoop我们就可以认为是这个独立的线程, 同时每一个EventLoop中维护了一个
Selector, 总结为一句话, 一个EventLoop中保存了一个Selector和一个线程, 这个线程是我们常说的I/O线程,
用于监听Selector这个多路复用器, 并对其产生的事件进行处理

小小的总结一下, EventLoopGroup中会保存着多个EventLoop, 一个EventLoop中有一个独立的I/O线程以及一个
Selector, 在bossGroup上, 由于服务器监听客户端的连接只需要一个线程及一个Selector即可, 所以我们在创建
NioEventLoopGroup的时候构造方法中传入的参数即为1, 而客户端与服务器之间的数据交互, 会分配给不同的
Selector中进行处理, 这个Selector处于EventLoop中, 通常情况下我们不会去指定childGroup的参数, 这样在
创建EventLoop的时候就会采用默认值(好像是CPU核心个数 * 2)
复制代码

NioEventLoopGroup的继承体系

如下图所示, 是NioEventLoopGroup的继承体系, 首先看最顶层的三个接口, Executor和ExecutorService是Java
中并发包中的内容了, 我们使用的线程池就是这两个接口的子类, 这两个提供了submit、execute这样的执行任务的
接口方法, 子类重写这样的方法来完成任务的分配与执行, 举个例子, 线程池中调用execute方法的时候, 会从自己
保存的线程中取出一个空闲的线程来执行这个任务

EventExecutorGroup继承于ExecutorService, 在其父类的基础上增加了一些接口方法, 如下:
public interface EventExecutorGroup extends xxxxxx {
    boolean isShuttingDown();
    Future<?> shutdownGracefully();
    EventExecutor next();
    <T> Future<T> submit(Runnable task, T result);
}

shutdownGracefully这个方法意思是优雅的关闭, 我们在Netty项目的入口通常会通过try..finally的形式在
finnaly语句块中优雅的关闭EventLoopGroup, 最为重要的是这个next方法, 以客户端连接所在
NioEventLoopGroup为例子, 一个NioEventLoopGroup会保存着多个EventLoop, 而这个next方法的作用就是, 当
一个客户端要注册到Selector的时候, 由next方法来决定到底注册到哪个Selector中

AbstractEventExecutorGroup里面其实就是对EventExecutorGroup中定义的方法进行了实现而已, 但是next方法
没有实现, 因为next方法应该由子类来实现, 由子类来决定多个Executor以什么方式保存(数组 / List), 所以子
类才能根据这个保存的方式来获取Executor

public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
    private final EventExecutor[] children;
    private final EventExecutorChooserFactory.EventExecutorChooser chooser;

    public EventExecutor next() {
        return chooser.next();
    }
    
    protected abstract EventExecutor newChild(Executor executor, Object... args);
}

可以看到, 在MultithreadEventExecutorGroup中就定义了多个Executor的存储方法---数组, 并且重写了next
方法, 通过EventExecutorChooserFactory中的内部接口类EventExecutorChooser来决定到底如何选择下一个
Exector, 在Netty中仅仅提供了两个该接口的实现类, 一个是通过轮询的方式来选择下一个Executor, 一个就进行
了一下优化, 当children中的个数是2的次方数个的时候, 就会采用这个优化的方式来进行下一个Executor的选择
并且, 提供了一个抽象方法newChild, 由子类来决定children这个数组中存放的是何种类型的Executor

public abstract class MultithreadEventLoopGroup
                     extends MultithreadEventExecutorGroup implements EventLoopGroup {
    private static final int DEFAULT_EVENT_LOOP_THREADS;

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

    @Override
    protected abstract EventLoop newChild(Executor executor, Object... args);

    public ChannelFuture register(Channel channel) {
        return next().register(channel);
    }
}

再往后就是MultithreadEventLoopGroup这个类了, 定义了一个静态属性DEFAULT_EVENT_LOOP_THREADS, 这个
属性就是我们之前所说的默认的EventLoop的数量, 即当我们在new一个NioEventLoopGroup的时候, 如果没有传
线程数, 则默认是用DEFAULT_EVENT_LOOP_THREADS这个值作为线程的个数, 该值在static静态块中进行了初始化
通常情况下是CPU核心数 * 2

newChild方法没有实现, 仍然是一个抽象方法, 交给子类进行实现, 与此同时提供了一个register方法, 参数是一
个Channel, 通过next方法来获取到一个合适的EventLoop, 然后将这个channel注册到这个EventLoop中的
Selector中

public class NioEventLoopGroup extends MultithreadEventLoopGroup {
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), 
            (RejectedExecutionHandler) args[2]);
    }
}

最后就是我们在最开始创建的NioEventLoopGroup了, 可以看到, 其重写了newChild方法, 里面其实就是创建了一
个NioEventLoop....我们不用理会创建NioEventLoop时的各个参数, 之后我们会进行分析
复制代码

 

 

NioEventLoop的继承体系

由下图所示, 是NioEventLoop的继承体系, 仅仅只是贴出来其中一部分而已, 不过我们只需要了解这一部分就够了

在之前我们有了解到, 一个NioEventLoop中会有一个线程以及一个Selector, 这个线程就是我们所说的I/O线程,
接下来我们从源码的层次上来看看这个关系
public abstract class SelectorImpl extends AbstractSelector {
    protected Set<SelectionKey> selectedKeys = new HashSet();
    protected HashSet<SelectionKey> keys = new HashSet();
    private Set<SelectionKey> publicKeys;
    private Set<SelectionKey> publicSelectedKeys;
}

public final class NioEventLoop extends SingleThreadEventLoop {
    private Selector selector;
    private Selector unwrappedSelector;
    private SelectedSelectionKeySet selectedKeys;
    private final SelectorProvider provider;
    private final SelectStrategy selectStrategy;
}

在Nio中, 当一个NioSocketChannel注册到Selector中的时候, 会返回该Channel对应的SelectionKey, 而
Selector(默认实现为SelectorImpl)中会维护一个Set来保存已经注册到其上面的Channel对应的SelectionKey,
在Selector的默认实现中, 这个保存所有SelectionKey的数据结构是一个HashSet(如上面第一段代码), 而Netty
为了优化性能, 自己实现了一个数据结构来保存Selectionkey, 即NioEventLoop中的SelectedSelectionKeySet
其实里面就是用一个数组来保存而已, 如下:
final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
    SelectionKey[] keys;
    int size;
}

在Nio保存SelectionKey的HashSet是放置在Selector的默认实现SelectorImpl中的, 在Netty中, 为了能够使用
自己定义的这一个数据结构来存储SelectionKey, Netty先是通过反射的方式将原来的Selector默认实现中保存
SelectionKey的集合替换成自己实现的, 其次通过继承原来的Selector, 然后自己实现相应的操作, 这个之后我们
分析源码的时候可以看的更加清楚, 其实做的事很简单, 伪代码如下:
public void xxxx () {
    Field selectedKeysField = SelectorImpl.class.getDeclaredField("selectedKeys");
    Field publicSelectedKeysField = SelectorImpl.class.getDeclaredField("publicSelectedKeys");

    selectedKeysField.set(unwrappedSelector, selectedKeySet);
    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
}

从上面的NioEventLoop上可以看到, 其保存了对应的Selector引用, 一共有两个Selector, unwrappedSelector
是Nio中的Selector, 即通过SelectorProvider.open()创建的, 而NioEventLoop中的selector属性, 就是
Netty通过继承Selector来实现的, 即为SelectedSelectionKeySetSelector, 其里面直接就有一个
SelectedSelectionKeySet类型的属性, 如下:
final class SelectedSelectionKeySetSelector extends Selector {
    private final SelectedSelectionKeySet selectionKeys;
    private final Selector delegate;
}

--------------------------------------------------------------------------------------------
ok, 到此为止, 我们分析了NioEventLoop中的结构, 知道了每一个NioEventLoop都会维护一个Selector, 接下来
我们跳过其父类, 直接看其父类的父类SingleThreadEventExecutor
--------------------------------------------------------------------------------------------

public abstract class SingleThreadEventExecutor {
    private volatile Thread thread;
    private final Queue<Runnable> taskQueue;
}

可以清楚的看到, 在SingleThreadEventExecutor中有一个Thread类型的属性, 这就是我们之前说的在每一个
NioEventLoop中都会维护着其自身的I/O线程, 对其内部的Selector多路复用器的监听就是在这个线程中完成的,
到此为止, 我们就从源码的层次上看到了, 一个NioEventLoop会维护着其独有的Selector以及I/O线程, 与此同时
还会有一个taskQueue的任务队列, 这个队列也是Netty中的核心, 是Netty用于解决并发的关键, 但是此时我们不
会对其作用进行分析, 之后当我们了解到了选择策略以及Selector在I/O线程中如何工作的时候, 再来说下这个队列
的作用, 这里仅仅是引出来让大家先知道有这么一个东西而已
复制代码

 

 

总结

在本篇文章中, 对Netty中的核心组件ChannelHandler、ChannelHandlerContext、ChannelPipeline、Channel
Unsafe、EventLoopGroup以及EventLoop之间的关系进行了分析, 一个Channel会维护着一个独立的
ChannelPipeline, 一个ChannelPipeline里面会有多个ChannelHandlerContext, 一个ChannelHandlerContext
里面有其对应的ChannelHandler, ChannelHandler仅仅是一个接口而已, 用于处理客户端-服务器的消息交互, 将
其放置在ChannelHandlerContext中, 一是为了将职责分离, 利用ChannelHandlerContext来保存其他的信息, 与
此同时利用ChannelHandlerContext来维护链表关系, 在ChannelPipeline中会有这个链表的入口, 即HeadContext
和TailContext, 我们通过ChannelInitializer来添加的一个个ChannelHandler, 最终会封装成一个个的
ChannelHandlerContext, 并且放入到HeadContext和TailContext之间

Unsafe是Netty中对I/O数据操作的标准, 所有的I/O操作都要经过Unsafe类, Unsafe类中定义了各种I/O操作的模
板, 这些模板方法中, 都会引用到对应的Channel中的抽象方法, Channel的不同子类对这些抽象方法进行实现, 比
如说AbstractNioChannel就对doRegister方法进行了重写, 实现了将Channel注册到Selector的通用逻辑

EventLoopGroup和EventLoop是一主多从Reactor编程模型的体现, 我们经常使用的是NioEventLoopGroup以及
NioEventLoop, 服务器接收请求独占一个NioEventLoopGroup, 通常情况下里面只会有一个NioEventLoop, 客户
端与服务器则利用额外的NioEventLoopGroup, 这个NioEventLoopGroup中会有多个NioEventLoop, 如果我们不主
动指定的话, 默认就是CPU核心个数 * 2

一个NioEventLoop中有一个Selector和一个I/O线程, 这个I/O线程就是用来监听这个多路复用器Selector的
复制代码
免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

信息安全

490

相关文章推荐

未登录头像

暂无评论