解密Netty的TCP连接接入机制:Boss/Worker线程组协作与 OP_ACCEPT事件处理流程

583 阅读8分钟

前言

当今高并发网络通信领域,Netty凭借其卓越的性能和灵活的架构设计,已成为构建高性能网络服务的首选框架。本文将深入剖析Netty最核心的连接接入机制,揭示其如何通过精巧的线程模型设计和高效的Channel管理,实现高效的连接接入能力。

Netty的连接接入过程本质上是一个高度优化的流水线作业系统,其核心由三个关键组件协同完成:

  1. BossGoup线程组: 作为连接接入的第一道门户,专门负责监听和接收新的TCP连接请求并且以非阻塞方式轮询OP_ACCEPT事件。
  2. WorkerGroup线程组: 作为已接入连接处理的骨干力量,负责处理已建立连接的所有IO操作。每个Wokrer线程都维护着注册在自己身上的Channel集合,实现无锁化的串行处理。
  3. ServerBootstrapAcceptor: 作为连接接入流水线的"最后一公里处理器",这个组件实现了从连接接收到工作线程分发的完整闭环。

本文将解析Netty高效处理网络连接的两大关键设计:首先,将剖析Boss与Worker线程组的协同工作模式,揭示Netty如何通过主从Reactor架构实现连接接收与I/O业务处理的完美解耦;其次,详细讲解Boss线程组基于NIO Selector的OP_ACCEPT事件处理流程,展现其高性能的连接接入能力。这两大机制相辅相成,共同构成了Netty卓越的网络通信性能基础。

一、Boss/Worker线程组协作机制

Netty的Boss/Worker线程组协作机制是其高性能网络通信的核心设计之一,采用了主从Reactor模型,Netty的代码体现如下:

//Boss线程组作为主Reactor,用于监听指定端口的连接事件(OP_ACCEPT),在只监听一个端口的情况下设置为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//Wokrer线程组作为从Reactor,用于处理已经接入的连接Channel,负责Channel的读写事件,根据业务情况可以设置为多个,默认为CPU的个数
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
    ServerBootstrap b = new ServerBootstrap();
    //将Boss线程组和Wokrer线程组注册进去,来构建主从Reactor的基本架构,用于后续的Boss/Worker线程组协作
    b.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    、、、
    ChannelFuture f = b.bind(8888).sync();
    f.channel().closeFuture().sync();
} finally {
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
}

以上代码中,b.group(bossGroup, workerGroup)这行代码构成了Netty主从Reactor模式,后续Boss/Worker协作的线程都是来源于此处。

接下来我们结合源码来分析一下主Reactor的建立过程

主Reactor的启动由此开始,绑定指定的端口,随后便开始进行端口的监听,准备接入新的客户端连接

ChannelFuture f = b.bind(PORT).sync();

继续跟进代码,doBind这个方法核心的功能一个是初始化NioServerSocketChannel并且将NioServerSocketChannel注册到EventLoop中,这个EventLoop来自于bossGroup,另一个就是绑定到指定的端口。

private ChannelFuture doBind(final SocketAddress localAddress) {
    //初始化NioServerSocketChannel并且将NioServerSocketChannel注册到EventLoop中
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        ChannelPromise promise = channel.newPromise();
        //进行端口的绑定
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                Throwable cause = future.cause();
                if (cause != null) {
                    promise.setFailure(cause);
                } else {
                    promise.registered();
                    doBind0(regFuture, channel, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

initAndRegister这个核心方法也做了两件事情,一个是创建一个新的NioServerSocketChannel对象并且将其进行初始化,另一个就是将其注册到boss线程组中的一个eventLoop上去。这里有个关键点,就是当register这个注册方法执行完毕后,这个eventLoop才开始启动,其实也是属于懒加载机制。

final ChannelFuture initAndRegister() {
    Channel channel = null;
    //创建一个Netty的NioServerSocketChannel对象
    channel = channelFactory.newChannel();
    // 初始化这个ServerSocketChannel
    init(channel);
    //将NioServerSocketChannel注册到boss线程组中的一个eventLoop上去
    ChannelFuture regFuture = config().group().register(channel);

init(Channel channel)这个方法一方面是为NioServerSocketChannel配置一些参数,另一个重要的点就是给NioServerSocketChannel的ChannelPipeline增加一个关键的ChannelHandler(ServerBootstrapAcceptor)

void init(Channel channel) {
    ....省略一些代码
    //获取 NioServerSocketChannel的ChannelPipeline
    ChannelPipeline p = channel.pipeline();
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(final Channel ch) {
            final ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }
            //向pipeline添加ServerBootstrapAcceptor
            //这个处理器专门处理新连接接入
            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            ch, // 当前ServerChannel
                            currentChildGroup, // worker线程组
                            currentChildHandler, // 业务处理器
                            currentChildOptions, / 子Channel选项
                            currentChildAttrs));// 子Channel属性
                }
            });
        }
    });
}

ServerBootstrapAcceptor负责处理新连接的接收和初始化工作,这个组件串联起了Netty的主从Reactor模式,可以说是连接接入流水线的"最后一公里处理器",我们通过源码分析一下

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    //新接入的客户端SocketChannel
    final Channel child = (Channel) msg;
    //为客户端SocketChannel配置channelHandler
    child.pipeline().addLast(childHandler);
    //设置SocketChannel的选项和属性
    setChannelOptions(child, childOptions, logger);
    setAttributes(child, childAttrs);
    try {
        //将新连接注册到worker线程组
        //后续新连接的IO操作交由worker线程负责
        //至此,主Reactor的连接接入工作结束
        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);
    }
}

以上内容主要介绍了主Reactor的建立过程以及Boss/Worker线程是如何进行协作的,还有就是其中的一些关键方法和组件内容。简要概括的流程图如下

image.png

二、NIO Selector的OP_ACCEPT事件处理流程

Netty对NIO的OP_ACCEPT事件处理进行了高度优化和封装,其实本质上处理OP_ACCEPT事件的流程与处理网络IO读写的事件的流程是一样的,都封装在NioEventLoop中的run方法里面,NioEventLoop基于selector来监听获取就绪的事件,只不过根据不同的事件类型进行不同的处理流程,源码参考如下:

/**
 * 处理已选择的SelectionKey,执行相应的I/O操作
 * 
 */
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    // 获取底层操作的unsafe实例,当我们处理OP_ACCEPT时,ch为NioServerSocketChannel
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    ...省略部分源码
    try {
        // 获取就绪的操作集
        int readyOps = k.readyOps();
        ...省略部分源码
        //处理读/接受连接(OP_READ/OP_ACCEPT)事件
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            // 处理读事件或接受新连接
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        // 如果SelectionKey已被取消,安全地关闭通道
        unsafe.close(unsafe.voidPromise());
    }
}

根据以上的源码分析可知,unsafe.read()方法用于处理读事件或者接受新连接事件,当参数ch为NioServerSocketChannel时,unsafe实例为NioMessageUnsafe,参数ch为NioSocketChannel时,unsafe实例为NioByteUnsafe,所以不同实例的read方法走的流程也是不一致的,我们重点分析NioMessageUnsafe的read方法,这个是接受新连接的核心方法。源码如下:

public void read() {
    // 确保在EventLoop线程中执行
    assert eventLoop().inEventLoop();
    //获取通道配置
    final ChannelConfig config = config();
    //获取NioServerSocketChannel的ChannelPipeline
    final ChannelPipeline pipeline = pipeline();
    //获取内存分配控制器并且reset重置分配器状态
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.reset(config);
    boolean closed = false;
    Throwable exception = null;
    try {
        try {
            do {
                //核心的获取新连接的方法,是模版方法,执行的是NioServerSocketChannel的doReadMessages
                int localRead = doReadMessages(readBuf);
                // 无数据可读
                if (localRead == 0) {
                    break;
                }
                //NioServerSocketChannel的doReadMessages不会返回小于0的,忽略
                if (localRead < 0) {
                    closed = true;
                    break;
                }
                 // 更新读取消息计数
                allocHandle.incMessagesRead(localRead);
                //动态判断是否继续读取
            } while (continueReading(allocHandle));
        } catch (Throwable t) {
            exception = t;
        }
        //readBuf里面读取到的是新接入的NioSocketChannel,for循环依次处理fireChannelRead,并将NioSocketChannel作为参数传入
        //这里的pipeline是NioServerSocketChannel的pipeline,最终会让ServerBootstrapAcceptor去执行
        int size = readBuf.size();
        for (int i = 0; i < size; i ++) {
            readPending = false;
            pipeline.fireChannelRead(readBuf.get(i));
        }
        readBuf.clear();
        allocHandle.readComplete();
        //本次处理完成,触发fireChannelReadComplete事件
        //触发的也是NioServerSocketChannel的pipeline
        pipeline.fireChannelReadComplete();
        ...省略部分代码
    } finally {
        ...省略部分代码
    }
}

通过源码分析可知,新接入的连接NioSocketChannel会逐一的被NioServerSocketChannel的pipeline进行处理,最终会执行到ServerBootstrapAcceptor的channelRead方法上面,继而将NioSocketChannel绑定到Worker线程组中的线程上去,进行后续的IO读写操作。

我们再来看一下doReadMessages方法里面的内容,看一下是如何接入新连接的,源码分析如下:

protected int doReadMessages(List<Object> buf) throws Exception {
    //根据jDK原生的ServerSocketChannel来获取新接入的连接SocketChannel,SocketChannel也是JDK原生的
    SocketChannel ch = SocketUtils.accept(javaChannel());
    try {
        if (ch != null) {
            //将JDK原生的SocketChannel封装进Netty的NioSocketChannel中,便于后续的操作以及管理
            //这个this参数指的是当前的NioServerSocketChannel实例,用于表示当前的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;
}

以上这段源码向我们揭示了Netty是如何接入新连接的,底层也是依赖JDK原生的连接接入,只不过在Netty框架层面对这些原生的类进行了Netty层面的封装。

下面用一张图来简单概括一下Netty的NIO Selector的OP_ACCEPT事件处理流程

image.png

总结

以上内容主要结合源码分析了Boss/Worker线程组协作与 OP_ACCEPT事件处理流程,希望以上内容对你学习Netty有所助益,谢谢大家阅读。