Netty源码分析-服务端启动流程

作者: LemonNan
原文地址: https://juejin.im/post/6890768117803253768/

前言

距离上一次写 Netty 源码解析已经过去了一年多时间了, 最近这段时间回顾 Netty 的源码,目前看的版本是 4.1.31, 跟最开始看的版本不太一样, 没记错应该是4.1.16(吧), 不过这并不妨碍我们阅读源码, 小版本的源码在主流程上的差别不会太大. 这次分析的源码是用 Netty 启动一个服务端的过程.

流程说明

从 Netty 官方给的 demo 中入手, 下面的代码在 Netty项目中的 example 模块中的 io.netty.example.echo 包中.

Demo

      	// Configure the server.
        // 线程池初始化
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception{
                     ChannelPipeline p = ch.pipeline();
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(serverHandler);
                 }
             });

            // Start the server.
            // 等待启动完成
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            // 等待关闭
            f.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

这是启动一个简单的输入输出的服务端, 本来代码中还有个 SSL的配置, 在这里去掉了.

在对上面这段服务端启动代码进行解析前, 我们来回想一下用NIO写服务端的代码逻辑.

NIO流程

// 一共下来就是这几步
// 1.创建
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 2.绑定
serverSocketChannel.bind(new InetSocketAddress(port));
// 3.注册事件
this.selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

总的来说NIO的流程就是下面三步:

  • 创建 ServerSockerChannel 并设置为非阻塞(因为默认还是阻塞的) (create ServerSocketChannel)
  • 绑定端口 (bind)
  • 注册感兴趣事件 (register)

在清楚以前用NIO写的流程之后, 对 Netty 给的 echo demo 进行分析.

这里在 ChannelFuture f = b.bind(PORT).sync() 之前的代码, 是简单的用链式对服务端一些参数进行初始化, 等到 bind 的时候, 才开始做启动处理.

接下来分析 bind 方法.

bind

首先bind 的时候, 传递了一个端口号进去, 这个大家应该比较熟悉了.

public ChannelFuture bind(int inetPort) {
  return bind(new InetSocketAddress(inetPort));
}

接着继续调用

public ChannelFuture bind(SocketAddress localAddress) {
 	  // 对一些初始化参数的校验
    validate();
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    return doBind(localAddress);
}

validate()

参数校验方法 validate()

public B validate() {
    if (group == null) {
        throw new IllegalStateException("group not set");
    }
    if (channelFactory == null) {
        throw new IllegalStateException("channel or channelFactory not set");
    }
    return self();
}

首先是对 boss 线程池的校验, 然后校验 channel工厂的初始化, 这里分别对应的是初始化代码中的 b.group(bossGroup, workerGroup)What is it ?

继续看了下, 它是一个 Channel 的工厂, 应该就是创建 Channel 的, 在初始化参数的时候, 也有对 Channel 信息进行设置的代码 channel(NioServerSocketChannel.class)

channel(Class) 的代码

public B channel(Class<? extends C> channelClass) {
  if (channelClass == null) {
    throw new NullPointerException("channelClass");
  }
  return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}

最后是调用下面这个方法

public B channelFactory(ChannelFactory<? extends C> channelFactory) {
  if (channelFactory == null) {
    throw new NullPointerException("channelFactory");
  }
  if (this.channelFactory != null) {
    throw new IllegalStateException("channelFactory set already");
  }

  this.channelFactory = channelFactory;
  return self();
}

在这里可以看出, 这个channelFactory 是一个 ReflectiveChannelFactory .

接下来就是bind中调用的 doBind(localAddress) 方法

doBind

    private ChannelFuture doBind(final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }
				// ... 下面一堆操作后调用 doBind0(), 这里用 doBind0 来简化一下
        doBind0(regFuture, channel, localAddress, promise);
    }

这里为了代码容易看, 省略了中间的一些逻辑代码处理.

首先从名字上看, 首先是 初始化和注册 initAndRegister() , 判断是否异常, 接着调用 doBind0.

initAndRegister

final ChannelFuture initAndRegister() {
  		channel = channelFactory.newChannel();
  		init(channel);
  		ChannelFuture regFuture = config().group().register(channel);
  		return regFuture;
}

在这里同样将不少的代码都省略掉, 抽取主要流程.

  • channelFactory.newChannel() 创建 ServerChannel
  • init(channel) 初始化
  • 调用注册方法

创建 channel

这里看到是用 channelFactory.newChannel() 来进行 channel 的创建, 从上面已知道的代码中, 这个 Factory 是 ReflectiveChannelFactory, 查看它的 newChannel() 方法:

	@Override
	public T newChannel() {
        try {
            return clazz.getConstructor().newInstance();
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class " + clazz, t);
        }
    }

这里是根据 class 使用反射进行 Channel 的创建, 也就是我们一开始传进来的 NioServerSocketChannel.class

init 初始化

ServerBootstrap 的 init 方法

void init(Channel channel) throws Exception

个人感觉这个方法, 似乎没什么好讲的…

注册方法

register 这里调用到 SingleThreadEventLoop 的 register 方法, 最后会调用到 AbstractChannel 的 register(EventLoop, ChannelPromise) -> register0(ChannelPromise) -> AbstractNioChannel#doRegister()

在doRegister() 方法中, 最后会调用 java nio 的注册

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

到这里, 注册和初始化已经完成了, 接下来调用后面的 doBind0()

这里有一个流程的问题, 就是最后调用 doRegister() 方法的时候, 它的一些处理流程.

private void register0(ChannelPromise promise) {
	doRegister();
	pipeline.fireChannelRegistered();
	if (firstRegistration) {
		pipeline.fireChannelActive();
	}else{
    beginRead();
  }
}
  • 执行 java nio 的注册
  • 调用链的 registered 事件, 这是一个 inbound 事件
  • 调用 active 事件, 也是 inbound 事件, 并且同一个通道只会触发一次 active 事件, 同一个通道调用注销(deregister)事件并且再次注册, 不会触发多次 Active 事件, 但是 registered 会触发多次.

doBind0()

@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    return pipeline.bind(localAddress, promise);
}

doBind0() 中调用 AbstractChannel#bind -> pipeline#bind -> tail.bind -> invokeBind

private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
    if (invokeHandler()) {
        try {
          // 最后调到这里, 
            ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    } else {
        bind(localAddress, promise);
    }
}

如果你有自定义的 Handle 实现了 ChannelOutboundHandler 接口 的话, 最终事件就会传递到自定义 Handle的 bind 方法上.

在这所有 Handler 的 bind 都调用之后, 最终会调用到 Netty 自动为你加上的最外层的 HeadContext 的 bind 方法, 因为它既是 inbound 也是 outbound.

public void bind(
        ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
        throws Exception {
    unsafe.bind(localAddress, promise);
}

HeadContext 的 bind 方法 -> AbstractChannel#bind -> NioServerSocketChannel.doBind(这是其中一个子类的实现).

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

最终还是调用的 javaChannel() 的 bind 方法.

channel的创建、事件注册、绑定的流程到这里就结束了, 下面说的是一个关于 Netty 中另一个东西: Future.

sync()

上面 bind 后的 sync() 方法还记得吗?

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

sync() 的作用是将当前线程阻塞在这里, 直至服务端启动完成, 而服务端启动完成依赖一个东西, 没错就是 Future, 阻塞在这里的实现还算是比较简单的, 这里简单的用了循环判断, 最终调用代码如下:

    @Override
    public Promise<V> await() throws InterruptedException {
        if (isDone()) {
            return this;
        }

        if (Thread.interrupted()) {
            throw new InterruptedException(toString());
        }

        checkDeadLock();
        // 无限等待, 直至完成
        synchronized (this) {
            while (!isDone()) {
                incWaiters();
                try {
                    wait();
                } finally {
                    decWaiters();
                }
            }
        }
        return this;
    }

从这里可以看到如果调用 sync() , 最终调用到 DefaultPromise#await() 会一直阻塞当前调用到线程, DefaultPromise 中存储一个执行的结果 result, 并且它是用 volatile 来修饰的, 也就是其它线程修改了结果对其可见, 从最开始的 bind 一直往后的调用中, 相信大家也都看到了 Promise 一直在传递, 为的就是将结果设置进 Promise, 最后修改为成功 or 失败, 其它线程都能知道.

这里有一篇之前写的 Future/Promise 的源码解读

Netty 异步 Future 源码解读

最后

本篇关于Netty服务端的启动流程到这里就结束了.

去年

  • Netty异步Future源码解读
  • Netty高性能ByteBuf源码解析
  • Netty使用及事件传递

你可能感兴趣的:(java,Netty,nio,java,nio,netty)