高性能网络编程框架Netty介绍

72 阅读27分钟

本文核心:

  • JAVA I/O模型是如何演化的?

  • Netty架构设计是怎样的?

  • Netty高性能体现在哪些方面?

一、网络编程框架NIO介绍

JAVA中的网络I/O模型有三种:BIO、NIO、AIO。

传统BIO编程

BIO的名称是同步阻塞I/O模式。

该模型的特点是:该模型特点是:有一个独立的Acceptor线程负责监听客户端的连接,一旦有新的连接请求,就会创建一个新的线程进行处理,然后处理完成之后返回客户端,销毁线程。对应的通信模型图如下:

由于每个连接都要创建一个线程来处理,因此该I/O模型的弊端也比较明显:

  • 线程多时占用内存大,并且线程频繁切换开销也大\

  • 线程多时会导致性能急剧下降\

因此 BIO只适用与连接数少的场景。

虽然该模型可以引入线程池+等待队列进行优化,避免使用过多的线程,但是依然无法解决线程利用率低的问题。

使用 BIO 实现网络编程使用的 Java 编程组件是 ServerSocket 和 Socket。服务端示例代码为:

public static void main(String[] args) throws IOException {    final ExecutorService executorService = Executors.newCachedThreadPool();    final ServerSocket serverSocket = new ServerSocket(8080);    while (true) {        final Socket socket = serverSocket.accept();        executorService.execute(() -> {            try {                handler(socket);            } catch (IOException e) {                e.printStackTrace();            }        });    }}

/**
 * 处理客户端请求
 */
private static void handler(Socket socket) throws IOException {    byte[] bytes = new byte[1024];    InputStream inputStream = socket.getInputStream();    socket.close();    while (true) {        int read = inputStream.read(bytes);        if (read != -1) {            System.out.println("msg from client: " + new String(bytes, 0, read));        } else {            break;        }    }}

NIO编程

NIO的名称是同步非阻塞I/O模式。

在这种模型中,服务器上一个线程处理多个连接,即多个客户端请求都会被注册到多路复用器(Selector)上,多路复用器会轮询这些连接,轮询到连接上有 I/O 活动就进行处理。NIO 降低了线程的需求量,提高了线程的利用率。

NIO的三个核心组件:

  1. 缓冲区Buffer

Buffer是NIO类库中一个新的概念,在NIO中,所有数据都是用缓冲区处理的。在写入数据时,是先写入缓冲区,在读取数据时,也是从缓冲区中读取。

缓冲区其实是个数组,但是不仅仅是一个数组,它提供了对数据的结构化访问以及维护读写位置的信息。

常见的缓冲区有:

  • ByteBuffer:最常使用,用于操作字节数组。

  • CharBuffer

  • ShortBuffer

  • IntBuffer

  • LongBuffer

  • FloatBuffer

  • DoubleBuffer

除了Boolean类型,JAVA中每种基本数据类型都有对应的缓冲区类型。

2. 通道Channel

通道是指在客户端和服务端连接建立后的传输数据的管道,可以通过它读取和写入数。通道与流不同之处在于流只是在一个方向上写入或者读取,二通道可以用于读也可以用于写,还可以同时用于读写。

Channel主要可以分为两大类:

  • FileChannel:用于文件操作

  • SelectableChannel:用于网络连接,根据网络协议不同,可以分为:

    • ServerSocketChannel和SocketChannle:用于TCP协议的数据读写,分别对应服务端和客户端的通道\

    • DatagramChannel:用于UDP协议的数据读写

3. 多路复用器Selector

Selector是NIO中一个比较重要的基础概念,Selector会不断轮询注册在其上面的channel,如果某个channel有新的TCP连接接入、读写事件,该channel就变成就绪状态,并且放入名为SelectionKey的选择键集合中,然后迭代SelectionKey集合中的每一个选择键,根据具体I/O事件类型,执行对应的业务操作。

可以注册到Selector上的I/O事件类型枚举有:

  • SelectionKey.OP_READ:可读

  • SelectionKey.OP_WRITE:可写

  • SelectionKey.OP_CONNECT:连接

  • SelectionKey.OP_ACCEPT:接收

一个Selector可以同时轮询多个channel,因此只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端连接,这是NIO模型能够实现高性能的一个关键原因。

使用 NIO 实现网络编程的服务端代码示例:

public static void main(String[] args) throws IOException {    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();    Selector selector = Selector.open();    // 绑定端口    serverSocketChannel.socket().bind(new InetSocketAddress(8080));    // 设置 serverSocketChannel 为非阻塞模式    serverSocketChannel.configureBlocking(false);    // 注册 serverSocketChannel 到 selector,关注 OP_ACCEPT 事件    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);    while (true) {        // 没有事件发生        if (selector.select(1000) == 0) {            continue;        }        // 有事件发生,找到发生事件的 Channel 对应的 SelectionKey 的集合        Set<SelectionKey> selectionKeys = selector.selectedKeys();        //轮询选择器集合        Iterator<SelectionKey> iterator = selectionKeys.iterator();        while (iterator.hasNext()) {            SelectionKey selectionKey = iterator.next();            // 发生 OP_ACCEPT 事件,处理连接请求            if (selectionKey.isAcceptable()) {                SocketChannel socketChannel = serverSocketChannel.accept();                // 将 socketChannel 也注册到 selector,关注 OP_READ                // 事件,并给 socketChannel 关联 Buffer                socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));            }            // 发生 OP_READ 事件,读客户端数据            if (selectionKey.isReadable()) {                SocketChannel channel = (SocketChannel) selectionKey.channel();                ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();                channel.read(buffer);                System.out.println("msg form client: " + new String(buffer.array()));            }                        //......其他事件            // 手动从集合中移除当前的 selectionKey,防止重复处理事件            iterator.remove();        }    }}

NIO编程难度比BIO大很多,但是也有很大的优势:

  • 通过Selector多路复用轮询机制,客户端的连接不会像之前那样被同步阻塞\

  • SocketChannel的读写操作都是异步的, 如果没有可读写的操作,不会同步等待,而是直接返回,这样I/O线程就可以去处理其他链路,不需要同步等待\

  • 由于对线程模型的优化,使得一个Selector可以处理成千上万个客户端连接,并且性能不会线性下降,因此非常适合做高性能、高并发的网络服务器。\

AIO编程

异步I/O模型,简称为AIO,它的基本流程是:

  • 用户线程通过系统调用,向内核注册某个IO操作

  • 内核在整个I/O操作完成后,发送一个信号通知用户程序

  • 用户收到信号后执行后续的业务操作

在AIO模型中,整个内核数据处理过程中,用户线程都不需要阻塞。

AIO,异步I/O模型,虽然AIO的编程比NIO更为简单,但是目前并没有得到很广泛的应用,主要原因有以下几个:

  • Linux对AIO的实现不够成熟

  • Linux下AIO相比NIO的性能提升不够明显。

基于以上几个因素,AIO的应用很少,Netty旧版本也支持AIO,但是后面的版本中仅仅支持NIO了。

二、为什么要使用Netty

NIO存在的问题

虽然NIO功能很强大,但是实际中使用起来并不容易:

  • NIO类库和API繁杂,使用麻烦

  • 对开发者编程水平要求高,需要熟练掌握额外的技能,如多线程

  • 开发工作量和工作难度很大,如:心跳检测、断线重连、粘包拆包、网络阻塞等

  • JDK NIO的BUG,如epoll可能会在某些情况下导致Selector空转,导致CPU 100%。

基于上述原因,在大多数场景下,并不适合直接使用NIO来进行开发,除非非常精通NIO或者有特殊的需求。因此在大多数场景下我们可以用一个基于NIO封装的Netty框架来代替原生的NIO进行开发。

Netty相比NIO的优点

Netty是基于NIO的框架之一,它的健壮性、功能、性能、可扩展性都是首屈一指的。

Netty有着如下优点:

  • API使用简单,开发门槛低

  • 设计优雅

  • 高性能、高吞吐量、可扩展性强

  • 比较成熟稳定,社区活跃,更新比较快

三、Netty入门介绍

Netty是什么

Netty是一个基于NIO的异步的、事件驱动的高性能网络编程框架,它以高性能、高并发著称。使用Netty可以在保证性能的前提下轻松编写出网络应用程序。

关于Netty在网络中的地位,下图可以很好的表示出来:

Netty应用场景

Netty在各个行业都被广泛使用,常用于开发各种高性能的网络通信框架,如:

  • 互联网行业:互联网行业的特点的并发量高,对性能要求高,Netty刚好满足行业的需求,在分布式系统中,各个节点之间都要进行通信,高性能的RPC框架必不可少,而Netty就是构建这些通信基础组件的基础。常见的应用有:阿里的RPC框架Dubbo

  • 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。

  • 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

Netty入门示例

我们将通过一个入门案例来演示第一个Netty开发的应用程序,在该程序中,服务端监听8080端口,客户端去连接本地的8080端口,连接成功后,会发送"Hello Netty!"给服务端,服务端接收到消息之后,也回复"Hello Netty!"给客户端。

Netty程序开发流程

  • 引入maven坐标

  • 服务端启动程序开发

  • 服务端业务处理handler开发

  • 客户端启动程序开发

  • 客户端业务处理handler开发

  1. 引入maven坐标

在该示例中,引入的是netty-all4.1版本

<dependency>  <groupId>io.netty</groupId>  <artifactId>netty-all</artifactId>  <version>4.1.55.Final</version></dependency>

2. 服务端启动程序开发

import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelPipeline;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.util.concurrent.Future;import io.netty.util.concurrent.GenericFutureListener;

/**
 * Netty服务端
 */
public class HelloNettyServer {    private static final Integer port = 8080;    public static void main(String[] args) throws InterruptedException {        //设置两个NioEventLoopGroup线程组:bossGroup和workerGroup        NioEventLoopGroup bossGroup = new NioEventLoopGroup();        NioEventLoopGroup workerGroup = new NioEventLoopGroup();        try {            //启动服务端引导            ServerBootstrap bootstrap = new ServerBootstrap();            // 设置线程组            bootstrap.group(workerGroup, workerGroup)                    //设置服务器端通道的实现类                    .channel(NioServerSocketChannel.class)                    // handler()方法用于给 BossGroup 设置业务处理器                    .handler(new MyChannelHandler())                    // childHandler()方法用于给 WorkerGroup 设置业务处理器                    .childHandler(new ChannelInitializer<SocketChannel>() {                        protected void initChannel(SocketChannel socketChannel) throws Exception {                            ChannelPipeline pipeline = socketChannel.pipeline();                            //加入自定义的handler处理器                            pipeline.addLast(new HelloNettyServerHandler());                        }                    });            //绑定端口号            ChannelFuture future = bootstrap.bind(port).sync();            System.out.println("服务器启动成功,监听端口:" + port);            //等待服务端监听端口关闭            future.channel().closeFuture().sync();        } finally {            //优雅关闭资源            bossGroup.shutdownGracefully();            workerGroup.shutdownGracefully();        }    }}

服务端启动程序主要逻辑是:

  • 创建两个NioEventLoopGroup线程组:bossGroup和workerGroup

  • 创建服务端启动引导类:ServerBootstrap

  • ServerBootstrap绑定两个线程组

  • 设置服务器端通道的实现类

  • 设置其他可选参数

  • 向pipeline中添加自定义的业务处理器:HelloNettyServerHandler

  • 启动并监听8080端口

  • 等待服务端监听端口关闭

在开发时,我们更多关注业务处理器:HelloNettyServerHandler,因为业务内容多数都通过handler来实现。

3.服务端业务处理handler开发

import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.util.CharsetUtil;

/**
 * 服务端业务处理器
 */
public class HelloNettyServerHandler extends ChannelInboundHandlerAdapter {    /**
     * 当通道有数据可读时执行
     * @param ctx 通道连接上下文对象
     * @param msg 接收到的消息
     * @throws Exception
     */
    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        //接收客户端发送的数据(byteBuf),并输出        ByteBuf in = (ByteBuf) msg;        System.out.println("服务端接收到一条消息:" + in.toString(CharsetUtil.UTF_8));        //将客户端发送的数据原样回复给客户端        ctx.writeAndFlush(in);    }    /**
     * 发生异常时调用
     * @param ctx 通道连接上下文对象
     * @param cause 异常
     * @throws Exception
     */
    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        System.out.println("出现异常 : ");        cause.printStackTrace();    }}

在该handler中,继承于ChannelInboundHandlerAdapter,该类是负责处理入站和用户自定义事件处理接口ChannelInboundHandler的实现类,在该类中实现channelRead方法,在服务端接收到客户端发送的消息后会回调该方法。

4. 客户端启动程序开发

import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import java.net.InetSocketAddress;

/**
 * Netty客户端
 */
public class HelloNettyClient {    private static final String host = "127.0.0.1";    private static final Integer port = 8080;    public static void main(String[] args) throws InterruptedException {        //设置客户端NioEventLoopGroup线程组        NioEventLoopGroup group = new NioEventLoopGroup();        try {            //设置客户端启动引导            Bootstrap bootstrap = new Bootstrap();            //绑定线程组            bootstrap.group(group)                    //设置客户端的socket通道实现类                    .channel(NioSocketChannel.class)                    //指定连接的服务器IP和端口                    .remoteAddress(new InetSocketAddress(host, port))                    //handler方法用于设置业务处理器                    .handler(new ChannelInitializer<SocketChannel>() {                        @Override                        protected void initChannel(SocketChannel socketChannel) {                            //像pipeline添加业务处理器                            socketChannel.pipeline().addLast(new HelloNettyClientHandler());                        }                    });            //连接服务器            ChannelFuture sync = bootstrap.connect().sync();            System.out.println("客户端连接服务器成功,服务器地址:" + host + ", 端口号:" + port);            //对通道关闭进行监听            sync.channel().closeFuture().sync();        } finally {            //优雅关闭资源            group.shutdownGracefully();        }    }}

客户端的流程和服务端类似,只是客户端只需要一个NioEventLoopGroup线程组,启动引导类是Bootstrap,对应的客户端通道实现类是NioSocketChannel,然后根据指定的IP和端口连接服务器即可。

同服务端一样,也需要添加客户端业务处理器来完成业务处理。

5. 客户端业务处理handler开发

import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.util.CharsetUtil;

/**
 * 客户端自定义事件处理器
 * 需要继承SimpleChannelInboundHandler,
 * 用于处理数据流入本端(客户端)的IO事件
 */
public class HelloNettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {    /**
     * 通道建立后调用该方法
     * @param ctx 通道连接上下文对象
     * @throws Exception
     */
    @Override    public void channelActive(ChannelHandlerContext ctx) throws Exception {        //像服务器发送数据,Unpooled是Netty提供的用来操作ByteBuf缓冲区的工具类        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty!", CharsetUtil.UTF_8));        System.out.println("客户端向服务端发送消息成功");    }    /**
     * 当通道有数据可读时执行
     * @param channelHandlerContext 通道连接上下文对象
     * @param byteBuf ByteBuf数据缓冲区
     * @throws Exception
     */
    @Override    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {        System.out.println("客户端接收到消息: " + byteBuf.toString(CharsetUtil.UTF_8));    }    /**
     * 发生异常时调用
     * @param ctx 通道连接上下文对象
     * @param cause 异常
     * @throws Exception
     */
    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        cause.printStackTrace();    }}

客户端业务处理器继承于客户端专用的SimpleChannelInboundHandler,该类继承自ChannelInboundHandlerAdapter,该类和其父类的主要区别是:

  • 需要指定泛型类型,对应传输的数据类型

  • channelRead方法完成后就自动释放指向保存该消息的ByteBuf的内存引用

实现以下三个方法完成业务处理:

  • channelActive通道建立后调用该方法,然后将消息"Hello Netty!"发送到服务端

  • channelRead0当通道有数据可读时调用该方法

  • exceptionCaught发生异常时调用该方法

6. 效果演示

先启动服务端,然后再启动客户端,可以看到服务端输出:

客户端输出:

通过上述代码的编写,就可以完成一个简单的Netty网络通信程序,虽然代码看着很复杂,但是性能却是很强大,而且相比NIO的代码量,Netty已经简化了太多编码工作了。

Netty架构设计

1. Netty整体架构

这是一张Netty官网关于Netty的核心架构图:

可以看到Netty由以下三个核心部分构成:

  • 传输服务:传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。

  • 协议支持:Netty支持多种常见的数据传输协议,包括:HTTP、WebSocket、SSL、zlib/gzip、二进制、文本等,还支持自定义编解码实现的协议。Netty丰富的协议支持降低了开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。

  • Core核心:Netty的核心,提供了底层网络通信的通用抽象和实现,包括:可扩展的事件驱动模型、通用的通信API、支持零拷贝的Buffer缓冲对象。

2. Netty逻辑架构

Netty采用典型的三层网络架构进行设计和开发,其逻辑架构如图:

2.1 网络通信层

网络通信层的职责主要是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,如连接创建、连接激活、读事件、写事件等,这些网络事件会分发给事件调度层进行处理。

网络通信层的核心组件有:

  • Bootstrap

启动引导,主要负责整个Netty程序的启动、初始化、服务器连接等过程,它相当于一条主线,把Netty的其他核心组件给串联起来。

Netty中的引导器分为两种:Bootstrap:客户端启动引导,另一种是ServerBootstrap:服务端启动引导。它们都继承自抽象类 AbstractBootstrap。

  • Channel

网络通信的载体,提供了基本的用于I/O操作的API,如:register、bind、connect、read、write、flush等。

Netty的Channel是在JDK的NIO Channel基础上进行封装的,提供了更高层次的抽象,同时屏蔽了底层Socket的复杂性,赋予了Channel更加强大的功能。

  • ByteBuf

当我们在进行数据传输的时候,往往需要使用到缓冲区。常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。

在NIO中,除了boolean类型外,其他JAVA基础数据类型都有自己的缓冲区实现,对于NIO来说,使用比较多的是ByteBuffer。但是ByteBuffer本身有一定的局限性:

  • 长度固定,一旦分配完成, 不能扩容和缩容

  • 只有一个标识位置的指针position,读写切换需要手动调用flip()方法

  • API封装的功能有限

因此,Netty提供了比BufferBuffer更为丰富强大的缓冲区实现-ByteBuf。

2.2 事件调度层

事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件( I/O 事件、信号事件、定时事件等),实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。

事件调度层的核心组件有:

  • EventLoopGroup
  • EventLoop

EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程处理请求。一个EventLoopGroup包含一个或者多个EventLoop,EventLoop用于处理Channel生命周期内的所有I/O事件,如accept、read、write等。

EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel。

每新建一个Channel,EventLoopGroup会选择一个EventLoop与其绑定,该Channel生命周期内都可以对EventLoop进行多次绑定和解绑。

2.3 服务编排层

负责组装各类服务,它是Netty的核心业务处理链,用于实现网络事件的动态编排和有序传播。

服务编排层的核心组件有:

  • ChannelPipeline:handler处理器链列表,类似于容器,内部通过双向链表将不同的ChannelHandler串联在一起,当I/O读写事件发生时,ChannelPipeline会依次调用ChannelHandler列表对Channel的数据进行拦截处理。每个Channel创建时就会绑定一个新的ChannelPipeline,他们是一一对应的关系。

  • ChandlerHandler:事件业务处理器顶层接口,它不处理入站和出站事件,而是由其子类来实现,核心子类包括:

    • ChannelInboundHandler:负责处理入站事件,以及用户自定义事件

    • ChannelOutboundHandler:负责处理出站事件

    • ChannelDuplexHandler:同时实现了ChannelInboundHandler和ChannelOutboundHandler两个接口,既可以处理入站事件,也可以处理出站事件

  • ChannelHandlerContext:ChandlerHandler的上下文对象,在调用ChandlerHandler方法时,都会传入该上下文对象,ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。

由于Netty的分层架构设计合理、优雅,因此,基于Netty开发的各种服务器程序才会越来越多。

四、Netty何以实现高性能

I/O模型

用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,I/O 模型在很大程度上决定了框架的性能。

Netty采用基于NIO的多路复用技术,用一个Selector单线程就可以接入成千上万的客户端连接,降低了线程资源消耗,并且可以避免多线程的切换带来的性能开销。

线程模型

Reactor线程模型是对于传统的I/O线程模型的一种优化。

传统的I/O线程模型采用阻塞I/O来获取输入流数据,并且每个连接都需要独立的线程完成数据的输入、业务处理、数据返回等一个完整的操作链路。这种模型在高并发场景下,有两个比较明显的缺点:

  • 每个连接都需要创建一个对应线程,线程大量创建占用大量的服务器资源

  • 线程没有数据可读情况下的阻塞会对性能造成很大的影响

Reactor线程模型为了解决这两个问题,提供了以下解决方案:

  • 基于I/O多路复用:多个客户端连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,通过事件驱动通知应用程序,线程从阻塞状态返回,开始进行业务处理\

  • 基于线程池技术减少线程创建:基于线程池,不必再为每一个连接创建线程,将连接完成后的业务处理分配给线程池进行调度。

三种Reactor线程模型

根据Reactor线程和handler处理线程的模型不同,有三种Reactor线程模型:

  • 单Reactor单线程模型

  • 单Reactor多线程模型

  • 主从Reactor线程模型

1. 单Reactor单线程模型

单Reactor单线程模型是指用一个线程通过多路复用来完成所有的I/O操作(accept、read、write等)。

服务端处理流程:

  • Reactor对象通过select监听客户端请求,收到请求后通过Dispatch分发

  • 如果是建立连接请求,则由Acceptor通过accept处理连接,然后创建一个handler对象处理完成连接后的各种事件

  • 如果不是建立连接请求,则分发调用对应的Handler来完成数据读取、业务处理,并将结果返回给客户端

单Reactor单线程模型的优点在于模型简单,没有多线程、进程间通信、竞争的问题,全部都在一个线程中完成。

缺点是:

  • 性能问题:只有一个线程去处理任务,在高并发情况下很容易阻塞

  • 可靠性问题:一旦线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障

为了解决上面的两个问题,演进了单Reactor多线程模型。

2. 单Reactor多线程模型

该模型和单Reactor单线程模型的最主要区别在于,Reactor主线程只负责监听、接收客户端请求以及派发任务,而比较耗时的I/O操作由另一个worker线程池来进行分配线程去处理。

服务端处理流程:

  • Reactor对象通过select监听客户端请求,收到请求后通过Dispatch分发\

  • 如果是建立连接请求,则由Acceptor通过accept处理连接,然后创建一个handler对象处理完成连接后的各种事件

  • 如果不是建立连接请求,则分发调用对应的Handler来处理\

  • Handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池的某个线程处理业务。

  • Worker线程池会分配独立线程完成真正的业务,并将结果返回给 Handler,Handler 收到响应后,通过 send 将结果返回给 客户端。

在绝大多数场景下,该模型性能表现优异,可以充分发挥多核CPU的处理能力。

但是在并发上百万的场景下,一个NIO线程负责监听和处理所有客户端连接可能存在性能问题。例如,某些场景下,会对客户端的请求进行安全认证等,这类请求非常耗时。在此场景下,单独一个Reactor线程可能存在性能不足的问题,为了解决这个问题,诞生了第三种线程模型:主从Reactor多线程模型。

3. 主从Reactor线程模型

该线程模型与单Reactor多线程模型主要在于Reactor线程分为了主从Reactor线程两部分,即下图中的Main Reactor和Sub Reactor。

服务端处理请求流程:

  • Reactor主线程对象通过seletct监听连接事件,收到事件后,通过Acceptor处理连接事件

  • 当Acceptor处理连接事件后,主Reactor线程将连接分配给子Reactor线程

  • Reactor子线程将连接加入连接队列进行监听,并创建handler进行各种事件处理

  • 当有新事件发生时,子Reactor线程会调用对应的handler进行处理

  • handler读取数据分发给worker线程池分配一个独立的线程进行业务处理,并返回结果给handler

  • handler收到响应的结果后,通过send将结果返回给客户端

该模型虽然编程复杂度高,但是其优势比较明显,体现在:

  • 主从线程职责分明,主线程只需要接收新请求,子线程完成后续的业务处理

  • 主从线程数据交互简单,主线程只需要把新连接传给子线程

因此该模型在许多项目中广泛使用,包括Nginx、Memcached、Netty等。

Netty线程模型

Netty的线程模型不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。

Netty官方推荐的是基于主从Reactor线程模型实现的NioEventLoopGroup线程模型:

NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup();

NioEventLoopGroup相当于一个事件循环组,这个组里面有多个事件循环,称为NioEventLoop,类似于线程池和中间的线程的关系。

NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上面的socket网络通信。

一个NioEventLoopGroup可以有多个NioEventLoop,可以通过构造函数指定,默认是CPU核心数x2。

Netty线程模型抽象出两组NioEventLoopGroup:

  • BossGroup:专门负责接收客户端的请求

  • WorkerGroup:专门负责网络的读写、业务处理

Boss NioEventLoop职责:

  • select:轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT)

  • processSelectedKeys:处理accept事件,与client事件建立连接,生成NioSocketChannel,并将其注册到某个worker NIoEventLoop上的selector上

  • runAllTasks:再循环处理任务队列的任务

Worker NioEventLoop职责:

  • select:轮询注册在其上的NioSocketChannel的read/write事件(OP_READ和OP_WRITE)、

  • processSelectedKeys:在对应的NIoSocketChannel 上进行I/O处理

  • runAllTasks:再循环处理任务队列的任务

每个Worker NioEventLoop处理业务时,会通过pipeline(管道),pipeline中包含了channel,管道中维护了很多的处理器,数据会在管道中的各个处理器间进行流转、处理。

序列化协议

影响序列化性能的关键因素总结如下:

  • 序列化后的码流大小(网络带宽的占用);

  • 序列化&反序列化的性能(CPU资源占用);

  • 是否支持跨语言(异构系统的对接和开发语言切换)。

采用什么样的通信协议,对系统的性能极其重要,netty默认提供了对Google Protobuf的支持,也可以通过扩展Netty的编解码接口,用户可以实现其它的高性能序列化框架

这是一张不同的序列化框架序列化后的字节数组对比图:

从上图可以看出,Protobuf序列化后的码流只有Java序列化的1/4左右。正是由于Java原生序列化性能表现太差(以及不能跨语言等),才催生出了各种高性能的开源序列化技术和框架。

其他

1. 零拷贝

Netty的零拷贝体现在三个方面:

  • Netty的接收和发送ByteBuf采用DIRECT BUFFERS,使用堆外内存进行socket读写,不需要进行自己缓冲区的二次拷贝。

  • Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

  • Netty的文件传输采用了transfer()方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write发送导致的内存拷贝问题。

2. 内存池

由于ByteBuf分配的是堆外内存空间,其分配和回收过程不像JAVA实例内存分配和回收那么简单,为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。

基于内存池重用的ByteBuf性能比普通的ByteBuf高几十个数量级。

3. 无锁化串行设计

为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

4. 高效并发编程

包括:

  • volatile关键字的大量、正确使用

  • CAS和原子类的广泛使用

  • 线程安全容器的使用

  • 通过读写锁提升并发性能

五、文章总结

鉴于篇幅原因, 本文只介绍了Netty的核心架构以及重要组件简单介绍,还有一些高级的知识点还未介绍,如高效序列化、心跳检测、TCP粘包拆包问题解决、多协议支持等。

总结一下文章的核心内容:

JAVA中的三种I/O模式:

  • BIO:传统同步阻塞IO

  • NIO:同步非阻塞IO

  • AIO:异步非阻塞IO

虽然NIO功能很强大,性能很好,但是由于其开发难度大,并且存在BUG,因此业界普通使用封装更完善、功能更强大、扩展性更好的Netty来实现高并发的网络编程。

Netty的整体架构分为:

  • 传输服务

  • 协议支持

  • Core核心

Netty的逻辑架构分为:

  • 网络通信层

  • 事件调度层

  • 服务编排层

Netty的核心组件有:

  • Bootstrap:启动引导\

  • Channel:网络通信载体

  • ByteBuf:数据缓冲区,读写都是直接操作缓冲区

  • EventLoopGroup和EventLoop:线程模型,实现高并发的核心

  • ChannelPipeline:handler处理器链容器

  • ChandlerHandler:事件业务处理器顶层接口

  • ChannelHandlerContext:ChandlerHandler的上下文对象

Netty实现高性能的原因:

  • 非阻塞I/O模型

  • 良好的线程模型设计

  • 高效序列化协议支持

  • 其他:零拷贝、内存池、无锁化减少线程切换开销、高效并发编程等

六、参考资料

  • 《Netty权威指南》:比较权威,知识点比较详细,有广度也有深度,但是有些内容不是很好理解

  • 《Netty编程实战》:注重根据实战来讲解理论知识

  • 《Netty、Redis、Zookeeper高并发实战》:对各种I/O模型中的同步、异步、阻塞、非阻塞等比较让人困惑的名词做了详细的解释

  • B站视频:尚硅谷Netty视频教程:如果没了解过netty,可以跟着视频教程学,不过比较耗时

  • 文章:NIO框架详解:Netty的高性能之道