Netty

先说下java的三种IO模型

BIO

BIO 同步阻塞, 一客户端对应一个处理线程, accept和read方法都是阻塞操作,如果没有连接请求,accept方法阻塞;如果无数据可读取,read方法阻塞

参考代码

NIO

NIO是同步非阻塞模型, 一个线程循环遍历,接入连接,然后分发给下面的线程

参考代码

NIO三大组件

  1. Buffer:用于存储数据,底层基于数组实现,针对8种基本类型提供了对应的缓冲区类。
  2. Channel:用于进行数据传输,面向缓冲区进行操作,支持双向传输,数据可以从Channel读取到Buffer中,也可以从Buffer写到Channel中。
  3. Selector:选择器,当向一个Selector中注册Channel后,Selector 内部的机制就可以自动不断地查询(Select)这些注册的Channel是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel,也可以说管理多个网络连接,因此,Selector也被称为多路复用器。当某个Channel上面发生了读或者写事件,这个Channel就处于就绪状态,会被Selector监听到,然后通过SelectionKeys可以获取就绪Channel的集合,进行后续的I/O操作

Epoll是Linux下多路复用IO接口select/poll的增强版本, 获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了
select和epoll的区别
当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另外的描述符虽然有数据但是不能读出来,这样实时性不能满足要求,大概的解决方案有以下几种:

  1. 使用多进程或者多线程,但是这种方法会造成程序的复杂,而且对与进程与线程的创建维护也需要很多的开销(Apache服务器是用的子进程的方式,优点可以隔离用户);
  2. 用一个进程,但是使用非阻塞的I/O读取数据,当一个I/O不可读的时候立刻返回,检查下一个是否可读,这种形式的循环为轮询(polling),这种方法比较浪费CPU时间,因为大多数时间是不可读,但是仍花费时间不断反复执行read系统调用;
  3. 异步I/O,当一个描述符准备好的时候用一个信号告诉进程,但是由于信号个数有限,多个描述符时不适用;
  4. 一种较好的方式为I/O多路复用,先构造一张有关描述符(FileDescriptor)的列表(epoll中为队列),然后调用一个函数,直到这些描述符中的一个准备好时才返回,返回时告诉进程哪些I/O就绪。select和epoll这两个机制都是多路I/O机制的解决方案,select为POSIX标准中的,而epoll为Linux所特有的。

它们的区别主要有三点:

  1. select的句柄数目受限,在linux/posix_types.h头文件有这样的声明:#define __FD_SETSIZE 1024表示select最多同时监听1024个fd。而epoll没有,它的限制是最大的打开文件句柄数目;
  2. epoll的最大好处是不会随着FD的数目增长而降低效率,在selec中采用轮询处理,其中的数据结构类似一个数组的数据结构,而epoll是维护一个队列,直接看队列是不是空就可以了。epoll只会对”活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数(把这个句柄加入队列),其他idle状态句柄则不会,在这点上,epoll实现了一个”伪”AIO。但是如果绝大部分的I/O都是“活跃的”,每个I/O端口使用率很高的话,epoll效率不一定比select高(可能是要维护队列复杂);
  3. 使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
    (1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

AIO

参考代码
AIO是异步非阻塞模型,一般用于连接数较多且连接时间较长的应用,在读写事件完成后由回调服务去通知程序启动线程进行处理。与NIO不同,当进行读写操作时,只需直接调用read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。

Netty

Channel - Socket
EventLoop - 控制流 多线程处理
ChannelFuture

  • Bootstrap or ServerBootstrap
  • EventLoop
  • EventLoopGroup
  • ChannelPipeline (提供了 ChannelHandler 链的容器)
  • Channel
  • Future or ChannelFuture
  • ChannelInitializer
  • ChannelHandler

Netty 包含两组线程池
BossGroup(接收客户端发来的链接) 和 WorkGroup(完成TCP三次握手等操作)

NioEventLoopGroup
线程池调度服务, 继承自ScheduledExecutorService, 包含了多个NioEventLoop,管理NioEventLoop的生命周期,
每个NioEventLoop 包含一个NIO selector 一个队列 一个线程, 线程用来轮询, 当注册一个Channel后, Netty将此Channel绑定到EventLoop上, io操作不需要同步,因为一直在一个线程上

Boss NioEventLoop

  1. 处理Accept事件,与client建立连接,生成NioSocketChannel
  2. 将NioSocketChannel注册到worker NioEventLoop上的selector
  3. 处理任务队列的任务, 即runAllTasks

Worker NioEventLoop

  1. 轮询注册到自己Selector上的所有NioSocketChannel的read和write事件。
  2. 处理read和write事件,在对应NioSocketChannel处理业务。
  3. unAllTasks处理任务队列TaskQueue的任务,一些耗时的业务处理可以放入TaskQueue中慢慢处理,这样不影响数据在pipeline中的流动处理。

ChannelPipeline
提供了 ChannelHandler 链的容器,消息在ChannelPipline中流动和传递, 持有I/O事件拦截器ChannelHandler的双向链表
ChannelPipeline是一系列的ChannelHandler实例,每当一个新的Channel被创建了,都会建立一个新的ChannelPipeline并绑定到该Channel上,这个关联是永久性的

当有新的ChannelHandler被添加时,则将其封装为ChannelHandlerContext对象,然后插入到链表中。

ChannelHandler
它充当了所有处理入站和出站数据的应用程序逻辑的容器。
@Sharable注解共享一个ChannelHandler在 多个实例中, 经常用来统计


public interface ChannelInboundHandler extends ChannelHandler {
    //当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
    void channelRegistered(ChannelHandlerContext ctx) throws Exception;
    //当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用
    void channelUnregistered(ChannelHandlerContext ctx) throws Exception; 
    //当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
    void channelActive(ChannelHandlerContext ctx) throws Exception;   
    //当 Channel 离开活动状态并且不再连接它的远程节点时被调用
    void channelInactive(ChannelHandlerContext ctx) throws Exception;  
    当从 Channel 读取数据时被调用
    void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;   
    //当 Channel上的一个读操作完成时被调用
    void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
    void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception; 
    void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

ChannelHandlerContext
负责在链上传播责任处理器接口的事件。 它有两个重要的方法,查找Inbound类型和Outbound类型的处理器。


abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {
    
    volatile AbstractChannelHandlerContext next;
    volatile AbstractChannelHandlerContext prev;
    
    //查找下一个Inbound类型的处理器,左 > 右
    private AbstractChannelHandlerContext findContextInbound(int mask) {
        AbstractChannelHandlerContext ctx = this;
        EventExecutor currentExecutor = executor();
        do {
            ctx = ctx.next;
        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
        return ctx;
    }
    //查找下一个Outbound类型的处理器,右 > 左
    private AbstractChannelHandlerContext findContextOutbound(int mask) {
        AbstractChannelHandlerContext ctx = this;
        EventExecutor currentExecutor = executor();
        do {
            ctx = ctx.prev;
        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
        return ctx;
    }
}


ctx.fireChannelRead(msg); 责任链一直调用下去

ByteBuf

ChannelPipeline 包含ChannelHandlerContext(ChannelHandler) 链表

状态

自己实现Netty

初始化
EventLoopGroup
多个EventLoop(newTaskQueue selector (selectorKeys))

 
 private ChannelFuture doResolveAndConnect(SocketAddress remoteAddress, final SocketAddress localAddress) {
        // 初始化并注册
        final ChannelFuture regFuture = initAndRegister();
        if (regFuture.cause() != null) {
            return regFuture;
        }
        
        final Channel channel = regFuture.channel();
        final EventLoop eventLoop = channel.eventLoop();
        final NameResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);

        if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
            // Resolver has no idea about what to do with the specified remote address or it's resolved already.
            return doConnect(remoteAddress, localAddress, regFuture, channel.newPromise());
        }

        final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);
        final Throwable resolveFailureCause = resolveFuture.cause();

        if (resolveFailureCause != null) {
            // Failed to resolve immediately
            channel.close();
            return channel.newFailedFuture(resolveFailureCause);
        }

        if (resolveFuture.isDone()) {
            // Succeeded to resolve immediately; cached? (or did a blocking lookup)
            return doConnect(resolveFuture.getNow(), localAddress, regFuture, channel.newPromise());
        }

        // Wait until the name resolution is finished.
        final ChannelPromise connectPromise = channel.newPromise();
        resolveFuture.addListener(new FutureListener<SocketAddress>() {
            @Override
            public void operationComplete(Future<SocketAddress> future) throws Exception {
                if (future.cause() != null) {
                    channel.close();
                    connectPromise.setFailure(future.cause());
                } else {
                    doConnect(future.getNow(), localAddress, regFuture, connectPromise);
                }
            }
        });

        return connectPromise;
    }

posted @ 2020-08-17 20:24  jojoworld  阅读(179)  评论(0)    收藏  举报