netty原理分析

187 阅读14分钟

今天我们主要讲下netty运行的原理,我们将从下面四个点来分析netty的运行原理

1、netty io模型

2、netty核心组件

3、netty项目架构

4、netty运行流程

首先我们来看一段netty服务端的代码

public final class NettyServer {

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(serverHandler);
                        }
                    });
            ChannelFuture f = b.bind(8007).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

可以看到的是,与jdk的nio来比较的话,netty来实现服务器端的逻辑还是相当简单的。大家可以自行百度jdk nio的服务器端相关代码。那么为啥低一点我们这里需要去介绍netty的io模型呢,因为netty采用的是大名鼎鼎的主从reactor线程模型。我们可以通过NIO学习--Reactor模型这里可以学习到三种reactor线程模型,那么为啥说netty使用的是多reactor线程模型呢,大家可以看到的是,上面的服务器端代码中有两个NioEventLoopGroup,分别是bossGroup和workerGroup,可以通过字面的意思来猜测,NioEventLoopGroup是一组线程池组,里面装的主要就是NioEventLoop,NioEventLoop的初始化就是在NioEventLoopGroup初始化里面完成的,下面列出NioEventLoop初始化的过程,这段代码位于MultithreadEventExecutorGroup里面

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        /**
         * 1、NioEventLoopGroup#NioEventLoopGroup()
         * 2、MultithreadEventLoopGroup#MultithreadEventLoopGroup(int, ThreadFactory, Object...)
         * 3、MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, EventExecutorChooserFactory, Object...)
         */
        if (nThreads <= 0) {
            throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
        }
        // when executor is null, we can use default executor
        if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
        // 创建nThreads个EventExecutor
        children = new EventExecutor[nThreads];

        // 针对workerGroup中的executor赋值
        for (int i = 0; i < nThreads; i++) {
            boolean success = false;
            try {
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                // 当初始化eventExecurtor数组失败的时候,将之前所有的eventExecuror注销掉
                if (!success) {
                    for (int j = 0; j < i; j++) {
                        children[j].shutdownGracefully();
                    }

                    for (int j = 0; j < i; j++) {
                        EventExecutor e = children[j];
                        try {
                            while (!e.isTerminated()) {
                                e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                            }
                        } catch (InterruptedException interrupted) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }
        // 事件执行器选择器,根据executors的数量来决定选用何种选择器
        chooser = chooserFactory.newChooser(children);

        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
                if (terminatedChildren.incrementAndGet() == children.length) {
                    terminationFuture.setSuccess(null);
                }
            }
        };

        for (EventExecutor e : children) {
            e.terminationFuture().addListener(terminationListener);
        }

        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
    }

其实NioEventLoopGroup的核心初始化流程就是这个方法了,在这个方法里面可以看到的是,里面的for循环中children[i] = newChild(executor, args); 这一句话就是做EventLoop初始化的逻辑,当你使用的是NioEventLoopGroup的时候,就会初始化成NioEventLoop,这里的newChild是一个抽象方法,会调用子类的逻辑来实现,我们可以看到NioEventLoopGroup中就有这个实现的方法

 @Override
    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的时候,children数组里面存放的都是NioEventLoop,当你使用其他Group的时候,children数组里面存放的自然都是其他的一些EventLoop。这里我们就把NioEventLoop的关系给大家介绍的很清楚了。

其实这里面我们可以看到的是,bossGroup就是一个reactor, 主要用来处理网络 IO 连接建立操作,通常,mainReactor 只需要一个,因为它一个线程就可以处理。

workderGroup就是另外的一个reactor,主要和建立起来的客户端的 SocketChannel 做数据交互和事件业务处理操作。通常,subReactor 的个数和 CPU 个数相等,每个 subReactor 独占一个线程来处理。netty中默认的情况下subReactor的个数就是cpu核数*2。

相信大家通过上面的介绍已经简单的理解了reactor线程模型了吧。

高层类图

下面我们来介绍netty的核心组件

1、BootStrap&ServerBootSrap

2、Channel

3、ChannelPipeline

4、ChannelHandler

5、EventLoop&EventLoopGroup

BootStrap&ServerBootSrap

大家仔细观察上面的图,可以看到的是BootStrap和ServerBootStrap基本上是用来做相关配置的,但是这些配置包含了配置EventLoopGroup、配置Channel,可以看到的是Channel和ChannelPipeline有关联,ChannelPipeline又与ChannelHandler有关联,而上面的介绍中,大家已经知道了EventLoopGroup与EventLoop有关联,所以来说这两个引导类,基本上就是我们netty程序的入口,令人感到高兴的是,里面的代码并不多,所以理解了每个类之间的关系,对于理解netty的工作原理还是相当重要的。

channel

Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel 的 EventLoop 。

在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而 Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势( 摘自《Netty权威指南( 第二版 )》) :

  • 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
  • Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
  • 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,由 Channel 统一负责和调度,功能实现更加灵活。

我们现在下面来介绍NioServerSocketChannel和NioSocketChannel,上面的可以知道,channel主要是netty网络抽象的类,所以来说channel就是我们实际读写事件的入口,那么我们来简单的看下channel中有哪些基本的属性呢

  private final SelectableChannel ch; // jdk的channel
  protected final int readInterestOp; // 感兴趣的操作
  volatile SelectionKey selectionKey; // 注册到selector上后返回的selectionKey
  boolean readPending;
  private final Runnable clearReadPendingRunnable = new Runnable() {
      @Override
      public void run() {
          clearReadPending0();
      }
  };
  private ChannelPromise connectPromise;
  private ScheduledFuture<?> connectTimeoutFuture;
  private SocketAddress requestedRemoteAddress;

这个是AbstractNioChannel类中的属性,当然这个类是NioServerSocketChannel的父类了,我们继续向上观察

    private final Channel parent;
    private final ChannelId id;
    private final Unsafe unsafe;
    private final DefaultChannelPipeline pipeline;
    private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);
    private final CloseFuture closeFuture = new CloseFuture(this);

    private volatile SocketAddress localAddress;
    private volatile SocketAddress remoteAddress;
    private volatile EventLoop eventLoop;
    private volatile boolean registered;
    private boolean closeInitiated;
    private Throwable initialCloseCause;

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

这个是AbstractChannel中的构造方法和部分属性信息,我们通过这个类可以观察到的信息点有哪些呢,每个NioServerSocketChannel都会对应一个channelPipeline和eventLoop,当然里面还有一个非常重要的属性,就是unsafe,可以看到构造方法里面就有unsafe和pipeline的实例化过程,大家感兴趣的可以自己去看下,这里也是非常简单的,newUnsafe是一个抽象的方法,也是会到子类里面去完成unsafe对象的实例化,大家可以去看下NioServerSocketChannel和NioSocketChannel分别产生的unsafe是哪个对象。

其实channel这个类就是相关的层次复杂了一点点,其实当你把整个继承关系弄清楚了之后,还是比较容易的,例如channel和eventloop,channel和pipeline之间的关系等等。相关一些channel的api等后面的运行原理里面会做更加仔细的介绍。这里只是简单分析一下,初始化的一些逻辑。

ChannelPipeline&ChannelHandler

下面呢,我们就简单的介绍一下ChannelPipeline,大家可以看到的是ChannelPipeline是和Channel绑定在一起的,channelPipeline的初始化依赖于Channel的初始化流程, pipeline = newChannelPipeline(); 这里就是channelPipeline实例化的代码,默认实现是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的初始化也是很简单的,其中最重要的就是TailContext和HeadContext,两个属性,可以看到的是DefaultChannelPipeline内部维护的是一个双向链表,首节点已经固定为HeadContext,尾节点已经固定为TailContext。如果仔细观察了之后可以发现HeadContext和TailContext都是AbstractChannelHandlerContext的子类,那我们再去窥探一下AbstractChannelHandlerContext

  	volatile AbstractChannelHandlerContext next;
    volatile AbstractChannelHandlerContext prev;

    private final boolean inbound;
    private final boolean outbound;
    private final DefaultChannelPipeline pipeline;
    private final String name;
    private final boolean ordered;

    AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
                                  boolean inbound, boolean outbound) {
        this.name = ObjectUtil.checkNotNull(name, "name");
        this.pipeline = pipeline;
        this.executor = executor;
        this.inbound = inbound;
        this.outbound = outbound;
        // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
        ordered = executor == null || executor instanceof OrderedEventExecutor;
    }

可以通过上面我们可以看到AbstractChannelHandlerContext的部分属性以及构造方法,看到next和prev节点的同时,是不是更加坐实了DefaultChannelPipeline维护了一个基于AbstractChannelHandlerContext为节点的双向链表呢。不止是这样,我们还可以观察到inbound和outbound两个属性,那么这两个属性是用来干嘛的呢,我也不卖关子,直接可以告诉大家,inbound代表入站属性,outbound代表出站属性,而两者都是true的时候就代表当前AbstractChannelHandlerContext里面的handler既处理入站事件,又处理出站事件。大家可以看到的是channelPipeline与channelHandler之间不是直接关联的,而是通过一个第三者来维护着彼此之间的关系的。这样处理的目的为的就是入站事件使用入站的handler按照顺序处理,出站的事件按照出站的顺序依次处理。

*                                                 I/O Request
*                                            via {@link Channel} or
*                                        {@link ChannelHandlerContext}
*                                                      |
*  +---------------------------------------------------+---------------+
*  |                           ChannelPipeline         |               |
*  |                                                  \|/              |
*  |    +---------------------+            +-----------+----------+    |
*  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  |               |
*  |               |                                  \|/              |
*  |    +----------+----------+            +-----------+----------+    |
*  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  .               |
*  |               .                                   .               |
*  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
*  |        [ method call]                       [method call]         |
*  |               .                                   .               |
*  |               .                                  \|/              |
*  |    +----------+----------+            +-----------+----------+    |
*  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  |               |
*  |               |                                  \|/              |
*  |    +----------+----------+            +-----------+----------+    |
*  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  |               |
*  +---------------+-----------------------------------+---------------+
*                  |                                  \|/
*  +---------------+-----------------------------------+---------------+
*  |               |                                   |               |
*  |       [ Socket.read() ]                    [ Socket.write() ]     |
*  |                                                                   |
*  |  Netty Internal I/O Threads (Transport Implementation)            |
*  +-------------------------------------------------------------------+

通过上面的图大家应该可以更好的理解handler与pipeline之间的关系,由于是双向链表,所以既可以从尾节点向首节点完成事件的传递,又可以完成首节点到未节点的传递。当然上面的图其实还是有一点问题的,因为所有的handler应该是在一条线上的,上面为了大家更好的理解,所以画成了这种样子。

    /**
     * 找到下一个inbound handler
     * @return
     */
    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        // 查找下一个handler,如果前一个handler不是inbound,继续向前查找,直到找到第一个inbound handler
        do {
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    }
 /**
     * 找到前一个outbaund handler
     * @return
     */
    private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
        // 查找前一个handler,如果前一个handler不是outbound,继续向前查找,直到找到第一个outbound handler
        do {
            ctx = ctx.prev;
        } while (!ctx.outbound);
        return ctx;
    }

可以看到的是netty的源码中确实存在寻找下一个inbound AbstractChannelHandlerContext和上一个outbound AbstractChannelHandlerContext的逻辑,似乎一切都在推理之中。

上面已经将netty的核心组件基本上都有一个简单的介绍,下面我们来介绍一下整个运行流程,netty是如何处理各种io事件的。PS:没有介绍netty启动流程,请大家自行查阅相关资料来研究吧。

netty运行流程

由于启动流程不在本篇的讨论范围之内,我就直接切入到netty运行的代码里面给大家介绍相关的逻辑了

run

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

                return;
            }
            if (eventLoop != this || eventLoop == null) {
                return;
            }
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
            // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
            // the NIO JDK channel implementation may throw a NotYetConnectedException.
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
                // See https://github.com/netty/netty/issues/924
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }

            // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
                unsafe.forceFlush();
            }


            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                // NIoMessageUnsafe
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

之前说了一个reactor线程模型,不知道大家是否还记得,作为服务器端在绑定端口之后,服务器端就会起多个线程,以上面的NettyServer代码为例的话,bossGroup会启动一个线程,也就是分析的children数组的数量为1,里面对应着一个NioEventLoop,另外workerGroup会启动cpu核心数*2个线程,就是说这些线程会一直在使用seletor来查询有没有发生io事件,如果有的话,最终就会调用上面的代码,我们来简单的分析下上面的代码,可以看到参数是SelectionKey和AbstractNioChannel,这里面的AbstractNioChannel其实会有多个对象,有可能是NioServerSocketChannel,或者是NioSocketChannel,还有可能是其他的一些Channel,这里面我们主要考虑这连个Channel,可以看到里面的代码都不复杂,最后都是使用unsafe对象来执行对应的业务逻辑,而且前面在channel的初始化的时候已经介绍过unsafe属性吧,可以看到的时候NioEventLoop已经找到了Channel了,上面分析到了事件是在channelPipeline中的双向链表的数据结构中传播的,那么我们来通过源码分析下,unsafe.read()api是怎么找到channelPipeline的,这里我们仅仅介绍NioServerSocketChannel中的unsafe属性的对象-NioMessageUnsafe

    private final class NioMessageUnsafe extends AbstractNioUnsafe {

        private final List<Object> readBuf = new ArrayList<Object>();

        @Override
        public void read() {
            assert eventLoop().inEventLoop();
            final ChannelConfig config = config();
            final ChannelPipeline pipeline = pipeline();
            final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
            allocHandle.reset(config);
            boolean closed = false;
            Throwable exception = null;
            try {
                try {
                    do {
                        int localRead = doReadMessages(readBuf);
                        if (localRead == 0) {
                            break;
                        }
                        if (localRead < 0) {
                            closed = true;
                            break;
                        }
                        allocHandle.incMessagesRead(localRead);
                    } while (allocHandle.continueReading());
                } catch (Throwable t) {
                    exception = t;
                }
                int size = readBuf.size();
                for (int i = 0; i < size; i ++) {
                    readPending = false;
                    pipeline.fireChannelRead(readBuf.get(i));
                }
                readBuf.clear();
                allocHandle.readComplete();
                pipeline.fireChannelReadComplete();

                if (exception != null) {
                    closed = closeOnReadError(exception);

                    pipeline.fireExceptionCaught(exception);
                }
                if (closed) {
                    inputShutdown = true;
                    if (isOpen()) {
                        close(voidPromise());
                    }
                }
            } finally {
                // Check if there is a readPending which was not processed yet.
                // This could be for two reasons:
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
                //
                // See https://github.com/netty/netty/issues/2254
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }
    }

大家可以看到的是里面有执行pipeline.fireXXXX()方法,似乎已经调用到了pipeline的相关api来完成事件的传播工作,一旦事件到达了pipeline就会到达我们写的ChannelHandler,这样的话,我们就可以处理业务逻辑了。

netty项目架构

依赖图

如果大家有下载过netty的源码,其实可以发现,netty基本上就是由5大块组成,就是上面着色的一些模块,其实要熟悉netty的原理的话,基本上只用了解transport相关的源码就可以了,当然transport会依赖于common包里面的一些逻辑,其实简单的来阅读的话,基本上就这两个包,就可以了解netty的原理,当然再深入一点点就是理解buffer包里面的代码,handler和codec其实不是很重要,因为codec就是编解码,其实就是一种特殊的handler,而netty提供的handler就是我们上面的channelHandler,就是提供的一种处理读写情况的一种业务逻辑实现,当然我们假如需要使用netty来编码的话,当然必须实现自己的ChannelHandler,当然有些功能可以直接使用netty提供的ChannelHandler来处理,个人推荐的阅读顺序是transport->common->buffer,其他两个自己随便,buffer其中的内存管理,以及零拷贝还是挺复杂的。

未分析的地方

其实在这边文章里面,笔者也有众多未分析的地方,包含但不局限于下面的一些点

1、NioServerSocketChannel接收到客户端的链接之后,是怎么把Channel注册到NioEventLoop上面的Selector上的

2、channelPipeline中的AbstractChannelHandlerContext是如何依次向后传递的,并且能让你无感的,大家写的ChannelHandler并没有明显的调用下一个handler方法

3、netty的启动流程,做了哪些工作?

4、内存管理&buffer模块的工作

5、netty的线程是什么时候启动的?

总结

那么现在来简单的总结一下netty的原理,简单的来说,我们使用ServerBootSrap配置好了NioEventLoopGroup之后,并且绑定了端口之后,我们有很多线程在一直轮询注册到NioEventLoop里面的selector对象有没有发生io事件,如果有发生的话,我们会把事件通过channel里面的unsafe对象传递给ChannelPipeline来处理逻辑,而ChannelPipeline会使用对应的ChannelHandler来处理最终的逻辑。

netty源码阅读心得

其实在粗粒度的读完netty源码之后,发现netty源码不是那么的复杂,对于这种作为通讯的框架来说,无非就是监听某个端口,然后一直在使用类似while(true)之类的代码去轮询检查有没有发生数据的读写或连接等情况的产生,如果有的话,那么就使用逻辑去处理数据,还有就是一定要找到主线逻辑,一般的情况下,在代码启动的时候的逻辑要读懂,还有就是运行的时候逻辑要清楚,另外的话,最好可以清楚的知道,这个框架启动的每个线程是干嘛的,只有这样,才可以说更好的弄懂了这个框架到底的数据是如何处理的,如果有不同意见,欢迎沟通。也希望大家可以帮着完善上面未分析的地方,让大家可以更加清晰的掌握netty的启动流程&运行流程的逻辑。