Netty 源码分析之 一 揭开 Bootstrap 神秘的红盖头 (服务器端)

目录java

服务器端

在分析客户端的代码时, 咱们已经对 Bootstrap 启动 Netty 有了一个大体的认识, 那么接下来分析服务器端时, 就会相对简单一些了.
首先仍是来看一下服务器端的启动代码:

public final class EchoServer {

    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        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();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoServerHandler());
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

和客户端的代码相比, 没有很大的差异, 基本上也是进行了以下几个部分的初始化:

  1. EventLoopGroup: 不管是服务器端仍是客户端, 都必须指定 EventLoopGroup. 在这个例子中, 指定了 NioEventLoopGroup, 表示一个 NIO 的EventLoopGroup, 不过服务器端须要指定两个 EventLoopGroup, 一个是 bossGroup, 用于处理客户端的链接请求; 另外一个是 workerGroup, 用于处理与各个客户端链接的 IO 操做.

  2. ChannelType: 指定 Channel 的类型. 由于是服务器端, 所以使用了 NioServerSocketChannel.

  3. Handler: 设置数据的处理器.

Channel 的初始化过程

咱们在分析客户端的 Channel 初始化过程时, 已经提到, Channel 是对 Java 底层 Socket 链接的抽象, 而且知道了客户端的 Channel 的具体类型是 NioSocketChannel, 那么天然的, 服务器端的 Channel 类型就是 NioServerSocketChannel 了.
那么接下来咱们按照分析客户端的流程对服务器端的代码也一样地分析一遍, 这样也方便咱们对比一下服务器端和客户端有哪些不同的地方.

Channel 类型的肯定

一样的分析套路, 咱们已经知道了, 在客户端中, Channel 的类型实际上是在初始化时, 经过 Bootstrap.channel() 方法设置的, 服务器端天然也不例外.
在服务器端, 咱们调用了 ServerBootstarap.channel(NioServerSocketChannel.class), 传递了一个 NioServerSocketChannel Class 对象. 这样的话, 按照和分析客户端代码同样的流程, 咱们就能够肯定, NioServerSocketChannel 的实例化是经过 BootstrapChannelFactory 工厂类来完成的, 而 BootstrapChannelFactory 中的 clazz 字段被设置为了 NioServerSocketChannel.class, 所以当调用 BootstrapChannelFactory.newChannel() 时:

@Override
public T newChannel() {
    // 删除 try 块
    return clazz.newInstance();
}

就获取到了一个 NioServerSocketChannel 的实例.

最后咱们也来总结一下:

  • ServerBootstrap 中的 ChannelFactory 的实现是 BootstrapChannelFactory

  • 生成的 Channel 的具体类型是 NioServerSocketChannel.
    Channel 的实例化过程, 其实就是调用的 ChannelFactory.newChannel 方法, 而实例化的 Channel 的具体的类型又是和在初始化 ServerBootstrap 时传入的 channel() 方法的参数相关. 所以对于咱们这个例子中的服务器端的 ServerBootstrap 而言, 生成的的 Channel 实例就是 NioServerSocketChannel.

NioServerSocketChannel 的实例化过程

首先仍是来看一下 NioServerSocketChannel 的实例化过程.
下面是 NioServerSocketChannel 的类层次结构图:

clipboard.png

首先, 咱们来看一下它的默认的构造器. 和 NioSocketChannel 相似, 构造器都是调用了 newSocket 来打开一个 Java 的 NIO Socket, 不过须要注意的是, 客户端的 newSocket 调用的是 openSocketChannel, 而服务器端的 newSocket 调用的是 openServerSocketChannel. 顾名思义, 一个是客户端的 Java SocketChannel, 一个是服务器端的 Java ServerSocketChannel.

private static ServerSocketChannel newSocket(SelectorProvider provider) {
    return provider.openServerSocketChannel();
}

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

接下来会调用重载的构造器:

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

这个构造其中, 调用父类构造器时, 传入的参数是 SelectionKey.OP_ACCEPT. 做为对比, 咱们回想一下, 在客户端的 Channel 初始化时, 传入的参数是 SelectionKey.OP_READ. 有 Java NIO Socket 开发经验的朋友就知道了, Java NIO 是一种 Reactor 模式, 咱们经过 selector 来实现 I/O 的多路复用复用. 在一开始时, 服务器端须要监听客户端的链接请求, 所以在这里咱们设置了 SelectionKey.OP_ACCEPT, 即通知 selector 咱们对客户端的链接请求感兴趣.

接着和客户端的分析一下, 会逐级地调用父类的构造器 NioServerSocketChannel <- AbstractNioMessageChannel <- AbstractNioChannel <- AbstractChannel.
一样的, 在 AbstractChannel 中会实例化一个 unsafe 和 pipeline:

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    unsafe = newUnsafe();
    pipeline = new DefaultChannelPipeline(this);
}

不过, 这里有一点须要注意的是, 客户端的 unsafe 是一个 AbstractNioByteChannel#NioByteUnsafe 的实例, 而在服务器端时, 由于 AbstractNioMessageChannel 重写了newUnsafe 方法:

@Override
protected AbstractNioUnsafe newUnsafe() {
    return new NioMessageUnsafe();
}

所以在服务器端, unsafe 字段实际上是一个 AbstractNioMessageChannel#AbstractNioUnsafe 的实例.
咱们来总结一下, 在 NioServerSocketChannsl 实例化过程当中, 所须要作的工做:

  • 调用 NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER) 打开一个新的 Java NIO ServerSocketChannel

  • AbstractChannel(Channel parent) 中初始化 AbstractChannel 的属性:

    • parent 属性置为 null

    • unsafe 经过newUnsafe() 实例化一个 unsafe 对象, 它的类型是 AbstractNioMessageChannel#AbstractNioUnsafe 内部类

    • pipeline 是 new DefaultChannelPipeline(this) 新建立的实例.

  • AbstractNioChannel 中的属性:

    • SelectableChannel ch 被设置为 Java ServerSocketChannel, 即 NioServerSocketChannel#newSocket 返回的 Java NIO ServerSocketChannel.

    • readInterestOp 被设置为 SelectionKey.OP_ACCEPT

    • SelectableChannel ch 被配置为非阻塞的 ch.configureBlocking(false)

  • NioServerSocketChannel 中的属性:

    • ServerSocketChannelConfig config = new NioServerSocketChannelConfig(this, javaChannel().socket())

ChannelPipeline 初始化

服务器端和客户端的 ChannelPipeline 的初始化一致, 所以就再也不单独分析了.

Channel 的注册

服务器端和客户端的 Channel 的注册过程一致, 所以就再也不单独分析了.

关于 bossGroup 与 workerGroup

在客户端的时候, 咱们只提供了一个 EventLoopGroup 对象, 而在服务器端的初始化时, 咱们设置了两个 EventLoopGroup, 一个是 bossGroup, 另外一个是 workerGroup. 那么这两个 EventLoopGroup 都是干什么用的呢? 其实呢, bossGroup 是用于服务端 的 accept 的, 即用于处理客户端的链接请求. 咱们能够把 Netty 比做一个饭店, bossGroup 就像一个像一个前台接待, 当客户来到饭店吃时, 接待员就会引导顾客就坐, 为顾客端茶送水等. 而 workerGroup, 其实就是实际上干活的啦, 它们负责客户端链接通道的 IO 操做: 当接待员 招待好顾客后, 就能够稍作休息, 而此时后厨里的厨师们(workerGroup)就开始忙碌地准备饭菜了.
关于 bossGroup 与 workerGroup 的关系, 咱们能够用以下图来展现:

clipboard.png

首先, 服务器端 bossGroup 不断地监听是否有客户端的链接, 当发现有一个新的客户端链接到来时, bossGroup 就会为此链接初始化各项资源, 而后从 workerGroup 中选出一个 EventLoop 绑定到此客户端链接中. 那么接下来的服务器与客户端的交互过程就所有在此分配的 EventLoop 中了.

口说无凭, 咱们仍是以源码说话吧.
首先在ServerBootstrap 初始化时, 调用了 b.group(bossGroup, workerGroup) 设置了两个 EventLoopGroup, 咱们跟踪进去看一下:

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    super.group(parentGroup);
    ...
    this.childGroup = childGroup;
    return this;
}

显然, 这个方法初始化了两个字段, 一个是 group = parentGroup, 它是在 super.group(parentGroup) 中初始化的, 另外一个是 childGroup = childGroup. 接着咱们启动程序调用了 b.bind 方法来监听一个本地端口. bind 方法会触发以下的调用链:

AbstractBootstrap.bind -> AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister

AbstractBootstrap.initAndRegister 是咱们的老朋友了, 咱们在分析客户端程序时, 和它打过不少交到了, 咱们再来回顾一下这个方法吧:

final ChannelFuture initAndRegister() {
    final Channel channel = channelFactory().newChannel();
    ... 省略异常判断
    init(channel);
    ChannelFuture regFuture = group().register(channel);
    return regFuture;
}

这里 group() 方法返回的是上面咱们提到的 bossGroup, 而这里的 channel 咱们也已经分析过了, 它是一个是一个 NioServerSocketChannsl 实例, 所以咱们能够知道, group().register(channel) 将 bossGroup 和 NioServerSocketChannsl 关联起来了.
那么 workerGroup 是在哪里与 NioSocketChannel 关联的呢?
咱们继续看 init(channel) 方法:

@Override
void init(Channel channel) throws Exception {
    ...
    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;

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

init 方法在 ServerBootstrap 中重写了, 从上面的代码片断中咱们看到, 它为 pipeline 中添加了一个 ChannelInitializer, 而这个 ChannelInitializer 中添加了一个关键的 ServerBootstrapAcceptor handler. 关于 handler 的添加与初始化的过程, 咱们留待下一小节中分析, 咱们如今关注一下 ServerBootstrapAcceptor 类.
ServerBootstrapAcceptor 中重写了 channelRead 方法, 其主要代码以下:

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    child.pipeline().addLast(childHandler);
    ...
    childGroup.register(child).addListener(...);
}

ServerBootstrapAcceptor 中的 childGroup 是构造此对象是传入的 currentChildGroup, 即咱们的 workerGroup, 而 Channel 是一个 NioSocketChannel 的实例, 所以这里的 childGroup.register 就是将 workerGroup 中的摸个 EventLoop 和 NioSocketChannel 关联了. 既然这样, 那么如今的问题是, ServerBootstrapAcceptor.channelRead 方法是怎么被调用的呢? 其实当一个 client 链接到 server 时, Java 底层的 NIO ServerSocketChannel 会有一个 SelectionKey.OP_ACCEPT 就绪事件, 接着就会调用到 NioServerSocketChannel.doReadMessages:

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = javaChannel().accept();
    ... 省略异常处理
    buf.add(new NioSocketChannel(this, ch));
    return 1;
}

在 doReadMessages 中, 经过 javaChannel().accept() 获取到客户端新链接的 SocketChannel, 接着就实例化一个 NioSocketChannel, 而且传入 NioServerSocketChannel 对象(即 this), 由此可知, 咱们建立的这个 NioSocketChannel 的父 Channel 就是 NioServerSocketChannel 实例 .
接下来就经由 Netty 的 ChannelPipeline 机制, 将读取事件逐级发送到各个 handler 中, 因而就会触发前面咱们提到的 ServerBootstrapAcceptor.channelRead 方法啦.

handler 的添加过程

服务器端的 handler 的添加过程和客户端的有点区别, 和 EventLoopGroup 同样, 服务器端的 handler 也有两个, 一个是经过 handler() 方法设置 handler 字段, 另外一个是经过 childHandler() 设置 childHandler 字段. 经过前面的 bossGroup 和 workerGroup 的分析, 其实咱们在这里能够大胆地猜想: handler 字段与 accept 过程有关, 即这个 handler 负责处理客户端的链接请求; 而 childHandler 就是负责和客户端的链接的 IO 交互.
那么其实是不是这样的呢? 来, 咱们继续经过代码证实.

关于 bossGroup 与 workerGroup 小节中, 咱们提到, ServerBootstrap 重写了 init 方法, 在这个方法中添加了 handler:

@Override
void init(Channel channel) throws Exception {
    ...
    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;

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

上面代码的 initChannel 方法中, 首先经过 handler() 方法获取一个 handler, 若是获取的 handler 不为空,则添加到 pipeline 中. 而后接着, 添加了一个 ServerBootstrapAcceptor 实例. 那么这里 handler() 方法返回的是哪一个对象呢? 其实它返回的是 handler 字段, 而这个字段就是咱们在服务器端的启动代码中设置的:

b.group(bossGroup, workerGroup)
 ...
 .handler(new LoggingHandler(LogLevel.INFO))

那么这个时候, pipeline 中的 handler 状况以下:

clipboard.png

根据咱们原来分析客户端的经验, 咱们指定, 当 channel 绑定到 eventLoop 后(在这里是 NioServerSocketChannel 绑定到 bossGroup)中时, 会在 pipeline 中发出 fireChannelRegistered 事件, 接着就会触发 ChannelInitializer.initChannel 方法的调用.
所以在绑定完成后, 此时的 pipeline 的内如以下:

clipboard.png

前面咱们在分析 bossGroup 和 workerGroup 时, 已经知道了在 ServerBootstrapAcceptor.channelRead 中会为新建的 Channel 设置 handler 并注册到一个 eventLoop 中, 即:

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    child.pipeline().addLast(childHandler);
    ...
    childGroup.register(child).addListener(...);
}

而这里的 childHandler 就是咱们在服务器端启动代码中设置的 handler:

b.group(bossGroup, workerGroup)
 ...
 .childHandler(new ChannelInitializer<SocketChannel>() {
     @Override
     public void initChannel(SocketChannel ch) throws Exception {
         ChannelPipeline p = ch.pipeline();
         if (sslCtx != null) {
             p.addLast(sslCtx.newHandler(ch.alloc()));
         }
         //p.addLast(new LoggingHandler(LogLevel.INFO));
         p.addLast(new EchoServerHandler());
     }
 });

后续的步骤就没有什么好说的了, 当这个客户端链接 Channel 注册后, 就会触发 ChannelInitializer.initChannel 方法的调用, 此后的客户端链接的 ChannelPipeline 状态以下:

clipboard.png

最后咱们来总结一下服务器端的 handler 与 childHandler 的区别与联系:

  • 在服务器 NioServerSocketChannel 的 pipeline 中添加的是 handler 与 ServerBootstrapAcceptor.

  • 当有新的客户端链接请求时, ServerBootstrapAcceptor.channelRead 中负责新建此链接的 NioSocketChannel 并添加 childHandler 到 NioSocketChannel 对应的 pipeline 中, 并将此 channel 绑定到 workerGroup 中的某个 eventLoop 中.

  • handler 是在 accept 阶段起做用, 它处理客户端的链接请求.

  • childHandler 是在客户端链接创建之后起做用, 它负责客户端链接的 IO 交互.

下面咱们用一幅图来总结一下服务器端的 handler 添加流程:

clipboard.png

后记

这是 Netty 源码分析 系列教程的第一篇, 按个人计划, 这一篇文章是一个简述性质的, 即这里会涉及到 Netty 各个功能模块, 可是我只是简单地提了一下, 而没有深刻地探索它们内部的实现机理. 之因此这样作, 第一, 是由于若是一上来就从细节分析, 那么未免会陷入各类琐碎的细节中难以自拔; 第二, 我想给读者展现一个一个完整的 Netty 的运行流程, 让读者从一个总体上对 Netty 有一个感性的认识.
此篇文章涉及的模块比较多, 面比较广, 所以写起来不免有一点跳跃, 而且我感受写着写着见见有点不知所云, 逻辑混乱了, 汗. 唉, 仍是感受本身功力不够, hold 不住.
接下来的几篇文章, 我会根据 Netty 的各个模块深刻分析一下, 但愿之后的文章可以组织的调理更加清晰一些.

本文由 yongshun 发表于我的博客, 采用 署名-相同方式共享 3.0 中国大陆许可协议.
Email: yongshun1228@gmail.com
本文标题为: Netty 源码分析之 一 揭开 Bootstrap 神秘的红盖头 (服务器端)
本文连接为: http://www.javashuo.com/article/p-ngtqdeei-ho.html