Netty 服务端启动源码解析

netty 是一个网络通信框架,底层是 java 的 NIO。使用 netty 后开发者可以将重点都放在对业务的处理上。而它的启动流程是有固定的模板的。

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建负责接收客户端连接的事件循环组(公司的老板)
        EventLoopGroup boss = new NioEventLoopGroup(1);
        // 2. 创建负责处理客户端发送数据的事件循环组(员工)
        EventLoopGroup worker = new NioEventLoopGroup();
        // 3. 生成服务端启动器
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                // 4. 设置线程队列中等待连接的个数
                .option(ChannelOption.SO_BACKLOG, 128)
                // 5. 保持活动连接状态
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                // 6. 老板接待客户的地方,走流程签合同
                .handler(new BossChannelInboundHandler())
                // 7. 员工跟客户谈事情的地方
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new WorkerChannelInboundHandler());
                    }
                });
        // 8. 启动服务器
        ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
        channelFuture.channel().closeFuture().sync();
    }
}

而 Netty 底层实现用到的是 Java NIO。那么一个标准的 NIO 服务器启动模板是这样的。

private Selector selector;
private ServerSocketChannel serverChannel;
    
public ChatRoomServer() throws IOException {
    // 1. 获取 ServerSocketChannel
    this.serverChannel = ServerSocketChannel.open();
    // 2. 获取 Selector
    this.selector = Selector.open();
    // 3. 设置非阻塞
    serverChannel.configureBlocking(false);
    // 4. 注册感兴趣的时间并添加附加对象
    SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT,new Object()); 
    sk.
    // 5. 可以改变感兴趣的事件
    sk.interestOps(SelectionKey.OP_ACCEPT);
    // 6. 绑定端口
    InetSocketAddress addr = new InetSocketAddress(8888);
    serverChannel.socket().bind(addr); 
}

所以现在需要找到在 Netty 服务器启动的过程中,在哪里调用了 NIO 的底层 api。

流程分析

EventLoopGroup 是什么

EventLoopGroup 是一个存放 NioEventLoop 的集合,这个看代码里怎么设置参数的。一般 Boss 设置1,Worker 不设置默认就是机器 CPU 数量 * 2。这种设置参数用到的 Reactor 模型就是主从多线程模型,即 Boss 是主,Worker 是从。

NioEventLoop 里面维护了一个 tailTasks 无界任务队列LinkedBlockingQueue
Reactor 线程就是开启一个线程,做三件事情:

  1. 轮询注册事件。
  2. 处理 IO 事件。
  3. 处理任务队列里面的任务。
 NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    if (strategy == null) {
        throw new NullPointerException("selectStrategy");
    }
    provider = selectorProvider;
    final SelectorTuple selectorTuple = openSelector();
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;
}

这段 NioEventLoop 的构造器中这段代码final SelectorTuple selectorTuple = openSelector();就是获取到了一个包装后的 Jdk Selector。那么这就找到了一个调用点

源码跟进

serverBootstrap.bind(8888)这行代码开始跟进。到io.netty.bootstrap.AbstractBootstrap#doBind(final SocketAddress localAddress)里面可以看到两行重要的代码。

System.out.println("开始初始化和注册");
final ChannelFuture regFuture = initAndRegister();
// 绑定地址
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);
        }
    }
});

进入initAndRegister()方法,这里重点关注三行代码。

channel = channelFactory.newChannel();
init(channel);
ChannelFuture regFuture = config().group().register(channel);

第一行是新建一个 ServerSocketChannel,这里是通过反射的方式生成了一个 NioServerSocketChannel 实例。看看 NioServerSocketChannel 的构造方法。

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
private static ServerSocketChannel newSocket(SelectorProvider provider) {
    return provider.openServerSocketChannel();
}
public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = readInterestOp;
    ch.configureBlocking(false);
}
protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    System.out.println("创建 pipeline");
    pipeline = newChannelPipeline();
}

在 NioServerSocketChannel 的无惨构造器中,发现做了很多事情。

  1. 通过newSocket(DEFAULT_SELECTOR_PROVIDER)获取到了 ServerSocketChannel。这又是一个点
  2. AbstractChannel的构造方法中,创建了 Netty 的核心 pipeline。
  3. 然后设置ch.configureBlocking(false);非阻塞。至此 Nio 方式的前三步已经找到了。

接下来看init(Channel channel)初始化 Channel 的方法。这里面的核心是加了一个 ChannelInitializer。

p.addLast(new ChannelInitializer<Channel>() {
            @Override
    public void initChannel(final Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline(); ChannelInitializer<ServerSocketChannel>()
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }
        ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                pipeline.addLast(new ServerBootstrapAcceptor(
                        ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
    }
});

这段代码很眼熟,因为我们在写 Netty 的时候,也会自己写一个 ChannelInitializer,然后再里面加上自己的 ChannelHandler。这里也是做这样的事情,在pipeline.addLast(handler);体现出来的。其实ChannelInitializer就是一个辅助类。然后加了一个任务到任务队列中,这个任务就是为了添加一个ServerBootstrapAcceptor,这也是一个 ChannelHandler,要等到后面才真正添加。

然后就是开始注册。 ChannelFuture regFuture = config().group().register(channel);。一路跟进,找到io.netty.channel.AbstractChannel.AbstractUnsafe#register(EventLoop eventLoop, final ChannelPromise promise)方法。这里面的核心是register0(promise)。这里有个判断,如果 Reactor 线程开启了,那么就立即执行,如果没有就等加入任务后面在执行,总之是要执行的。那么看看加入任务的方法。

addTask(task);
if (!inEventLoop) {
    startThread();
}

将任务加入任务队列,然后判断当前线程是不是 Reactor 线程,如果不是就开启 Reactor 线程。再次跟进,到SingleThreadEventExecutor.this.run();,进入。此时找到io.netty.channel.nio.NioEventLoop#run。看核心代码

switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
    case SelectStrategy.CONTINUE:
        continue;
    case SelectStrategy.BUSY_WAIT:
        // fall-through to SELECT since the busy-wait is not supported with NIO
    case SelectStrategy.SELECT:
        select(wakenUp.getAndSet(false));
    default:
}
if (ioRatio == 100) {
    try {
        processSelectedKeys();
    } finally {
        runAllTasks();
    }
} else {
    final long ioStartTime = System.nanoTime();
    try {
        processSelectedKeys();
    } finally {
        final long ioTime = System.nanoTime() - ioStartTime;
        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    }
}

这段代码就是之前说的 Reactor 线程做的三件事。首先判断任务队列里有没有任务,如果有任务,那么先去执行selector.selectNow()看下有没有连接过来,立即返回。如果没有任务,那么就去轮询注册事件(这时候是没有任何 SocketChannel 注册进入 Selector 的),然后处理 IO 事件,然后再去执行任务队列,但是这时候的任务队列是同步执行的,是直接调用run而不是start

现在回顾刚才加入任务队列的任务register0(promise)。核心代码如下:

doRegister();
pipeline.invokeHandlerAddedIfNeeded();
safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (isActive()) {
if (firstRegistration) {
        pipeline.fireChannelActive();
    } else if (config().isAutoRead()) {
        beginRead();
    }
}

一行一行看,先看doRegister();这里面有一行关键性的代码。

 selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);

将之前的 ServerSocketChannel 注册进入了 Selector,并且附加对象是当前的 NioServerSocketChannel,但是关心的事件是0,就是什么都不关心。

接着看pipeline.fireChannelRegistered();。这里面是处理之前加入 pipeline 的 ChannelInitializer,通过调用它的initChannel方法将我们自己定义的 ChannelHandler 加入进去,然后移除掉自己,剩下的就是head--->myhandler--->tail。然后又通过任务的方式加入ServerBootstrapAcceptor到 pipeline 中。

safeSetSuccess(promise);是触发之前的回调监听,这样就可以去执行bind0方法了。bind0中主要就是添加一个任务。

 channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);

跟进。

public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    if (isNotValidPromise(promise, false)) {
        // cancelled
        return promise;
    }
    System.out.println("找出栈处理器,head");
    final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
    EventExecutor executor = next.executor();
    // 判断是否已经开启了 reactor 线程
    if (executor.inEventLoop()) {
        System.out.println("开启了 reactor 线程,立即执行真正的 bind");
        next.invokeBind(localAddress, promise);
    } else {
        System.out.println("没有开启 reactor 线程,加入执行真正的 bind 的任务,稍后执行");
        safeExecute(executor, new Runnable() {
            @Override
            public void run() {
                next.invokeBind(localAddress, promise);
            }
        }, promise, null);
    }
    return promise;
}

一路debug跟进,找到了真正的核心代码了。

public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    boolean wasActive = isActive();
    try {
        doBind(localAddress);
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }

    if (!wasActive && isActive()) {
        // 这里又加入了一个任务
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }
}

@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    if (PlatformDependent.javaVersion() >= 7) {
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}

这段代码是真正的 bind 方法了,但是还缺了一点,就是 selector 还没有感兴趣的时间。可以看到在执行完绑定方法之后,又加入了一个任务pipeline.fireChannelActive();。后面的同步执行任务的时候,会触发这个方法。这个方法是传播通道活跃事件的,

@Override
public void channelActive(ChannelHandlerContext ctx) {
    ctx.fireChannelActive();
    readIfIsAutoRead();
}

@Override
public void read(ChannelHandlerContext ctx) {
    unsafe.beginRead();
}

// io.netty.channel.nio.AbstractNioChannel#doBeginRead
protected void doBeginRead() throws Exception {
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }
    readPending = true;
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

io.netty.channel.DefaultChannelPipeline.HeadContext#channelActive跟进,找到了selectionKey.interestOps(interestOps | readInterestOp);。至此,Nio server 的设置已经全部找到了。

pipeline.fireChannelRegistered();就是调用 pipeline 的channelRegistered方法了,一层一层传播。

总结

说到底,启动流程就是开启一个 Reactor 线程去做三件事。轮询注册事件、处理 IO 事件和处理任务队列。任务队列里主要有这几件事:

  1. bind 任务。
  2. 添加 ServerBootstrapAcceptor 的任务。
  3. register0 任务。
  4. 绑定完成后的 pipeline.fireChannelActive() 任务。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值