Netty框架源码篇 - 服务端事件集处理


前言

回顾下<<Netty框架源码篇 - 深入分析服务端启动流程>> 这篇文章,其中分析到了一点,当对实例化的 Channel 进行注册时,由于发起注册的线程不是 EventLoop 所持有的线程,因此会调用 eventLoop.execute 方法提交注册任务。在执行 execute 方法过程中,其内部执行了 doStartThread 方法,而该方法内又提交了一个任务。当该任务执行时,会调用到 SingleThreadEventExecutor.this.run() 方法,而这里的 SingleThreadEventExecutor 实际上是其实现类 NioEventLoop,而 NioEventLoop 的 run 方法中,所做的事情就是不断的进行 Select 和事件集的处理以及其他系统任务的处理,这是前面分析的,本文将基于该基础上,对事件集处理进行详细分析

处理各种事件

public final class NioEventLoop extends SingleThreadEventLoop {
    ....
    @Override
    protected void run() {
        for (;;) {
            try {
                //通过 hasTasks() 判断当前任务列表中是否还有未处理的任务
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    //没有任务则执行 select() 处理网络IO
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                        // fall through
                    default:
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                //处理IO事件所需的时间和花费在处理 task 时间的比例,默认为 50%
                final int ioRatio = this.ioRatio;
                 如果 ioRatio 的比例是100,表示每次都处理完IO事件后,才执行所有的task
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    // 记录处理 IO 开始的执行时间
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        // 计算处理 IO 所花费的时间
                        final long ioTime = System.nanoTime() - ioStartTime;
                        // 执行 task 任务,判断执行 task 任务时间是否超过配置的比例,如果超过则停止执行 task 任务
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }
}

run 方法执行过程中,首先会调用 selectStrategy.calculateStrategy 判断当前 EventLoop 中是否有 Task 任务,如果有则调用 selectSupplier.get() 方法,该方法会阻塞,来获取需要处理的Channel。如果没有则返回 SelectStrategy.SELECT,并执行 select(wakenUp.getAndSet(false)) 方法,阻塞等待可处理的 IO 就绪事件

接下来,当有IO事件发生时,就会从 select 阻塞方法中唤醒,开始处理对应的 IO 事件。但是在处理前,会先判断 ioRatio 的比率值,该值为 EventLoop 处理 IO 任务和 处理 Task 任务的时间的比率,缺省比率为 50%

  1. 如果 ioRatio 为100,则说明优先处理所有的 IO 任务,处理完所有的IO事件后才会处理所有的 Task 任务
  2. 如果 ioRatio 小于 100, 则优先处理所有的IO任务,处理完所有的IO事件后,才会处理所有的Task 任务,但处理所有的 Task 任务的时候会判断执行 Task 任务的时间比率,如果超过配置的比率则中断处理 Task 队列中的任务

处理IO事件时,会执行 processSelectedKeys 方法,该方法内部最终会调用到 processSelectedKey(SelectionKey k, AbstractNioChannel ch) 方法,而该方法里面,完成了对发生的各种 IO 事件集进行了处理

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;
            }
            // close the channel if the key is not valid anymore
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
            //处理 OP_CONNECT
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }
            // 处理 OP_WRITE 事件
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                ch.unsafe().forceFlush();
            }
            //处理 OP_READ 或者 OP_ACCEPT 事件
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

服务端接收连接事件

处理 OP_ACCEPT 事件,会调用到 unsafe.read 方法,而对于服务端来说,这里的 unsafe 实现为 AbstractNioMessageChannel 内部的 NioMessageUnsafe 实例

private final class NioMessageUnsafe extends AbstractNioUnsafe {
        
        //用于存放与客户端通信的 SocketChannel
        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 {
                        //调用到 NioServerSocketChannel 实现类的该方法,将JDK原生的SocketChannel,包装成Netty的NioSocketChannel
                        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;
                    //触发父channel的ChannelRead方法,这里就会执行到之前添加到Pipeline里面的 ServerBootstrapAcceptor 该handler
                    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 {
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }
    }
}

NioMessageUnsafe 调用 read 方法处理客户端连接时,内部会定义一个 readBuf 集合用来存放与客户端通信的所有 SocketChannel 实例。然后,在处理时,考虑到可能存在多个客户端同时发起连接,因此,这里通过一个循环来处理所有的客户端连接。对于每一个连接,又会调用 doReadMessages(readBuf) 方法进行处理,而 doReadMessages 是抽象方法,会调用其子类。即NioServerSocketChannel 的doReadMessages 方法

public class NioServerSocketChannel extends AbstractNioMessageChannel
                             implements io.netty.channel.socket.ServerSocketChannel { 
    ....   
    @Override
    protected int doReadMessages(List<Object> buf) throws Exception {
        // 获取JDK 原生的 SocketChannel 
        SocketChannel ch = SocketUtils.accept(javaChannel());

        try {
            if (ch != null) {
                // 包装原生的SocketChannel ,创建一个 Netty 的NioSocketChannel,并添加到集合中
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
        } catch (Throwable t) {
            logger.warn("Failed to create a new channel from an accepted socket.", t);

            try {
                ch.close();
            } catch (Throwable t2) {
                logger.warn("Failed to close a socket.", t2);
            }
        }

        return 0;
    }
}
public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
        try {
            return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
                @Override
                public SocketChannel run() throws IOException {
                    return serverSocketChannel.accept();
                }
            });
        } catch (PrivilegedActionException e) {
            throw (IOException) e.getCause();
        }
    }

NioServerSocketChannel 调用 doReadMessages 方法时,基于每个客户端连接,通过原生的 ServerSocketChannel 调用 accept 方法,获取一个原生的 SocketChannel 实例。之后,再将该 SocketChannel 包装成Netty的 NioSocketChannel 实例。并将每一个 NioSocketChannel 维护在 readBuf 集合中

注意一点:实例化 NioSocketChannel 实例时,会在其父类 AbstractNioByteChannel 中传入其关注的事件 OP_READ,在更顶级的父类 AbstractNioChannel 中设置通道为非阻塞,在更顶级的父类 AbstractChannel 中为当前 NioSocketChannel 创建了对应的 Pipeline,即DefaultChannelPipeline,而DefaultChannelPipeline中又增加两个缺省的 handler,即 TailContext 和 HeadContext

接下来,回到 NioMessageUnsafe 的 read 方法中,当从循环中退出后,将对收集到每一个 NioSocketChannel 作为入参,调用当前 NioServerSocketChannel 对应的Pipeline 的 fireChannelRead方法,将channelRead 事件在该 Pipeline 上进行传播。由于 channelRead 属于入站事件,因此它会从当前 Pipeline 的 HeadContext 第一个入站处理器开始传播,当前 HeadContext 未做任何处理,因此就会传递给后一个 ServerBootstrapAcceptor 进行处理(这里为什么后一个是ServerBootstrapAcceptor ,可以看下前面那篇文章《Netty框架源码篇 - 深入分析服务端启动流程》)

 private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
        ....
        @Override
        @SuppressWarnings("unchecked")
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;
            //向子Channel的pipeline中,添加启动器配置的所有handler
            child.pipeline().addLast(childHandler);
            //设置子Channel的通信参数
            setChannelOptions(child, childOptions, logger);
            //设置子Channel的属性
            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }

            try {
                //将子channel注册到Selector选择器上,但未关注任何事件
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
        }
}

ServerBootstrapAcceptor 的 channelRead 方法,首先为每一个子Channel,即NioSocketChannel ,将启动服务端时配置的各种业务Handler添加到该Channel的对应的Pipeline中,同时再设置一些配置的通信参数和属性。接下来,将子Channel 进行注册,而注册的流程与服务端启动时,注册 ServerSocketChannel 的流程是一样的。到这,服务端接收连接事件的主要流程就完成了

可以看下下面的流程图,梳理下大概的流程:
在这里插入图片描述

写操作

//连接建立以后
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
	//发送消息
	ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty",CharsetUtil.UTF_8));
}

通常在我们编写业务handler时,会调用 ctx.writeAndFlush 方式向对端发送消息,那发送消息的过程是怎么样的呢?下面进行分析:

abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
        implements ChannelHandlerContext, ResourceLeakHint {
    ...
    @Override
    public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        }

        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return promise;
        }

        write(msg, true, promise);

        return promise;
    }
}

当调用 ctx.writeAndFlush 方法时,实际上会调用到 AbstractChannelHandlerContext 类的 writeAndFlush 方法,在该方法内部又会调用一个 write 方法,而该 write 方法有一个参数标识 flush,用于确认消息是否写入缓冲区同时刷往对端,还是只写入缓冲区

private void write(Object msg, boolean flush, ChannelPromise promise) {
        AbstractChannelHandlerContext next = findContextOutbound();
        final Object m = pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            if (flush) {
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        } else {
            AbstractWriteTask task;
            if (flush) {
                task = WriteAndFlushTask.newInstance(next, m, promise);
            }  else {
                task = WriteTask.newInstance(next, m, promise);
            }
            safeExecute(executor, task, promise, m);
        }
    }

对于 write 方法来说,首先,由于它是一个出站事件,所以会从当前的 ChannelHandlerContext 往前找,找到前一个出站handler来进行处理。然后,确认当前执行 write 方法的线程是否与当前出站 handler 对应的 Eventloop 所持有的线程一致,如果是同一个线程,则直接通过出站handler对应的ctx调用写操作。否则,将写操作封装成一个任务,然后提交到 Eventloop 内部的队列中,由 Eventloop 内部调度执行

private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
        if (invokeHandler()) {
            invokeWrite0(msg, promise);
            invokeFlush0();
        } else {
            writeAndFlush(msg, promise);
        }
}

当我们调用 writeAndFlush 方法时,就会执行这里的 invokeWriteAndFlush 方法,这里主要做两件事件:

  1. 将消息写入到 buffer 应用层缓冲里
  2. 将 buffer 缓冲信息写往对端
private void invokeWrite0(Object msg, ChannelPromise promise) {
        try {
            ((ChannelOutboundHandler) handler()).write(this, msg, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
}
 protected abstract class AbstractUnsafe implements Unsafe {
        ....
        @Override
        public final void write(Object msg, ChannelPromise promise) {
            assertEventLoop();

            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null) {
                safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
                // release message now to prevent resource-leak
                ReferenceCountUtil.release(msg);
                return;
            }

            int size;
            try {
                msg = filterOutboundMessage(msg);
                size = pipeline.estimatorHandle().size(msg);
                if (size < 0) {
                    size = 0;
                }
            } catch (Throwable t) {
                safeSetFailure(promise, t);
                ReferenceCountUtil.release(msg);
                return;
            }

            outboundBuffer.addMessage(msg, size, promise);
        }
}

当调用 invokeWrite0 方法写消息时,首先,由于 write 方法是一个出站事件,所以会通过出站 handler来调用 write 方法,而我们知道,在Pipeline中最后一个出站 handler 为 HeadContext,因此通常情况它就会调用到 HeadContext 的 write 方法,
而调用 HeadContext 的 write 方法,实际上又会调用到 AbstractUnsafe 类的 write 方法,而这里的 write 方法中,定义了一个 ChannelOutboundBuffer 缓冲,然后将消息写入到该缓冲。这一步操作,与我们在编写 NIO 代码时,定义一个 ByteBuffer,然后将消息写入该 ByteBuffer 里是一样的操作

private void invokeFlush0() {
        try {
            ((ChannelOutboundHandler) handler()).flush(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
}
protected abstract class AbstractUnsafe implements Unsafe {
        ....
        @Override
        public final void flush() {
            assertEventLoop();

            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null) {
                return;
            }

            outboundBuffer.addFlush();
            flush0();
        }
 
        @SuppressWarnings("deprecation")
        protected void flush0() {
            if (inFlush0) {
                // Avoid re-entrance
                return;
            }
            final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null || outboundBuffer.isEmpty()) {
                return;
            }
            inFlush0 = true;
            if (!isActive()) {
                try {
                    if (isOpen()) {
                        outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
                    } else {
                        outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
                    }
                } finally {
                    inFlush0 = false;
                }
                return;
            }
            try {
                doWrite(outboundBuffer);
            } catch (Throwable t) {
                if (t instanceof IOException && config().isAutoClose()) {
                    close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
                } else {
                    try {
                        shutdownOutput(voidPromise(), t);
                    } catch (Throwable t2) {
                        close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
                    }
                }
            } finally {
                inFlush0 = false;
            }
        }
}

当调用 invokeFlush0 方法刷新缓冲时,首先,对于 flush 方法它也是一个出站事件,所以也会通过出站 handler来调用 flush方法,因此通常情况就会调用到 HeadContext 的 flush 方法,而调用 HeadContext 的 flush 方法,实际上又会调用到 AbstractUnsafe 类的 flush0 方法,而这里的 flush0 方法,会先拿到前面调用 invokeWrite0 方法创建的 ChannelOutboundBuffer 缓冲,然后基于该缓冲,调用 doWrite 方法进行真正的写操作。而这里的 doWrite 方法,实际上会调用到 NioSocketChannel 的 doWrite 方法,具体的实现流程,看下面的代码注释

public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {
    ...
    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        //获取JDK 原生的SocketChannel 
        SocketChannel ch = javaChannel();
        // 获取缓存可写次数,缺省为 16次
        //这里为什么指定循环可写次数:因为一个线程对应一个EventLoop,而一个EventLoop负责多个channel,因此一个线程就负责处理多个channel的写事件了,而如果一个channel的写入消息非常多,如果不做控制,那么这个线程就会被这个channel一直占用,使得其它的channel无法执行操作了。因此为了,避免线程一直被某个channel的写事件占用,就通过循环可写次数进行了控制
        int writeSpinCount = config().getWriteSpinCount();
        do {
            // 确认发送缓冲消息是否为空,为空,表示消息都已经发送完成了
            if (in.isEmpty()) {
                // All written so clear OP_WRITE
                // 清除写事件
                clearOpWrite();
                // Directly return here so incompleteWrite(...) is not called.
                return;
            }

            // Ensure the pending writes are made of ByteBufs only.
            //最大字节数
            int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
            //获取发送缓冲中所有的ByteBuffer,在获取的时候会通过参数控制获取的 ByteBuffer的最大数量和最大字节数
            ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
            // 获取ByteBuffer总数
            int nioBufferCnt = in.nioBufferCount();

            // Always us nioBuffers() to workaround data-corruption.
            // See https://github.com/netty/netty/issues/2761
            switch (nioBufferCnt) {
                case 0:
                    // We have something else beside ByteBuffers to write so fallback to normal writes.
                    //除了 ByteBuffers 之外,我们还有其他要写的东西,因此可以回 
//退到普通写操作,它的意思是如果 nioBufferCnt 个数为 0,可能是其他内容的数据,比如 
//FileRegion,则调用父类 AbstractNioByteChannel 的 doWriter0 方法去写
                    writeSpinCount -= doWrite0(in);
                    break;
                case 1: {
                    // 创建JDK 原生的 ByteBuffer 
                    ByteBuffer buffer = nioBuffers[0];
                    int attemptedBytes = buffer.remaining();
                    //直接通过SocketChannel 进行写
                    final int localWrittenBytes = ch.write(buffer);
                    if (localWrittenBytes <= 0) {
                        //写入字节小于0,则需要注册写事件,并退出,等待下一次select时,继续写
                        incompleteWrite(true);
                        return;
                    }
                    //如果写入了一定量的数据,则调整最大字节数,并将已经写入的数据从发送消息缓存 ChannelOutboundBuffer 中移除,然后开始新的一轮循环写
                    adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
                    in.removeBytes(localWrittenBytes);
                    //写计数扣减
                    --writeSpinCount;
                    break;
                }
                default: {
                    //如果 ByteBuffer 个数大于 1,则使用 Nio 的 gather 聚合 Buffer 写入,一次就可以写入多个 ByteBuffer 的数据。在实现上,和只有一个 ByteBuffer 时没有太大差别
                    // Zero length buffers are not added to nioBuffers by ChannelOutboundBuffer, so there is no need
                    // to check if the total size of all the buffers is non-zero.
                    // We limit the max amount to int above so cast is safe
                    long attemptedBytes = in.nioBufferSize();
                    final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
                    if (localWrittenBytes <= 0) {
                        incompleteWrite(true);
                        return;
                    }
                    // Casting to int is safe because we limit the total amount of data in the nioBuffers to int above.
                    adjustMaxBytesPerGatheringWrite((int) attemptedBytes, (int) localWrittenBytes,
                            maxBytesPerGatheringWrite);
                    in.removeBytes(localWrittenBytes);
                    --writeSpinCount;
                    break;
                }
            }
        } while (writeSpinCount > 0);

        incompleteWrite(writeSpinCount < 0);
    }
}
//对于完成循环可写次数后的操作
protected final void incompleteWrite(boolean setOpWrite) {
        // Did not write completely.
        //还未写完成
        if (setOpWrite) {
            //注册一个写事件
            setOpWrite();
        } else {
            //清除写事件
            clearOpWrite();

            // Schedule flush again later so other tasks can be picked up in the meantime
            //稍后再安排刷新,以便在此期间可以处理其他任务
            //向eventLoop中提交一个刷新任务,由eventLoop内部进行调度执行
            eventLoop().execute(flushTask);
        }
}

解决JDK NIO空轮询BUG

private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

            for (;;) {
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
                // 轮询过程中发现有任务加入,中断本次轮询
                if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                int selectedKeys = selector.select(timeoutMillis);
                // select 次数 ++ , 通过该次数可以判断是否出现了 JDK NIO中的 Selector 空轮循 bug
                selectCnt ++;
                // 如果selectedKeys不为空、或者被用户唤醒、或者队列中有待处理任务、或者调度器中有任务,则break
                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    break;
                }
                if (Thread.interrupted()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely because " +
                                "Thread.currentThread().interrupt() was called. Use " +
                                "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                    }
                    selectCnt = 1;
                    break;
                }

                long time = System.nanoTime();
                //  如果超时,把 selectCnt 置为 1,开始下一次的循环
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    // timeoutMillis elapsed without anything selected.
                    selectCnt = 1;
                //  如果 selectCnt++ 超过 默认的 512 次,说明触发了 Nio Selector 的空轮训 bug
                //  则需要重新创建一个新的 Selector,并把注册的 Channel 迁移到新的 Selector 上
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    logger.warn(
                            "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                            selectCnt, selector);
                    //解决NIO Selector空轮询bug
                    rebuildSelector();
                    selector = this.selector;
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }

            if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                            selectCnt - 1, selector);
                }
            }
        } catch (CancelledKeyException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                        selector, e);
            }
        }
    }

在一个循环内部,每次都会调用 selector.select(timeoutMillis) 方法进行阻塞,直到有事件就绪时才会返回,每次调用 select 方法返回时,会执行 selectCnt++(通过该变量来记录发生了 Selector 空轮询的次数)。如果selectedKeys不为空,则退出循环,进行后续的 IO 事件处理。而如果selectedKeys为空,并且 selectCnt 次数超过SELECTOR_AUTO_REBUILD_THRESHOLD 阈值(缺省为 512),则调用 rebuildSelector 方法来进行重建Selector,以此来解决 NIO Selector空轮询bug

private void rebuildSelector0() {
        final Selector oldSelector = selector;
        final SelectorTuple newSelectorTuple;

        if (oldSelector == null) {
            return;
        }

        try {
            // 创建新的 Selector
            newSelectorTuple = openSelector();
        } catch (Exception e) {
            logger.warn("Failed to create a new Selector.", e);
            return;
        }

        // Register all channels to the new Selector.
        int nChannels = 0;
         // 循环原 Selector 上注册的所有的 SelectionKey
        for (SelectionKey key: oldSelector.keys()) {
            Object a = key.attachment();
            try {
                if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                    continue;
                }

                int interestOps = key.interestOps();
                //取消
                key.cancel();
                //将旧的 Selector上的channel,注册到新的Selector上
                SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                if (a instanceof AbstractNioChannel) {
                    // Update SelectionKey
                    ((AbstractNioChannel) a).selectionKey = newKey;
                }
                nChannels ++;
            } catch (Exception e) {
                logger.warn("Failed to re-register a Channel to the new Selector.", e);
                if (a instanceof AbstractNioChannel) {
                    AbstractNioChannel ch = (AbstractNioChannel) a;
                    ch.unsafe().close(ch.unsafe().voidPromise());
                } else {
                    @SuppressWarnings("unchecked")
                    NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                    invokeChannelUnregistered(task, key, e);
                }
            }
        }
        // 将新的 Selector 替换 原 Selector
        selector = newSelectorTuple.selector;
        unwrappedSelector = newSelectorTuple.unwrappedSelector;

        try {
            // time to close the old selector as everything else is registered to the new one
            //关闭旧的selector
            oldSelector.close();
        } catch (Throwable t) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to close the old Selector.", t);
            }
        }

        if (logger.isInfoEnabled()) {
            logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
        }
}

执行 rebuildSelector 方法过程中,首先,先创建一个新的 Selector,接下来,遍历将旧的 Selector 上注册的Channel以及监听的事件类型,重新注册到新的 Selector 上。注册完成后,将新的 Selector 替换掉旧的 Selector,最后再关闭旧的 Selector

Netty 的粘包/拆包处理

处理 OP_READ,会调用到 unsafe.read 方法,这里的 unsafe 实现为 AbstractNioByteChannel 内部的 NioByteUnsafe 实例

protected class NioByteUnsafe extends AbstractNioUnsafe {
        ....
        @Override
        public final void read() {
            final ChannelConfig config = config();
            if (shouldBreakReadReady(config)) {
                clearReadPending();
                return;
            }
            final ChannelPipeline pipeline = pipeline();
            final ByteBufAllocator allocator = config.getAllocator();
            final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
            allocHandle.reset(config);

            ByteBuf byteBuf = null;
            boolean close = false;
            try {
                do {
                    byteBuf = allocHandle.allocate(allocator);
                    //调用实现的doReadBytes方法,将Socket缓冲区中消息写入到当前 byteBuf 中
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));
                    //没有读到任何消息
                    if (allocHandle.lastBytesRead() <= 0) {
                        // nothing was read. release the buffer.
                        //释放byteBuf
                        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;
                    //传播ChannelRead事件
                    pipeline.fireChannelRead(byteBuf);
                    byteBuf = null;
                } while (allocHandle.continueReading());

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

                if (close) {
                    closeOnRead(pipeline);
                }
            } catch (Throwable t) {
                handleReadException(pipeline, byteBuf, t, close, allocHandle);
            } finally {
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }
}

NioByteUnsafe 调用 read 方法处理读事件时,首先会定义一个Netty 提供的 ByteBuf 字节缓冲实例,然后调用doReadBytes(readBuf) 方法进行处理,而 doReadBytes是抽象方法,会调用其子类。即NioSocketChannel的doReadBytes方法。该方法内部会将 Socket 缓冲区中的数据写入到该 ByteBuf 字节缓冲。之后,消息成功写入 ByteBuf 后,会向当前 Channel 对应的Pipeline传播 ChannelRead 事件。

而我们业务在处理 粘包/拆包 问题时,会引入指定的解码器,比如常见的将字节流转为对象的解码器,而在Netty中该类型对应的解码器都需要继承自 ByteToMessageDecoder (ByteToMessageDecoder 是一个入站处理器,提供了 ChannelRead 方法)。因此当传递 ChannelRead 事件时,就会执行到解码器的 ChannelRead 方法。Netty 在该 ByteToMessageDecoder 的 ChannelRead 方法中,就针对粘包和拆包做了处理

对于将 byte 转为 message 的问题在于,如何确定当前发送的数据包是否是一个完整的数据包。对于 Netty 它通过为每个监听的 Socket 都构建一个本地缓存,当前监听线程如果遇到字节数不够的情况就先将获取到的数据存入缓存,继而处理别的请求,等到这里有数据的时候再来将新数据继续写入缓存直到数据构成一个完整的包取出。这种方案来解决了这种问题。

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
    
    //保存累计读取到的字节流,会将新读取到的字节流追加到该实例中
    ByteBuf cumulation;
    //实现累计子节点的操作,有两种实现,分别为:MERGE_CUMULATOR和COMPOSITE_CUMULATOR
    private Cumulator cumulator = MERGE_CUMULATOR;
    //是否是第一次读取数据
    private boolean first;
    //控制可以累计读取的次数,超过该次数,会丢弃数据
    private int discardAfterReads = 16;
    //累计读取数据的次数
    private int numReads;
    ....

先看下 ByteToMessageDecoder 内部定义的属性。比较关键的是定义了一个 cumulation 累计区变量,该变量用于保存累次读取的字节,主要用于控制对未能成功解码的字节进行保存,然后与后续读取的字节进行整合,再重新进行解码

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            //用于保存成功解码后的对象,由于一个字节流可能会解析出多个对象,所以采用list进行存储
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                //是否第一次接收处理数据
                first = cumulation == null;
                if (first) {
                    //直接将接收的数据传递给 cumulation 变量
                    cumulation = data;
                } else {
                    //将新接收到的数据与cumulation 进行追加,由于ByteBuf 支持扩容,会在cumulation中剩余空间不足时,进行扩展来保证追加成功
                    //之后还会将 data 进行释放
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                // 对cumulation进行解码处理
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Exception e) {
                throw new DecoderException(e);
            } finally {
                //如果cumulation没有数据可读,而表示字节流已被解析成功,此时就会对cumulation进行释放
                if (cumulation != null && !cumulation.isReadable()) {
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                //累计读取次数,如果超过16次,就将cumulation中已经读取过的数据丢弃,重置 readIndex 读索引
                } else if (++ numReads >= discardAfterReads) {
                    // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                    // See https://github.com/netty/netty/issues/4275
                    numReads = 0;
                    discardSomeReadBytes();
                }
                //获得解码后的对象大小
                int size = out.size();
                decodeWasNull = !out.insertSinceRecycled();
                //向后一个入站handler进行传播
                fireChannelRead(ctx, out, size);
                out.recycle();
            }
        } else {
            //如果消息不是 ByteBuf 类型,直接交给Pipeline的下一个入站处理器进行处理
            ctx.fireChannelRead(msg);
        }
    }

调用 channelRead 方法过程中,首先,先定义了一个CodecOutputList 容器,用于存放解码成功后的消息对象,由于一个二进制字节流可能会被解码出多个消息对象,因此使用容器进行保存。接下来,确认是否是第一次接受数据,如果是,则直接将 data 字节缓冲赋值给 cumulation 累计区,否则,将 data 字节缓冲追加到 cumulation 累计区中。之后,调用 callDecode 方法传递 cumulation 累计区,尝试将累积区的字节数据进行解码。最后,如果累计区中没有未读的字节数据,则释放累计区。如果还有未读的字节数据并且累计读取次数超过了16次,则对累积区进行压缩。将累计区读取过的数据进行清空,并重置读索引。之后,调用 fireChannelRead 方法,将CodecOutputList 容器中的元素发送到后面的入站的 handler 进行处理,之后,再将 CodecOutputList 容器进行清空

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            //如果cumulation中有数据可读的话,一直循环
            while (in.isReadable()) {
                //获取out中元素数量
                int outSize = out.size();
                if (outSize > 0) {
                    fireChannelRead(ctx, out, outSize);
                    out.clear();
                    if (ctx.isRemoved()) {
                        break;
                    }
                    outSize = 0;
                }

                int oldInputLength = in.readableBytes();
                //实际上调用了具体子类的 decode方法,真正进行解码的操作,并将解码后消息放入到out集合中
                decodeRemovalReentryProtection(ctx, in, out);
                if (ctx.isRemoved()) {
                    break;
                }
                //当前调用 decode 方法未能解析出有效信息。
                if (outSize == out.size()) {
                    //解码前后的剩余可读字节数相同,则说明本次解码未能解析出完整消息,则退出循环
                    //可能剩余的二进制数据不足以构成一条消息
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                        continue;
                    }
                }

                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                                    ".decode() did not read anything but decoded a message.");
                }

                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Exception cause) {
            throw new DecoderException(cause);
        }
    }

调用 callDecode 方法过程中,会对 cumulation 累计区中二进制字节数据进行解码,将二进制字节数据解码成消息对象,并将解码后的消息对象存放到 out 容器中。而由于cumulation 累计区中的二进制字节数据可能会解码成多个消息对象,因此会不断循环判断 cumulation 是否有可读的字节,然后每次都会调用 decode 方法进行具体解码操作。

我们在覆写 decode 方法时,每次只会解析一个消息,添加到 out 容器中,callDecode 方法通过多次回调 decode 方法。每次传递进来都是相同的 out 实例,因此每一次解析出来的消息,都存储在同一个 out 实例中。当 cumulation 累计区中没有数据可读或者调用 decode 方法前后, out 容器中元素个数没有变化并且调用 decode 方法前后的可读字节数未发生变化, 则退出循环,结束方法调用

总结

本文详细分析了,服务端对接受连接的事件处理以及写操作的过程。至于客户端的连接以及读操作这些,实际上跟本文分析的内容是差不多的,可以结合这篇文章去看相关的代码,很容易就能理解的

由于本人能力有限,分析不恰当的地方和文章有错误的地方的欢迎批评指出,非常感谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值