6、我眼中的Netty—源码剖析

本文深入剖析Netty的EventLoop设计,解释其如何保证单线程执行,以及BossGroup中EventLoop的处理机制。通过引导类配置,详细阐述Netty的启动流程,包括监听、事件处理和任务执行。文中还探讨了Handler链的调用、数据传递和心跳事件的实现。此外,分析了任务队列与定时任务的执行策略,以及它们与EventLoop的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、总体概述

每一部分的源码剖析在第一步都应该关联Netty设计模型中对应的步骤,以求知识达到连贯!先对各个模块的源码有一个跟踪理解,最后需要通过自己的语言来描述一下Netty运行的一个完整流程,这一部分将成为一个主要的知识成果输出。

1、源码流程概述

1、最初的时候通过Group的相关方法穿件两个Gruop,这里面关键的就是他们的构造函数是一个多态的通过一层层的调用扩开发的灵活性、可用性

  • EventLoop是如何保证单线程执行的
  • 如果要是BossGroup中包含多个EventLoop处理的机制是什么

2、通过引导类进行各种各样的配置,将若干个关键组件组合起来

  • 里面一些Factory是在什么地方使用的,做什么用的

3、引导类配置完成之后就会通过bind的形式进行地址绑定,绑定完成后需要开始进行程序的启动

4、启动完成之后开始开启循环进行监听

  • 在Boss中如果监听到连接时间是通过什么样的形式交给Work中的
  • 事件的注册流程是怎样的
  • 结合流程图中的三大步骤要梳理清楚关于select、process、allTask的三大方法是如何协调的

5、完成监听程序就开始执行pipleline的hander链

  • 如何开始调用handler链的
  • 调用的过程中如何进行数据传递
  • 心跳监听事件是依托handler实现的那么他的机制是怎么样的,里面几个关键的计算如何进行计算

二、核心源码分解

想要了解Netty之前最起码应该对Netty的体系有一个大概的了解,在前面一些列关于Netty的文章中都已经多多少少的提现过了,在理解Netty的时候主要类比于NIO实际上Netty的底层调用的就是NIO的接口。Netty与服务端的思想都是类似的这里主要就一服务端为例来讲。

使用Netty我们首先要定义Netty的基本组件并将他们组合,最后启用,组合的部分称之为引导类。在服务端创建组件的时候首相创建的是Boss的EventLoopGroup——用来监听连接事件、以及用来监听具体连接的EventLoopGroup这个是用来监听读写事件并执行具体任务的我们将之称为Worker。有关EventLoopGroup的创建过程是通过构造函数的多态来层层进行丰富的,EventLoopGroup可以想成是一个EventLoop数组,为了叙述方便接下来以EventLoop为讲述对象。

EventLoop是一个单线程或者说是一个单线程池模型,他里面有一个Selector用来监听注册与他的Channel,当监听到Channel的相关事件后他将会在当前的线程中执行监听到的事件。这里需要注意的是每个EventLoop都唯一绑定一个线程而每一个Channel都唯一的绑定一个Channel,这样做的好处就是可以避免在处理Channel事件的时候同步的问题以及线程切换带来的开销。有关EventLoopGroup创建的过程在他的专题中已经讲得非常详细了。在进行EventLoop进行创建的过程中经过一个重要的方法——initAndRegister,这个方法返回一个ChannelFutur,方法体中主要执行channel的初始化以以及channel与EventLoop的绑定——注册,注册的过程中产生了一个DefaultChannelPromise,入参就是两个要操作的对象——channel、SingleThreadEventLoop:

public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}

这里面的绑定绑定的主要是服务端的Channel,当这个线程绑定成功之后会有一个通知其他监听这个动作。在上面的过程中应该已经通过execute(Runnable task)触发了startThread()方法在这个方法中又会调用doStartThread();

private void doStartThread() {
    assert thread == null;
    executor.execute(new Runnable() {
        @Override
        public void run() {
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }

            boolean success = false;
            updateLastExecutionTime();
            try {
                SingleThreadEventExecutor.this.run();

14行的代码中调用的run方法在此场景最终的实现是NIOEventLoop在这里就是一个死循环,执行的操作主要分为

  • 监听事件 select(wakenUp.getAndSet(false));
  • 处理SelectorKey processSelectedKeys();
  • 执行任务runAllTasks();

到这里之后EventLoop就开始他的工作了,那么EventLoop的单线程又是如何实现的:

1、EventLoop为什么是单线程的

在SingleThreadEventExecutor的类中有doStartThread方法,这个方法中启动了一个线程,这个线程是一个Executor executor,他在EventLoop进行初始化的时候会进行赋值,在这个线程中执行了一些操作,其中一个关键的就是调用 SingleThreadEventExecutor.this.run();,在这个run方法中有Netty的关键三个步骤,在此线程中运行的这三个步骤主要是为与这个EventLoop绑定的Channel服务,由于这三大操作都是在这个线程种运行的,那么自然也就保证了他的单线程运行。上面说的这个Executor默认的是:

if (executor == null) {
    executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}

2、如果要是BossGroup中包含多个EventLoop处理的机制是什么

正常情况下Boss对应的EventLoopGroup我们初始化的时候都是指定为1、如果是1的话好理解那么如果不是1的时候也是类似于Worker的也是多个EventLoop呢。原来追踪源码的结果是他最终根据一定的算法确定了一个EventLoop,来对服务端的Channel进行绑定,回想一下Netty的设计模式也确实应该如此——每个Channel都智能对应一个EventLoop并且一直绑定。

上面主要讲述的是围绕EventLoop展开的,讲述了他的创建(在EventLoop专题中)创建完成之后实际上是会通过引导类进行一些类的绑定,这里关于引导类的绑定及不再多说了可以理解他是对服务的一些参数配置。当绑定服务的地址成功后线程就会通知所有的监听着,同时在这个过程中也会通过启动一下线程来对Channel进行监听以及处理事件。集合Netty的的线程模型在EventLoop的死循环中主要执行的三个步骤就是监听事件、处理监听到的时间、运行任务。

程序中事件的监听实际上用的也是NIO中的API接口,第一步调用的select方法,这里面做的工作是在规定的时间内看是否有事件,但是却并不会对事件做具体的处理,关于NIO中的selector方法要等待多长时间也是经过计算得出的:

long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
int selectedKeys = selector.select(timeoutMillis);

通过select监听到的事件可能是各种各样的,此时是通过processSelectedKeys方法来进行执行,在第一步的执行中应该是会跟新一下selectedKeys,这里面放的是监听到的事件,这个变量是一个类级别的变量,在processSelectedKeys中要做的就是遍历这个selectedKeys,根据item的类型就走不同的路径其中与NIO相关的是:

if (a instanceof AbstractNioChannel) {
    processSelectedKey(k, (AbstractNioChannel) a);
} else {
    @SuppressWarnings("unchecked")
    NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
    processSelectedKey(k, task);
}

进入到processSelectedKey就开始对事件的类型进行判断:

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
	ch.unsafe().forceFlush();
}
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
	unsafe.read();
}

这里面有一个Unsafe的概念这应该是与线程安全有关的,有关Unsafe相关的知识在线程的章节到时候在详细的去了解一下。到这里跟一下read方法看看Netty是如何调用Handler的。

do {
    byteBuf = allocHandle.allocate(allocator);
    allocHandle.lastBytesRead(doReadBytes(byteBuf));
    if (allocHandle.lastBytesRead() <= 0) {
        // nothing was read. release the buffer.
        byteBuf.release();
        byteBuf = null;
        close = allocHandle.lastBytesRead() < 0;
        if (close) {
            // There is nothing left to read as we received an EOF.
            readPending = false;
        }
        break;
    }

    allocHandle.incMessagesRead(1);
    readPending = false;
    pipeline.fireChannelRead(byteBuf);
    byteBuf = null;
} while (allocHandle.continueReading());

allocHandle.readComplete();
pipeline.fireChannelReadComplete();

if (close) {
    closeOnRead(pipeline);
}

在此循环中会不断的读直到allocHandle.continueReading()条件不满足,在循环调用的过程中将会调用fireChannelRead(byteBuf);这里面会调用到他对应的下一个同类型的Handler但是这里调用的fire方法应该调用到的是pipeline中的第一个对应的类型方法:

@Override
public final ChannelPipeline fireChannelRead(Object msg) {
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}

    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            ...............
        }
    }

private void invokeChannelRead(Object msg) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRead(this, msg);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelRead(msg);
        }
    }

通过invokeChannelRead方法的入参可以很好地看到他的入参直接就是pipeline的头结点,所以说通过pipeline调用的时候数据是流经整个handler链的,而通过Context类的方法调用的时候是从当前的Handler开始流经:

@Override
public ChannelHandlerContext fireChannelActive() {
    invokeChannelActive(findContextInbound());
    return this;
}

    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    }

哈哈兄弟们到这里不知道你们明白了没有反正我是明白了Handler之间的数据是如何调用的。在上面的文章中讲了时间的监听发现以及事件的处理,那么还设有一个关键的步骤就是处理任务队列中的任务,任务队列中的任务我们将他理解为是耗时的任务,这个耗时任务队列执行的周期占多长时间是可以通过参数设置的。

private volatile int ioRatio = 50;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
    try {
        processSelectedKeys();
    } finally {
        // Ensure we always run tasks.
        runAllTasks();
    }
} else {
    final long ioStartTime = System.nanoTime();
    try {
        processSelectedKeys();
    } finally {
        // Ensure we always run tasks.
        final long ioTime = System.nanoTime() - ioStartTime;
        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    }
}

ioRatio实际上设置的是一个比率,具体执行多长时间是根据单轮中processSelectedKeys执行的时间长短。**上面的变量命名虽然说是IO执行的比率但是实际计算的话是ioRatio越大则IO执行的时间相来说是越少的,**如果要是IORatio设置为100那么效果就是将任务队列中的任务执行完,否则的话按照下面的执行时间进行唤醒

for (;;) {
    safeExecute(task);
    runTasks ++;
    if ((runTasks & 0x3F) == 0) {
        lastExecutionTime = ScheduledFutureTask.nanoTime();
        if (lastExecutionTime >= deadline) {
            break;
        }
    }
    task = pollTask();
    if (task == null) {
        lastExecutionTime = ScheduledFutureTask.nanoTime();
        break;
    }
}

上面说到了关于任务队列的执行,这个任务的执行队列与定时任务的队列不属于一个队列,掉调用EventLoop的execute方法时候会将一个任务提交大taskQueue,提交此队列中的任务会在后面的runtask系列方法中执行,例如initAndRegister方法中的注册步骤就是提交到这个队列中执行的,在提交任务之前首先会检查当前的这个线程是否是属于EventLoop如果不是的话将会执行startThread();方法来启动当前EventLoop的线程,并在之后再去提交任务。

if (eventLoop.inEventLoop()) {
    register0(promise);
} else {
    try {
        eventLoop.execute(new Runnable() {
            @Override
            public void run() {
                register0(promise);
            }
        });
    } catch (Throwable t) {
        logger.warn(
                "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                AbstractChannel.this, t);
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

关于定时任务与EventLoop中的三大步骤是如何关联起来的这个确实是不清楚,个人猜测应该是通过Javajdk的一个定时框架同时与EventLoop共用一个单线程池Excutor。

有关Netty部分的源码剖解已经讲完自己对Netty的真个运行过程也有一个较为清晰的认识后面复习的时候最好是可以通过流程图或是时序图重新梳理一遍这样的记忆将会更加牢固。如果有幸看到这里的朋友有什么疑问的非常欢迎大家讨论,理越辩越明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值