java I/O体系总结(五)netty架构浅析

2,077 阅读7分钟

简介

netty是使用java编写的高性能IO框架,旨在为高并发场景提供支持。netty可提供多种IO模型的支持,如OIO,NIO等。一般来说,非阻塞IO更适合于大规模高并发场景,我们使用netty主要也因为其封装了原生NIO,规避了其中复杂易出错的细节,更加易用、通用。

从示例讲起

netty既然是以java NIO为基础构建的(当然添加了大量特性),那就不能不了解java NIO的处理方式。NIO实现非阻塞的关键在于Selector(选择器)以及通道。下面先复习一下nio的示例,然后再对比netty。

java nio 示例


public void start() throws IOException {
         ServerSocketChannel ssc = ServerSocketChannel.open();
        Selector selector = Selector.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        // ①将服务器的channel注册到选择器
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            try {
                //阻塞,至少一个连接到来时才会继续
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();
                    //  连接进入
                    if (key.isAcceptable()) {
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        // ② 服务器接受连接,创建客户端的channel,然后注册到选择器(Selector)
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        // ③ 客户端的channel
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int count = sc.read(byteBuffer);
                        if (count < 0) {
                            key.cancel();
                            sc.close();
                        } else {
                            byteBuffer.flip();  //切换到读模式
                            String msg = Charset.forName("UTF-8").decode(byteBuffer).toString();
                            System.out.println("received from: " + msg);
                            sc.write(ByteBuffer.wrap(msg.getBytes(CharsetUtil.UTF_8)));
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

示例很简单,就是该服务器接受来自客户端的连接,并打印客户端的信息。解释如下:

  1. selector为选择器,即可以把要关注的事件注册到这里来。待该事件发生时,可给与通知。如将表示与服务器相连的serverSocketChannel注册,等有新连接过来后(accept事件),会通知该channel。
  2. ①处即为向选择器注册服务器channel及需关注的accept事件。
  3. ②处为向选择器注册接收的客户端channel,及关注的read事件
  4. ③处为客户端channel的read事件,处理read事件
  5. 从上面我们看出,selector注册了两种channel。一种是服务器channel,一种是客户端channel。前者只有一个,后者却很多,来一个请求便创建一个。且后者是前者在②处创建出来的。这两种channel有种父子关系的特征,后面netty就是用了这种概念表示。

下面看看netty的示例。学之前以为netty的非阻塞是以nio为基础创建的,应该差不多。看过来发现,果然,一点也不一样。

public class NettyServer {

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        final ByteBuf buf = Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8"));
        try {
            //服务端的引导类
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 设置线程组
            serverBootstrap.group(group)
                    // 设置非阻塞channel
                    .channel(NioServerSocketChannel.class)
                    // 设置绑定本地的端口
                    .localAddress(new InetSocketAddress(8080))
                    // 设置
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                @Override
                                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                    ByteBuf byteBuf = (ByteBuf) msg;
                                    String text = CharsetUtil.UTF_8.decode(byteBuf.nioBuffer()).toString();
                                    System.out.println("接受到的消息:" + text);
                                }
                            });
                        }
                    });
            ChannelFuture f = serverBootstrap.bind().sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

解释一下

  1. netty和java原生nio实现方式相当不一样,它将nio和oio的实现方式做了统一,所以,上面非阻塞式的代码,只需改动一点即可实现oio的方式。
  2. 从概念上来讲,Bootstrap(引导器)的说法在java nio上是没有的,它相当于一个用于集成引导配置的容器。有ServerBootstrap(用于服务器)和BootStrap(用于客户端)。
  3. EventLoopGroup和EventLoop很重要。EventLoopGroup用于管理多个EventLoop,而EventLoop关联一个线程。同时EventLoop又充当选择器(Selector)的角色。用于选取已注册的准备好的事件。
  4. 还有一个点是childHandler,用于设置处理接收而来的客户端channel。而handler,则用与设置服务器Channel。

netty流程浅析

你可以以理解java nio的方式理解netty。ServerBootstrap作为服务端的引导类,作用为串联配置,启动服务器。EventLoop是netty中的重要部件,有java nio中的选择器的功能,可以选择就绪的channel,且自身关联一个Thread。看下图

这里写图片描述

这图是从《netty实战》中找的,可以简单概括出EventLoopGroup、EventLoop以及Channel的关系。

EventLoopGroup可在创建时指定EventLoop的个数,如图中为3个。同时,EventLoopGroup负责为每个新创建的Channel(客户端Channel)分配一个EventLoop。一般采用顺序循环的方式分配。如此,客户端连接一多,每个EventLoop就会负责多个Channel。EventLoop本身还关联着一个Thread。负责处理Channel的读或写等事件。每个Channel的整个生命周期的事件均由其关联的EventLoop的线程处理,这样可避免多线程环境下数据同步等问题。

对比java nio的选择器模型,可以发现一些相似之处。这里的selector同样负责多个channel的事件处理。

这里写图片描述

当channel的某个事件准备好后,就可以根据业务需要处理这些数据了(或读或写等)。netty的处理流程对应的是一个处理链。ChannelPipeline。处理链上可添加若干个单个处理逻辑:ChannelHandler。这种处理方式使得处理逻辑简单清晰(如可将处理编解码的handler和序列化以及处理业务逻辑的代码分离开)。并且当需要改变处理流程时(如出站数据需要进行加密),只需动态添加(或移除)一个ChannelHandler即可。

这里写图片描述

图中直观显示了ChannelPipeline和ChannelHandler的关系。上面示例中,设置childHandler即可设置一个ChannelHandler。

启动流程

ServerBootstrap作为server端的引导器,是串联整个流程的关键。前面也说过,netty的引导器分2种,服务端的(ServerBootStrap)和客户端的(Bootstrap)。其类继承关系如图

这里写图片描述

这里写图片描述

可见两者均继承了AbstractBootstrap,这里只分析ServerBootStrap。

ServerBootStrap的group方法用于设置EventLoopGroup。上面示例中类似于这样设置的。

group(new NioEventLoopGroup())

看其源码

    @Override
    public ServerBootstrap group(EventLoopGroup group) {
        return group(group, group);
    }

以及

   /**
     * Set the {@link EventLoopGroup} for the parent (acceptor) and the child (client). These
     * {@link EventLoopGroup}'s are used to handle all the events and IO for {@link ServerChannel} and
     * {@link Channel}'s.
     */
    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (childGroup == null) {
            throw new NullPointerException("childGroup");
        }
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = childGroup;
        return this;
    }

这里需解释下parentGroup和childGroup的含义。parentGroup用于处理ServerSocketChannel对应的事件(也就是accept()事件),而childGroup用于处理客户端channel的读写等的事件。前面提过这两种channel有一种父子对应的关系,所以netty就这样做的命名。

从源码可以看出,如果只设置一个group,则parentGroup和childGroup共用一个group。

目前来说,一般在引导器中主动设置两个EventLoopGroup,即

EventLoopGroup parentGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, workerGroup);

看一下NioEventLoopGroup类的构造器方法


public NioEventLoopGroup() {
        this(0);
}
public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor) null);
}
    

可知,传递的数字参数为线程数,跟踪代码知道,若不设线程数(无参),则最终为核心数的2倍。

 protected MultithreadEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
                                     Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, chooserFactory, args);
    }
    
 DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

ServerBootstrap最关键的方法是bind方法。也是开启netty服务器的方法。其具体实现在父类AbstractBootstrap。 调用链为 doBind()-> initAndRegister()->init()。init()依靠子类实现。这也是模板方法的应用。看看init()方法。

 void init(Channel channel) throws Exception {
         
         // 设置属性option及attr,略过
         
        // 获取pipeline 
        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(final Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                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));
                    }
                });
            }
        });
    }

init(channel)的参数channel要说明一下。其来源于NioServerSocketChannel,经反射得到的。

serverBootstrap.group(group).channel(NioServerSocketChannel.class)

也即这个channel是与服务器相关联的channel,这些代码为设置服务端channel的pipeline和handler。看看ServerBootstrapAcceptor


 private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {

        //省略其他字段及方法
       
        @Override
        @SuppressWarnings("unchecked")
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);

            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }

            try {
                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继承了ChannelInboundHandlerAdapter。用于负责接收客户端的连接。当连接过来后注册到childGroup中。

handler与childHandler的区别在于前者处理服务端handler,如接收新客户端连接;后者处理客户端连接,如客户端读写等事件。

文本为简要介绍netty流程,后续尝试逐步分析。若有问题还请指正。

参考

  1. Netty 源码分析之 三 我就是大名鼎鼎的 EventLoop(一)
  2. Netty:EventLoopGroup
  3. Netty 源码分析(三):服务器端的初始化和注册过程
  4. netty实战
  5. Netty 源码解析(二):对 Netty 中一些重要接口和类的介绍