Netty设计思想之API

Netty作为十分流行的NIO网络库,除了高性能的优点外,另一大优势就是其简洁易用的API,使得开发者们可以快速高效的开发网络应用程序。这得益于Netty作者多年的网络协议实现经验,让网络库变得简单、易用、稳定、高性能和可扩展。

网络库那么多,为什么要使用Netty呢?官方的回答很有意思:

The answer is the philosophy it is built on. Netty is designed to give you the most comfortable experience both in terms of the API and the implementation from the day one. It is not something tangible but you will realize that this philosophy will make your life much easier as you read this guide and play with Netty.

舒适的使用体验是Netty的设计哲学。简而言之,人生苦短,请用Netty

API作为软件系统的门面,是软件系统思维模型的直接体现,其设计的合理性决定着软件系统后续的可维护性。好的API的设计一般遵循如下几个规则

本文从API的角度看一下Netty的设计,从其API中可以知晓其背后的思维模型。

Boostrap

我们首先从开发者的角度,看一下如何用Netty快速实现启动一个网络应用。AbstractBootstrap是Netty提供的以链式的方式快速配置并启动的脚手架抽象类,提供了BootstrapServerBootstrap分别对应client和server的配置启动。其API和继承类图如下所示(这里主要列举核心的API):

AbstractBootstrap的API主要分为四类:

核心API的描述如下表:

从上面的API的设计,我们其实可以知道Netty两个核心模型:

上面两个模型后面会详细介绍,我们以server端为例看下Netty的实现方式:

// 配置连接事件的reactor线程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);

// 配置读写事件的reactor线程
EventLoopGroup workerGroup = new NioEventLoopGroup(8);

// 处理器
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
      // 指定ServerSocketChannel具体实现
     .channel(NioServerSocketChannel.class)

     // 指定新建立的连接的链式处理器
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
             ChannelPipeline p = ch.pipeline();
             p.addLast(serverHandler);
         }
     });

    // 绑定地址、端口并启动
    ChannelFuture f = b.bind(PORT).sync();

    // 等待直到ServerSocket关闭
    f.channel().closeFuture().sync();
} finally {
    // 关闭reactor线程池
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
}

Channel, ChannelFuture

Channel作为Netty的核心组件之一,是对Socket数据通道的抽象,负责I/O操作,比如readwriteconnectbind。I/O操作的具体实现是由底层的网络库提供的,比如NIO、Epoll等。

Channel的接口设计如下图所示:

Channel的API设计可以看出,Channel主要职责为:

Netty作为异步的NIO框架,其所有的I/O操作都是异步的。这意味着对I/O操作的调用是立马返回的,Netty提供了ChannelFuture接口在操作完成后回调listener通知结果。

ChannelFuture的接口设计如下图所示:

ChannelFuture主要是在Future的基础上添加了获取相应Channel的方法,以及设置listener回调的接口,在I/O操作完成时可以获取到操作结果。

这里需要注意的是在listener回调函数中不宜使用长时间的阻塞操作,因为I/O操作一般都是在I/O线程中执行的,长时间的阻塞操作会阻塞I/O线程从而不能响应新的I/O事件。

ChannelHandler, ChanelPipeline, ChannelHandlerContext

ChannelPipeline是Netty的数据处理组件,由一系列ChannelHandler组成,以管道流的方式处理Channel相关的上行和下行数据流。这种设计解耦了网络层和数据处理层,使得开发者只需要实现、复用、组装自己的ChannelHandler,而不用关心底层的网络实现。

整个ChannelPipeline的接口设计如下图所示:

Netty将I/O操作分为上行(ChannelInboundInvoker)和下行(ChannelOutboundInvoker)两部分。相应的,事件处理器也分为上行处理器(ChannelInboundHandler)和下行处理器(ChannelOutboundHandler)。

ChannelPipeline并不是直接操作ChannelHandler,而是通过ChannelHandlerContext进行交互,ChannelHandlerContext的作用在于:

Netty的数据流由下图所示:

网络层socket读取的数据会按配置顺序自低向上的由各个上行处理器(ChannelInboundHandler)处理。同样的,应用层写入的数据也会按配置顺序自顶向下的由个下行处理器(ChannelOutboundHandler)处理,最后写入到网络层的socket中。相邻ChannelHandler之间是由ChannelHandlerContext基于事件机制触发的。

我们注意到ChannelPipeline在添加ChannelHandler时有如下两个接口:

    /**
     * Appends a {@link ChannelHandler} at the last position of this pipeline.
     *
     * @param name     the name of the handler to append
     * @param handler  the handler to append
     *
     * @throws IllegalArgumentException
     *         if there's an entry with the same name already in the pipeline
     * @throws NullPointerException
     *         if the specified handler is {@code null}
     */
    ChannelPipeline addLast(String name, ChannelHandler handler);

    /**
     * Appends a {@link ChannelHandler} at the last position of this pipeline.
     *
     * @param group    the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler}
     *                 methods
     * @param name     the name of the handler to append
     * @param handler  the handler to append
     *
     * @throws IllegalArgumentException
     *         if there's an entry with the same name already in the pipeline
     * @throws NullPointerException
     *         if the specified handler is {@code null}
     */
    ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler);

这两个接口的不同之处在于,第二个接口指定了ChannelHandler执行的线程池EventExecutorGroup。默认情况下,ChannelHandler的处理操作是在I/O线程中执行的,但是如果ChannelHandler中有耗时的阻塞操作,就需要指定专有线程池以免阻塞I/O线程。

ChannelPipeline这种设计可以称之为SEDA(staged event-driven architecture,分阶段事件驱动架构),将数据流的处理划分为不同的阶段,阶段之间通过事件的方式异步通知,每个阶段可以有单独的线程池进行处理。这种架构的好处在于,将数据处理异步化、模块化,提高吞吐量的同时简化编程。

EventLoop, EventLoopGroup

EventLoop是Netty提供的Reactor线程组件,主要处理网络I/O事件及并发任务。相对应的,EventLoopGroup则是Reactor线程池组件。Netty线程模型就是多Reactor线程模型,参见Netty设计思想之Reactor

EventLoop的API类图如下所示(引用Netty In Action):

由上图可知,EventLoop是直接扩展自java.util.concurrent.ScheduledExecutorService进行任务处理,以及io.netty.util.concurrent.EventExecutor进行线程调度,并添加一些Channel相关接口进行I/O操作。

一个EventLoop对应一个Thread,上面也说到,一个EventLoop可以服务(注册)多个Channel,这得益于NIO的Selector接口进行I/O多路复用。

注意到EventLoop有如下接口:

    /**
     * Calls {@link #inEventLoop(Thread)} with {@link Thread#currentThread()} as argument
     */
    boolean inEventLoop();

这个接口揭示了EventLoop的执行逻辑:任务的执行必须在本线程中执行,否则会添加至任务队列之中。

EventLoopGroup是由多个EventLoop组成的,注册Channel时会唯一分配一个EventLoopChannel后续相关的I/O事件都由这个EventLoop处理,事件和任务都以FIFO的顺序执行,可以避免额外的线程同步操作,保证了高性能。

总结

从Netty的API设计之中,我们可以看到其两个核心模型:多Reactor线程模型和SEDA线程模型,前者让Netty保持了高性能的特性,后者让Netty易于使用和维护。

还是那句话,人生苦短,请用Netty。

Powered by Jekyll and Theme by solid