Java-第十七部分-NIO和Netty-Netty源码

158 阅读8分钟

NIO和Netty全文

Netty源码

  • io.netty.example下有案例

启动过程

  • 分析echo案例
  • SSL配置 image.png

group

  • 创建两个group对象,并进行配置 image.png
  • 调用group初始构造,会传递0参 image.png
  • 默认为设备核数*2 image.png
  • group下的EventExecutor数组,跟其中的NioEventLoop元素,都有一个父接口EventExecutor image.png

EventLoop

  • 最终生成group中的NioEventLoop的函数解析
MultithreadEventExecutorGroup(int nThreads, //线程数
Executor executor, //如果为null,默认为ThreadPerTaskExecutor
EventExecutorChooserFactory chooserFactory, //单例DefaultEventExecutorChooserFactory
Object... args //创建执行器的时候传入的参数
  • 生成executor image.png
  • children数组,创建NioEventLoop image.png
  • 如果创建失败,有异常,要全部关闭 image.png
  • 为每一个NioEventLoop增加一个管理监听器 image.png
  • 所有单例线程放到Setimage.png

bootstrap

  • group方法,将bossgroup交给父类AbstractBootstrap,将workergroup交给自己ServerBootstrap image.png
  • channel方法,通过AbstractBootstrapReflectiveChannelFactorychannelFactory反射创建类NioServerSocketChannel image.png
  • option方法将参数放在Map<ChannelOption<?>, Object>中管理 image.png
  • handler方法,AbstractBootstrap绑定到bossgroup,与ServerSocketChannel相关 image.png
  • childHandlerServerBootstrap绑定到workergroup,与SocketChannel相关 image.png

bind

  • bind方法中进行通道绑定 image.png
  • doBind中调用 image.png
  • initAndRegister创建NioServerSocketChannel image.png
  • newChannel()创建了唯一的ChannelIDNioMessageUnsafe操作消息,DefaultChannelPipelinepipelineNioServerSocketChannelConfig配置对象
  • init(chanel)同步处理options,初始化channelPipepline,添加ChannelIntiailizer,主要功能和pipeline进行关联 image.png
  • 绑定端口调用DefaultChannelPipeline中的unsafe.bind(),调用AbstractBootstrapbind方法,然后调用NioServerSocketChanneldoBind,再调用ServerSocketChannel的方法,最后调用Net的本地方法bind image.png image.png image.png
  • 绑定完端口后,执行safeSetSuccess告诉promise任务完成 image.png
  • 最后进入NioEventLooprun方法,进行事件监听 image.png
  • select事件 -> process处理IO事件 -> runAllTasks处理其他事件 image.png

pipeline

  • addLast时,会先生成context image.png
  • addLast最终执行,插在tail前面 image.png

请求过程

  • 接受连接
  • 创建NioSocketChannel
  • 注册到workerGroup
  • 注册select事件

客户端请求

  • 当有客户端请求时,服务端select该事件,processSelectedKey处理该事件,并检测出标识位为16accept事件 image.png image.png
  • AbstractNioMessageChannel类的unsafe调用read方法,并断言是否为当前线程 image.png image.png
  • 调用doReadMessages,通过NioServerSocketChannel,使用SocketUtils获取请求,并包装成NioSocketChannel放到ArrsyListimage.png
  • pipeline.fireChannelRead,开始依次调用pipeline中的handler,去处理 image.png
  • head开始调用 image.png image.png
  • 会反复执行invokeChannelRead image.png
  • 进入ServerBootstrap的内部类ServerBootstrapAcceptor,并将该请求的channel注册到workergroup image.png

register

  • register轮询使用线程,其中的next()方法,使用&的方式,决定使用一个线程 image.png
  • 包装成DefaultChannelPromise image.png
  • 调用AbstractChannel中的register0 image.png
  • 调用AbstractChannel中的doRegister,实际上由AbstractNioChannel实现 image.png
  • AbstractNioChannel调用doRegister完成最终的注册 image.png
  • 在第一次注册时,会调用pipeline.invokeHandlerAddedIfNeeded();,通过自定义的ChannelInitializerhandler添加 image.png
  • 注册完成后,进行该连接对应的pipeline的注册,及handler的注册 image.png
  • AbstractNioChanneldoBeginRead,为新连接的通道注册感兴趣的事 image.png

pipeline源码

  • ServerSocket创建一个新的连接,就会创建一个Socket,对应目标客户端
  • 当创建一个Socket就会分配一个全新的ChannelPipeline
  • 每一个ChannelPipeline都是双向链表,内部包含多个ChannelHandlerContext
  • 每个ChannelHandlerContext,包装一个ChannelHandler
  • 当一个请求进来时,进入Socket对应的pipeline,经过所有的handler,也就是责任链/过滤器模式

ChannelPipeline

  • 继承关系,接口多继承 image.png
  • 可以对链表进行操作,addLast/addFirst/addBefore...,遍历链表继承了接口Iterable`
  • 可以调用数据出入站的相关方法,继承了ChannelInboundInvoker/ChannelOutboundInvoker
  • headoutbound和inbound,但是inbound设置为false,只为了能够接受入站信息,然后传给下一个inbound的handler image.png image.png
  • tailinbound image.png

出入站调用关系

  • 入站操作时,通过调用fireChannelRead,将数据传给下一个handler image.png
  • findContextInbound通过while循环找到一个inbound的context image.png
  • ctx.write调用出站时,会直接找当前handler的前一个outboundhanlder image.png
  • findContextOutbound,从tail往前找一个outbound的context image.png

ChannelHandlerContext

  • 继承关系 image.png
  • ChannelInboundInvoker/ChannelOutboundInvoker针对出入站的过程,在出入站handler在包装一层,在触发handler前后做一些特定操作
  • ChannelHandlerContext增加了能够获取上下文环境中的channel/executor/handler/pipeline...

ChannleHandler

  • handlerAdded,被添加是调用;handlerRemoved,被移除时调用,而在连接注册成功之后,才会真正的为这个生成pipeline-handler的双向链表,handler才会被添加 image.png
  • ChannelDuplexHandler处理入站/出站IO事件

生成过程

  • 在反射创建Channel,会调用AbstractChannel的构造函数,创建pipeline image.png
  • 创建pipeline image.png image.png
  • 生成DefaultChannelPipeline,生成链表头尾;同时为当前pipeline绑定channel;生成futurepromise用于异步回调使用 image.png
  • 在完成channel注册之后,会为pipeline进行注册 image.png
  • addLast的过程中,会为handler进行包装,包装成HandlerContext,为了防止多线程导致安全问题,需要同步执行;如果没有被注册,会通过CAS的方式进行安全注册 image.png

调度流程

  • 入站从head开始 image.png
  • 执行read,最终需要inbound类的handler调用处理 image.png
  • 出站从tail开始调用,从内部像外面写 image.png

read流程

  • 流程图 IMG_BDBEDE330A4C-1.jpeg
  • fireChannelRead,从head开始 image.png
  • invokeChannelRead image.png
  • 调用真正的handler执行read动作要做的事 image.png
  • 通过前一个handler执行ctx.fireChannelRead(msg);,找到下一个inboundHandler,继续执行invokeChannelRead image.png image.png

心跳包源码

  • 三种handler能够检测连接的有效性
  1. IdleStateHandler当连接空闲时间太长,触发一个IdleStateEvent,可以通过重写userEventTrigged进行业务处理
  2. ReadTimeoutHandler,继承IdleStateHandler,在指定的时间没有发生读事件,抛出异常,并自动关闭这个链接 image.png
  3. WriteTimeoutHandler,当调用write时,创建一个定时任务,根据传入的promise的完成情况,判断是否超出写时间。当一个写操作不能再一定的时间内完成,抛出异常,并自动关闭这个链接 image.png
  • IdleStateHandler四个主要属性
private final boolean observeOutput; //是否考虑出站慢的情况,默认为false;网络不稳定,传输慢
private final long readerIdleTimeNanos; //读事件空闲时间,0禁用
private final long writerIdleTimeNanos; //写事件空闲时间
private final long allIdleTimeNanos; //读或写事件空闲时间
  • ticksInNanos返回当前纳秒时间,十亿分之一秒 image.png
  • initialize方法,将state状态设置为1,防止重复初始化,initOutputChanged出入站数据监控 image.png image.png
  • IdleStateHandler被添加时,会调用initialize方法,只要参数大于0,就为其创建定时任务 image.png image.png
  • schedule定时任务,添加到当前任务队列 image.png

定时任务类

  • 共同父类AbstractIdleTask,通道关闭直接返回,run(ctx)由子类实现 image.png
  • AllIdleTimeoutTask
  • ReaderIdleTimeoutTask
  • WriterIdleTimeoutTask

ReaderIdleTimeoutTask

  • 读时间时,记录第一次发生事件的标识位,后面判断是否是某次读事件后的第一次空闲;读完成后,reading设置为false,并记录最近的read时间 image.png
  • 当定时任务触发时,如果读完成后,!reading检查有效,会检查上一次read完成的时间,与现在的差值,与设置的空闲时间的差值,如果差值为负数,则重设定时任务,并抛出读空闲事件 image.png
  • 否则,仅仅重新设置 image.png
  • channelIdle抛出事件,触发用户自定义的userEventTriggered image.png image.png

WriterIdleTimeoutTask

  • lastWriteTime通过监听器,监听写事件 image.png
  • 逻辑大致相同,但是需要处理缓慢出站的情况 image.png
  • hasOutputChanged,检查缓冲区,如果是某次出站事件后的第一次空间则必须触发,如果出站缓慢有可能造成OOM image.png
  • 通过缓冲区的情况进行检查 image.png

AllIdleTimeoutTask

  • 计算离读写两个发生的最近的时间 image.png

EventLoop

  • 第一次添加时,启动run方法
  • 继承关系,单线程的 image.png
  • ScheduledExecutorService定时任务接口
  • EventLoop接口,一旦channel被注册,就处理其IO操作
  • SingleThreadEventExecutor单个线程线程池

启动EventLoop

  • 对于ServerSocketChannel来说,第一次注册过程时,会调用execute的方法,创建新的EventLoop,并启动起run方法 image.png
  • 调用父类SingleThreadEventExecutor中的execute image.png
  • startThread image.png
  • doStartThread启动线程,并进入run方法 image.png
  • run方法,select事件 -> process处理IO事件 -> runAllTasks处理其他事件 image.png image.png
  • execute中,如果线程停止,并且删除任务失败,则执行拒绝策略;如果线程休眠或者任务不是NonWakeupRunnable,则唤醒线程 image.png
  • addTask,如果线程关闭,拒绝,否则,添加到队列中 image.png image.png

三个主要过程

  • run中的select image.png
  • 默认阻塞一秒,如果有定时任务,则在定时任务剩余时间的基础上+0.5秒 image.png
//满足以下条件直接break
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
    // - Selected something, //有事件
    // - waken up by user, or //被用户唤醒
    // - the task queue has a pending task. //任务队列有任务
    // - a scheduled task is ready for processing //有定时任务
    break;
}
  • 定时任务添加0.5秒 image.png
  • processSelectedKeys image.png
  • processSelectedKeysOptimized遍历selectedKeys.keys,处理完后置空 image.png
  • processSelectedKey(SelectionKey k, AbstractNioChannel ch)根据时间类型判断 image.png
  • 根据ioRation执行runAllTasks,如果任务队列中的任务比较多,ioRation调小,就能多执行任务队列中的任务 image.png

异步任务源码

  • 通过eventLoop提交的异步任务,实际上是同一个线程排队执行
ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName());
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello client", StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

handler提交

  • 异步线程池
//异步线程池,可以将任务提交到该线程
final static EventExecutorGroup group = new DefaultEventExecutorGroup(16);
  • 提交
group.submit(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        //接受客户端信息
        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        Thread.sleep(10000);
        System.out.println(Thread.currentThread().getName());
        System.out.println(Thread.currentThread().getId());
        System.out.println(new String(bytes, CharsetUtil.UTF_8));
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello client", StandardCharsets.UTF_8));
        return null;
    }
});
  • submit添加任务,最终也会进入addTask的逻辑中 image.png
  • AbstractChannelHandlerContextwrite中,当执行下一个outbound的线程不是当前线程时,会将该任务封装成task,放入IO线程队列中,等待IO任务执行完毕后,执行该任务 image.png image.png

context添加

  • 线程池,轮询使用,handler正常方式写业务
static final EventExecutorGroup group = new DefaultEventExecutorGroup(2);
复制代码
  • handler添加时,将handler添加到线程池中,不添加时优先使用IO线程
p.addLast(group, new EchoServerHandler());
  • handler添加到group中之后,后续出入站方法都会在另一个线程异步执行,最终handler执行的是当前group线程中的task image.png