Netty源码浅析

1,459 阅读20分钟

之前第一次接触就被这种处理I/O的方式惊艳到了,写Netty总有一种不是在写业务;而是真的在写代码的感觉!而且写过不少Reactor模型的代码,但基本都是Echo版本的,于是很想知道Netty到底封装了什么,为什么是很多高性能框架(SpringWebFlux,Dubbo,Lettuce)的选择?于是找个时间准备深究一下源码。

其实这源码也不是我强烈想看的,因为网上没找到什么文章和资源,官方文档也没啥好看的。除了之前看过的《Netty实战》这本书之外,就没了。于是决定看看实现来进一步理解它。

组织结构

Netty的源码组织结构如下:

image.png

(还好不复杂!(:p

Bootstrap

image.png

bootstrap包主要负责启动服务,比如BootStrap负责启动客户端连接服务;ServerBootStrap负责启动服务端监听服务;和我们传统Socket差不多的意思,所以并没有什么好谈的。源码相对简单: image.png

Buffer

image.png

buffer则是对于数据的操作,包括了对于直接内存的操作,对于缓冲数据的操作等。Netty把网络I/O的buffer和直接内存的buffer整合到了一起,这样我们在 处理数据时,可以比较好的利用这种抽象编写方便的代码。

Channel

image.png

channel则是定义了一些核心组件,比如对于连接的封装Channel,对于请求响应链的组合ChannelPipeline,以及对于Channel的轮询管理EventLoop/EventLoopGroup等。

Handler

image.png

为了方便我们的使用,Netty提供了一些开箱即用的Handler,比如Http编解码器,日志记录器等。

Resolver

一些解析器

Util

常用的工具类。但是有一个需要我们注意,就是concurrent包,他是对于java.util.concurrent的封装和扩展,方便我们在Netty使用线程来进行一些并发操作,以及对于线程的管理(包括对于轮询的管理)。Netty的高连接的一个很重要的原因之一就是它对于线程的活用,使之满足一个线程管理多个连接(如果是小连接的话甚至可以一个线程管理成千上万个连接)。

image.png

核心组件

EventLoop

如果你写过Reactor模型的Java NIO网络通信,你肯定知道,在NIO中,是一个线程对应一个Selector,一个Selector管理N个远程连接。

在NIO中,我们想要知道当前Selector管理的N个连接有没有就绪事件,需要调用select()方法,这个方法底层调用操作系统的select/poll/epoll来实现,当发生了就绪事件,方法从阻塞状态返回;然后轮询就绪列表,所以称为"Loop"。对于感兴趣的时间集合进行轮询就是EventLoop名称的由来。

NioEventLoop

在Netty中,实现了EventLoop接口的类有很多,这正是Netty封装底层差异的表现之一。除了特定于操作系统的,比如Linux的EpollEventLoop和macOS的Kqueue;我们一般使用NIO。是因为NIO屏蔽了系统差异,会自动根据当前程序系统选择特定于系统的最好的方式。 image.png

上三层都是JUC标准库提供的,下面都是Netty进行了封装。通过观察我们很容易发现,

  • 🍔EventExecutorGroup

首先我们分析EventExecutorGroup。它是一个扩展了JUC线程池操作的接口,主要做了三件事:

1⃣️添加了“优雅关闭”线程池的方法。

2⃣️实现了迭代器接口,并添加了next()方法,实现返回下一个EventExecutor的功能(就是迭代功能)。

3⃣️把submit()和execute()方法的返回值改写为Netty自己的Future(扩展了JUC的Future,添加了Netty自己的功能)。

  • 🍟EventExecutor

刚刚我们提到了EventExecutor接口,它是Netty中一个事件执行接口,主要提供判断当前执行线程是否是EventLoop线程的方法,还有parent()方法,返回管理它的EventExecutorGroup;通过重写next()使之返回自己。EventExecutorGroup负责对多个EventExecutor进行管理。

  • 🍕EventLoopGroup

此时我们发现了EventLoopGroup也是继承自EventExecutorLoop的,而它重写了next()方法来获得由他管理的EventLoop。此外,还添加了注册Channel的方法,当我们把Channel注册给EventLoopGroup时,EventLoopGroup会选取一个由它管理的EventLoop来负责对这个Channel进行事件轮询操作。

  • 🥪AbstractEventExecutor

那AbstractEventExecutor是干啥的?从继承关系看,我们知道它是EventExecutor的一个抽象类实现,其实查看源码可以看到它仅仅实现了部分功能image.png

比如说,它实现了next()方法,这个方法返回一个EventExecutor,也就是它自己;注意,我们要明确一件事,EventExecutor不一定要有Group对它管理。所以它的parent(EventExecutorGroup)可以是空的。

  • 🥙AbstractScheduledEventExecutor

则是为EventExecutor提供了定时调度的执行功能。

但是迄今为止,我们还是没有实现线程运行功能。

  • 🌮OrderedEventExecuto

来我们继续哈!OrderedEventExecutor只是一个标记接口,指明任务会按照提交顺序执行。

  • 🥘SingleThreadEventExecutor

而SingleThreadEventExecutor虽然实现了线程池的execute()方法,但是它的实现行为和我们预料的线程池实现有很大的不同,核心实现在execute()方法,我们来看看:

private void execute(Runnable task, boolean immediate) {
    boolean inEventLoop = inEventLoop();
    // 把任务添加到任务队列,任务队列里面的任务的执行,是子类实现的。
    // 子类会把任务队列里面的所有任务一口气全部运行完
    // 正常来说它的任务队列应该是空的才对,不应该存在任何任务。所以添加了一个方法体为空的任务在队列中实现占位符作用。
    // SingleThreadEventExecutor子类运行的run()方法内部实现是做网络轮询操作+运行任务队列所有tasks
    addTask(task);
    if (!inEventLoop) {
        // 当第一调用时,因为thread == null,所以会触发startThread()方法
        startThread();
        if (isShutdown()) {
            boolean reject = false;
            try {
                if (removeTask(task)) {
                    reject = true;
                }
            } catch (UnsupportedOperationException e) {
                    // The task queue does not support removal so the best thing we can do is to just move on and
                    // hope we will be able to pick-up the task before its completely terminated.
                    // In worst case we will log on termination.
            }
            if (reject) {
                reject();
            }
        }
    }
    
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0411f7f6044a4e64a415220b019d17b3~tplv-k3u1fbpfcp-watermark.image)
    // 当再次添加任务,唤醒执行线程
    if (!addTaskWakesUp && immediate) {
        wakeup(inEventLoop);
    }
}

然后看看startThread():

image.png

发现它调用了doStartThread()方法:

image.png

查看源码知道这个方法做的事情很单一,就是运行自定义的run()方法,并在运行之后关闭线程池。而且注意到这里设置了thread = Thread.currentThread();所以上述的execute()只会进入startThread()一次!

注意一下,这里还有一个小细节,就是SingleThreadEventExecutor.this.run()跑在了一个Runnable里,而这个Runnable提交到了executor来执行,这个executor是作为参数传递过来的;通过源码我们可以知道这个executor是MultithreadEventExecutorGroup提供的。这个executor的execute(task)会开辟新的线程去跑task所以这个Runnable是在新的线程执行的。那么处于这个Runnable里面的run()方法最终也是跑在了一个全新的线程中,但是因为run()自始至终只会被调用一次,所以每一个EventExecutor都只有一个新开辟的线程来执行逻辑操作。而这就实现了每个EventLoop对应一个线程的逻辑

总结一下就是:

  • 传参到EventExecutor实现类的executor(java.util.concurrent.Executor)的execute(Runnable)方法每次都会新建一个线程去执行Runnable。
  • 为了实现一个线程管理一个Selector和任务执行的目标,我们就不能放任任务(Runnable对象)每次都被提交给属性executor去执行(否则每次都会开辟线程),所以我们我们把任务放到了任务队列。
  • 但是任务队列的任务怎么被执行呢?答案是放在子类的run()方法去执行,里面有一个死循环,会不断重复:轮询网络I/O操作+执行全部任务队列的任务;这么一个过程。
  • 所以既然每次都会开辟一个新的线程,而run()恰好可以不停地处理I/O+任务队列,那就索性只把run()提交给executor去做,这样就只有一个线程,同时又实现了I/O+任务队列的逻辑。

注意啦!运行的这个run()这个可不是通过实现Runnable接口实现的,是SingleThreadEventExecutor自定义的抽象方法:

image.png

子类,比如NioEventLoop则负责实现这个抽象方法。通过debug我们确实看到了,这个SingleThreadEventExecutor.this.run()确实被NioEventLoop实现了,所以最终调用的是NioEventLoop的run()方法。

  • 🌯EventLoop

现在来看看EventLoop。EventLoop之于EventLoopGroup就像EventExecutor之于EventExecutorGroup。EventLoop重写了EventExecutor的parent(),返回管理自己的Group。

  • 🥗SingleThreadEventLoop

SingleThreadEventLoop就是倒数第二步了,它实现了注册逻辑register。

  • 🥫NioEventLoop

而我们需要的select()操作,打开Selector,设置连接感兴趣事件则是在NioEventLoop里面完成的。

前面我们提到,NioEventLoop实现了run()方法:

protected void run() {
    int selectCnt = 0;
    for (;;) {
        int strategy;
        strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
        switch (strategy) {
            case SelectStrategy.CONTINUE:
                continue;
            case SelectStrategy.BUSY_WAIT:
            case SelectStrategy.SELECT:
                long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                if (curDeadlineNanos == -1L) {
                    curDeadlineNanos = NONE;
                }
                nextWakeupNanos.set(curDeadlineNanos);
                try {
                    if (!hasTasks()) {
                        if (curDeadlineNanos == NONE) {
                            return selector.select();
                        }
                        long timeoutMillis = deadlineToDelayNanos(curDeadlineNanos + 995000L) / 1000000L;
                        return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
                    }
                } finally {
                    nextWakeupNanos.lazySet(AWAKE);
                }
            default:
        }
        selectCnt++;
        cancelledKeys = 0;
        needsToSelectAgain = false;
        final int ioRatio = this.ioRatio;
        boolean ranTasks;
        if (ioRatio == 100) {
            try {
                if (strategy > 0) {
                    processSelectedKeys();
                }
            } finally {
                // 确保总是可以运行所有的任务队列中的任务
                ranTasks = runAllTasks();
            }
        } else if (strategy > 0) {
            final long ioStartTime = System.nanoTime();
            try {
                // 这个函数会获取就绪事件集合并处理,在此不表
                processSelectedKeys();
            } finally {
                // 确保总是可以运行所有的任务队列中的任务
                final long ioTime = System.nanoTime() - ioStartTime;
                ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
            }
        } else {
            ranTasks = runAllTasks(0); // This will run the minimum number of tasks
        }
    }
}

image.png

通过继承关系可以看到,EventLoop的最终实现(包含那些特定于系统的)最终都是和NioEventLoop一样的继承层次。

这里需要明确一下,虽然NioEventLoop使用线程来执行所有的上述实际操作,但是这个线程不是它创建的,而是NioEventLoopGroup根据入参设定的线程数量生成如是数量的线程,然后绑定到每一个NioEventLoop上来实现的,详见下述源码。

EventLoopGroup

image.png

NioEventLoopGroup

同上面的原因,我们分析NioEventLoopGroup。

在这里我们很容易注意到,NioEventLoop的AbstractEventExecutor被替换成了AbstractEventExecutorGroup。也就是管理EventExecutor的Group的抽象实现。

  • 🍝AbstractEventExecutorGroup

AbstractEventExecutorGroup实现比较简单粗暴,它主要实现了submit()和execute()方法(这两个方法来自JUC)和优雅关闭操作。并且对于submit()和execute()的实现是通过简单的调用next()方法得到EventExecutor,然后通过EventExecutor来执行的。

通过查看源码,我们可以很容易了解这一点:

image.png

  • 🍜MultithreadEventExecutorGroup

通过理解NioEventLoop我们知道,抽象类实现的扩展是通过单线程模式的EventExecutor来实现的,那么对于“Group版”,既然它管理多个EventExecutor的话,那它肯定是多个单线程的集合,也就是多个线程。所以有了MultithreadEventExecutorGroup这个类。

MultithreadEventExecutorGroup定义了一个新的方法,叫做newChild()。这个方法作用是:返回一个和可以被next()方法返回的EventExecutor,同时这个方法会为每一个线程都调用一次。换句话说,对于MultithreadEventExecutorGroup管理的N个线程,我们通过newChild()方法返回N个EventExecutor,每一个对应一个线程。此方法需要子类重写

同时,它还有一个Chooser属性,用来通过自定义/默认策略从可用EventExecutor数组中选择一个作为next()方法的返回值。此外,它的迭代器方法返回一个不可更改的迭代器对象。

这个类的构造器实现很有意思。EventLoopGroup很大程度上依赖了这个构造器构造出来的一些数据。

首先来看看它的字段吧!

image.png

然后来看看它的构造器:

image.png

EventExecutor数组元素通过newChild()方法进行填充,这和我们上面分析一致。然后就是构建Chooser和不可变的用来做迭代的集合。

  • 🍲MultithreadEventLoopGroup

MultithreadEventLoopGroup则是实现了注册逻辑,对应了SingleThreadEventLoop的注册逻辑,它的注册逻辑就是通过调用next()方法获取EventLoop,然后注册到EventLoop来实现。

  • 🍛NioEventLoopGroup

NioEventLoopGroup则是只实现了newChild()方法,这就是我们前面说的创建EventExecutor并绑定一个线程的实现。本质上是由MultithreadEventExecutorGroup传递参数,由专门化的EventLoopGroup接收参数并生成特定于某一平台或方式EventExecutor。

image.png

小结

至此我们的EventLoop/EventLoopGroup讲完了,我们发现,Netty是这样实现的:

  • 封装JUC的Executors得到EventExecutor,它是事件执行的执行体,但是这个执行体实现略微特殊罢了。
  • 对于管理多个EventExecutor的想法,诞生出了EventExecutorGroup系列;但是Netty选择让EventExecutor继承自EventExecutorGroup的方式来实现这种组合操作,在开始阅读时有点头昏。
  • 但是目前我们只是封装了执行体,没有对其实现,而且我们知道Netty特定于网络操作,所以我们需要的事件应该是网络I/O事件才对,于是我们有了EventLoop和它对应的管理者EventLoopGroup。
  • EventLoop相比EventExecutor实现了多路复用轮询I/O事件的操作,使得我们可以拿EventExecutor去处理网络I/O;说白了就是扩展了EventExecutor。
  • 虽然EventExecutor可以执行Runnable,但是其实现是压入任务队列,然后在XxxEventLoop的run()方法出队所有tasks来实现对task的执行的,且XxxEventLoop的run()调用在新的线程,同时只会被调用一次,所以做到了一个EventExecutor拥有一个线程去执行所有给它的tasks的同时实现事件轮询。

ChannelInboundInvoker/ChannelOutboundInvoker

  • 🍣ChannelInboundInvoker/ChannelOutboundInvoker

这两个接口其实没什么好说的,但是因为他们是很重要的基础接口:

image.png image.png

所以我单独拎出来讲一下。ChannelInboundInvoker定义了一堆入操作,比如fireXxx,表示向入方向下一个触发(比如数据读取,异常捕获这种需要向下流传递的);而ChannelOutboundInvoker定义了一堆出操作,比如写操作,还有连接/绑定这种对外暴露的操作,以及要求从外界读的read()操作等。

Channel

  • 🍱Channel

Channel是一个比较抽象的接口,直接分析不好入手,不妨我们先看看继承层次,试着理一下:

E0C381A7-CCFA-488B-9CD9-6C0ADF349403.png

Channel把read操作和flush操作自己保留,把write和flush(空返回值版)扔给了Unsafe实现(Unsafe是它的内部接口)。

  • 🍫AbstractChannel

AbstractChannel则把大部分实现扔给了ChannelPipeline去实现。但是ChannelPipeline是它构造的,它会构造一个DefaultChannelPipeline实例,入参就是它自己(Channel实例)。

一般使用的构造器是这样的:

image.png

其中的parent一般指的是ServerSocketChannel,也即在服务端创建这个SocketChannel的ServerSocketChannel,在Netty就是封装的NioServerSocketChannel。

  • 🍩AbstractNioChannel

AbstractNioChannel则是进一步封装成了Nio相关的操作,包括提供NioUnsafe接口。

  • 🍪AbstractNioByteChannel

AbstractNioByteChannel则是提供了把I/O特定于ByteBuf的操作,以字节形式读取写入数据,属于进一步具体化。所以它实现了读写操作,这两个最基础的功能。

  • 🍯NioSocketChannel

NioSocketChannel可以看成是最后的交互实现了,我们可以通过看看构造器猜到一些用法:

image.png 其中SocketChannel回到了Java,它就是java.nio.channels.SocketChannel。此类更多是实现了关闭管道的操作。

而整个Channel链需要交互的根本——java.nio.channels.SocketChannel就是在这里提供的,我们来看看是谁调用了它的构造方法:

image.png

而这个accept()方法我们就相当熟悉了:

image.png

Channel.Unsafe

  • 🍿Channel.Unsafe

直接上继承层次,干脆明了:

36F79D5E-BB71-4251-9608-7A1272CD6707.png

首先Unsafe本身提供了一些直接与外界数据交互的方法,这些方法按理说只能被Netty内部调用,而不应该被用户代码调用。

image.png

  • 🥛NioUnsafe

其中呢,NioUnsafe扩展了Unsafe,它最主要的方法就是,提供了访问SelectableChannel的能力:

image.png

  • 🍵AbstractUnsafe

那AbstractUnsafe呢?它润色了很多主要的方法,比如刷新flush,写出write,注册register。但是这些方法的实现还是委托给子类来做的,它仅仅把需要刷新/写出的消息加入到刷新缓冲区,需要注册的Channel包装一下,然后调用子类的register来完成注册逻辑。

image.png

  • ☕️AbstractNioUnsafe

AbstractNioUnsafe则是实现了大部分逻辑,包括返回SelectableChannel的逻辑(实现很简单,直接返回javaChannel)。

  • 🍺NioByteUnsafe 而NioByteUnsafe则实现了重要的read()方法,负责把上一次读感兴趣的操作读到的数据放到ByteBuf中并fire到下一个Handler。注意,doBeginRead()负责设置当前Channel为读感兴趣,read()负责把读到的数据放到ByteBuf供使用。

小结

Channel系列,包括Channel.Unsafe都是一个很复杂的系列,因为它们封装了JDK的SocketChannel,而且涉及到真正的数据写入写出,Netty为了实现统一和方便,封装了太多的层次,每一个Abstract都有自己的职责和工作,每一个最终实现类都有自己被父类委托的方法,所以想要梳理清晰,最好结合继承层次一起分析。

ChannelPipeline

  • 🥟ChannelPipeline

首先看结构层次:

image.png

ChannelPipeline实现起来比较单一,因为它的实现类只有一个DefaultChannelPipeline。而且它的方法可以分为以下四类:

  • 1⃣️添加一个ChannelHandler

  • 2⃣️移除一个ChannelHandler

  • 3⃣️从头触发ChannelHandler链的fireXxx()方法

  • 4⃣️获取ChannelHandler/ChannelHandlerContext/Channel

  • 🦪DefaultChannelPipeline

我们直接查看DefaultChannelPipeline的源码,可以看到其实现是很简单的:

  • 1⃣️对于fireXxx操作,则是调用head节点的invokeXxx方法实现的,所以如果调用pipeline的fireXxx会触发从头开始的操作,而调用ChannelHandlerContext的fireXxx则会触发下一个Handler。
  • 2⃣️对于读、写、bind、connect等操作,则是调用tail节点的相关方法实现的,tail的write很好理解,毕竟tail在最后,你想写出数据,从最后节点刷新当然是正确的。但是tail的read操作可能不是那么好理解,它不是channelRead()操作,而是表示此时程序想要读,也就是注册读感兴趣事件;最终会变成调用head的read方法,而此时就会触发真正的读感兴趣事件的注册。

ChannelHandlerContext

  • 🍤ChannelHandlerContext

我们直接看继承层次图会更好理解:

image.png

源码这么写到:ChannelHandlerContext可以让当前ChannelHandler获得与它所在的ChannelPipeline和同属于同意Pipeline的其他Handler交互的能力。一般指的是与Pipeline中数据流方向上的下一个Handler交互的能力(其实就是那一堆fireXxx方法)。

其实我们可以看到,它提供的方法无非就是获取ChannelPipeline、ChannelHandler、Channel、EventExecutor、和重写了fireXxx这五个操作。就没了。然后就是一些继承而来的操作。ChannelHandlerContext如果单纯的看接口定义所能获得信息就是:提供了ChannelHandler与其他ChannelHandler和Channel与ChannelPipeline交互的操作

image.png

而ChannelHandlerContext很多关键操作是有AbstractChannelHandlerContext来实现的:

image.png

  • 🍙AbstractChannelHandlerContext

它把关键实现委托给invokeXxx()方法,在invokeXxx中的逻辑基本是一样的:获取下一个ChannelHandlerContext=>判断下一的context的EventExecutor(其实是ChannelHandler的EventExecutor)是否是轮询线程(Netty可以为每一个ChannelHandler指定一个EventExecutor)=>如果是,则直接调用下一个的read操作;如果不是,则在它(next)的eventExecutor中执行read()操作。

这里我想说一下关于为某一ChannelHandler自定义执行上下文的事情,如果为某一个ChannelHandler指定了EventExecutor的话,AbstractChannelHandlerContext在invokeXxx之前会加一个判断,因为Singlethread.this.thread在开辟I/O轮询线程时已经被设置为I/O轮询线程(且只会调用一次),所以调用executor.inEventLoop()时会得到false,然后就会在executor中执行,乍一看没毛病。但是如果你debug的话,会发现这个Handler(自定义了执行上下文的Handler)后面的所有Handler都会在新的线程上,而不会再切回到I/O线程,这是为什么呢?我明明没有为后面的开辟新的执行上下文啊?

答案还是executor的inEventLoop()方法,此时后面的Handler因为没有指定executor,所以它们的executor还是I/O线程,所以返回值为true,所以会直接在当前线程运行,注意⚠️,当前线程不是I/O线程,而当前线程早就因为前一个Handler的自定义执行器切换到了新的线程,所以它也会在新的线程执行。

因为一开始的HeadContext是在I/O线程跑的,所以会导致后面的所有Handler都会在它的线程上跑,除非有谁显示地切换了执行上下文。所以Netty的fireXxx方法会导致后面的Handler运行在当前Handler所处的执行上下文中

image.png

其他操作基本一样。

  • 🍡DefaultChannelHandlerContext

而AbstractChannelHandlerContext是没有和ChannelHandler相关的实现的,它把ChannelHandler的相关实现委托给了它的实现类去做,而在Netty中,实现类只有一个,就是DefaultChannelHandlerContext,所以它的实现也异常简单:

image.png

  • 🍧HeadContext/TailContext

除此之外,我们还有两个比较特殊的ChannelHandlerContext,我们之前说过,Netty是通过链表结构来组织ChannelHandler的。所以肯定要有首尾节点作为边界来进行圈定。所以还有两个特殊的实现,就是HeadContext和TailContext。

HeadContext实现了InboundHandler/OutboundHander接口,也继承了AbstractHandlerContext。其实我们可以猜猜,作为链表头,那肯定是要为读写负责的嘛,所以它肯定要作为双向BoundHandler存在,而且不出意外它还应该拥有真正从Channel读数据的能力,然后fire到后面的Handler;而且还要可以写数据,把数据真的写入到Channel。这是它身为头节点必须拥有的能力。

HeadContext的实现的方法分为三种:

  • 1⃣️直接忽略的,比如handlerAdded/Removed这种通知方法。
  • 2⃣️调用Channel.Unsafe实现的,比如bind,connect,read,write这种实质性的对应于JavaChannel的方法。
  • 3⃣️直接转发给下一个Handler的,因为头节点做界限和实际Channel作用,所以对于读到数据和写出数据,异常处理这种业务逻辑不应该属于它,所以直接fire给下一个。比如channelXxx方法。

TailContext就更简单了,它仅仅作为尾界定符的作用,所有的方法都是空(其实有些方法仅仅打了日志,约等于为空)。

小结

  • ChannelHandlerContext提供了ChannelHandler与同一ChannelPipeline下的后一个ChannelHandler交互的能力。
  • 对于把请求转发给链中下一个ChannelHandler的实现,则是通过判断执行上下文再调用下一个Handler的相关方法实现的。

ChannelHandler

  • 🍦ChannelHandler

这是一个ChannelHandler链的实际组成: image.png 因为涉及逻辑组成,所以又可以看成下面这种方式: image.png

直接看接口定义吧:

image.png

其实就俩方法,做回掉用的,一个是添加Handler,一个是移除Handler。

同时还有一个注解,@Shareable,指出这个Handler是否可以被共享,即,在多个Pipeline之间共享同一个实例。

image.png

  • 🥧ChannelHandlerAdapter

而ChannelHandlerAdapter主要添加了通过解析注解判断这个Handler是否是可共享的方法,实现很简单,就不说了。

image.png

ChannelHander我们不太好单独的拎出来说,因为实际使用时,一般要分入和出两个Handler:

image.png

image.png

ChannelInboundHandler

  • 🍰ChannelInboundHandler

如果说ChannelHandler提供了添加/移除ChannelHandler回掉的话,那么ChannelInboundHandler则提供了更细致的关于Channel的状态更改的回掉,说白了就是当Channel被注册,被激活,读到数据时,数据读取完时,遇到异常时,得到一个回调。

image.png

ChannelOutboundHandler

  • 🍭ChannelOutboundHandler

ChannelOutboundHandler则是会在I/O操作的出操作(bind,connect,write)可用时得到回调。

image.png

ChannelDuplexHandler

  • 🍬ChannelDuplexHandler

一个双向的Handler,同时拥有In/Out两种功能。

ByteBuf

ByteBuf是Netty数据缓冲,操作,读写的核心类,先挖个坑,日后再填ByteBuf相关的源码。

参考

暂无