Netty启动流程、整体结构及核心组件分析

本文将Netty服务端示例代码入手,分析Netty启动流程、整体结构及核心组件。

1. Netty 是什么

Netty 是 JBoss 开源项目,是异步的、基于事件驱动的网络应用框架,它以高性能、高并发著称。所谓基于事件驱动,说得简单点就是 Netty 会根据客户端事件(连接、读、写等)做出响应。

Netty 是基于 Java NIO 构建出来的,Java NIO 又是基于Linux提供的高性能 IO 接口/系统调用构建出来的。

2. 从一个简单的示例开始

Netty 的模块化设计非常优雅,客户端或者服务端的启动方式基本是固定的。作为开发者来说,只要照葫芦画瓢即可轻松上手。大多数场景下,你只需要实现与业务逻辑相关的一系列 ChannelHandler,再加上 Netty 已经预置了 HTTP 相关的编解码器就可以快速完成服务端框架的搭建。下面通过一个服务端示例,熟悉下 Netty 的编程模式。

public class HttpServer {

    public void start(int port) throws Exception {
      
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
						// 引导器
            ServerBootstrap b = new ServerBootstrap();
						// 配置线程池
            b.group(bossGroup, workerGroup)
										// channel初始化
                    .channel(NioServerSocketChannel.class)

                    .localAddress(new InetSocketAddress(port))

                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
												
                        public void initChannel(SocketChannel ch) {

                            ch.pipeline()
                                    .addLast("codec", new HttpServerCodec())                  // HTTP 编解码
                                    .addLast("compressor", new HttpContentCompressor())       // HttpContent 压缩
                                    .addLast("aggregator", new HttpObjectAggregator(65536))   // HTTP 消息聚合
                                    .addLast("handler", new HttpServerHandler());             // 自定义业务逻辑处理器
                        }
                    })

                    .childOption(ChannelOption.SO_KEEPALIVE, true);

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

            System.out.println("Http Server started, Listening on " + port);

            f.channel().closeFuture().sync();

        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {
        new HttpServer().start(8088);
    }

}

所有 Netty 服务端的启动类都可以采用上述代码结构进行开发。简单梳理一下流程:首先创建引导器;然后配置线程模型,通过引导器绑定业务逻辑处理器,并配置一些网络参数;最后绑定端口,就可以完成服务器的启动了。

3. Netty启动流程分析

从上一节的示例中可以看到,Netty 服务端的启动过程大致分为三个步骤:

  1. 配置线程池;
  2. Channel 初始化;
  3. 端口绑定。

3.1 配置线程池

Netty 是采用 Reactor 模型进行开发的,可以非常容易切换三种 Reactor 模式:单线程模式多线程模式主从多线程模式

(1)单线程模式

Reactor 单线程模型所有 I/O 操作都由一个线程完成,所以只需要启动一个 EventLoopGroup 即可。

EventLoopGroup group = new NioEventLoopGroup(1);

ServerBootstrap b = new ServerBootstrap();

b.group(group)

(2)多线程模式

Reactor 单线程模型有非常严重的性能瓶颈,因此 Reactor 多线程模型出现了。在 Netty 中使用 Reactor 多线程模型与单线程模型非常相似,区别是 NioEventLoopGroup 可以不需要任何参数,它默认会启动 2 倍 CPU 核数的线程。当然,你也可以自己手动设置固定的线程数。

EventLoopGroup group = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();

b.group(group)

(3)主从多线程模式

在大多数场景下,我们采用的都是主从多线程 Reactor 模型。Boss 是主 Reactor,Worker 是从 Reactor。它们分别使用不同的 NioEventLoopGroup,主 Reactor 负责处理 Accept,然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。

EventLoopGroup bossGroup = new NioEventLoopGroup();

EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)

从上述三种 Reactor 线程模型的配置方法可以看出:Netty 线程模型的可定制化程度很高。它只需要简单配置不同的参数,便可启用不同的 Reactor 线程模型,而且无需变更其他的代码,很大程度上降低了用户开发和调试的成本。

3.2 Channel 初始化

(1)设置 Channel 类型

NIO 模型是 Netty 中最成熟且被广泛使用的模型。因此,推荐 Netty 服务端采用 NioServerSocketChannel 作为 Channel 的类型,客户端采用 NioSocketChannel。设置方式如下:

b.channel(NioServerSocketChannel.class);

当然,Netty 提供了多种类型的 Channel 实现类,你可以按需切换,例如 OioServerSocketChannel、EpollServerSocketChannel 等。

(2)注册 ChannelHandler

在 Netty 中可以通过 ChannelPipeline 去注册多个 ChannelHandler,每个 ChannelHandler 各司其职,这样就可以实现最大化的代码复用,充分体现了 Netty 设计的优雅之处。那么如何通过引导器添加多个 ChannelHandler 呢?其实很简单,我们再看下 服务器代码示例:

b.childHandler(new ChannelInitializer<SocketChannel>() {

    @Override
    public void initChannel(SocketChannel ch) {
        ch.pipeline()
                .addLast("codec", new HttpServerCodec())
                .addLast("compressor", new HttpContentCompressor())
                .addLast("aggregator", new HttpObjectAggregator(65536)) 
                .addLast("handler", new HttpServerHandler());
    }
})

ServerBootstrap 的 childHandler() 方法需要注册一个 ChannelHandler。ChannelInitializer是实现了 ChannelHandler接口的匿名类,通过实例化 ChannelInitializer 作为 ServerBootstrap 的参数。

Channel 初始化时都会绑定一个 Pipeline,它主要用于服务编排。Pipeline 管理了多个 ChannelHandler。I/O 事件依次在 ChannelHandler 中传播,ChannelHandler 负责业务逻辑处理。上述 HTTP 服务器示例中使用链式的方式加载了多个 ChannelHandler,包含HTTP 编解码处理器、HTTPContent 压缩处理器、HTTP 消息聚合处理器、自定义业务逻辑处理器

(3)设置 Channel 参数

Netty 提供了十分便捷的方法,用于设置 Channel 参数。关于 Channel 的参数数量非常多,如果每个参数都需要自己设置,那会非常繁琐。幸运的是 Netty 提供了默认参数设置,实际场景下默认参数已经满足我们的需求,我们仅需要修改自己关系的参数即可。

b.option(ChannelOption.SO_KEEPALIVE, true);

ServerBootstrap 设置 Channel 属性有optionchildOption两个方法,option 主要负责设置 Boss 线程组,而 childOption 对应的是 Worker 线程组。

这里列举了经常使用的参数含义,可以结合业务场景,按需设置。

参数

含义

SO_KEEPALIVE

设置为 true 代表启用了 TCP SO_KEEPALIVE 属性,TCP 会主动探测连接状态,即连接保活

SO_BACKLOG

已完成三次握手的请求队列最大长度,同一时刻服务端可能会处理多个连接,在高并发海量连接的场景下,该参数应适当调大

TCP_NODELAY

Netty 默认是 true,表示立即发送数据。如果设置为 false 表示启用 Nagle 算法,该算法会将 TCP 网络数据包累积到一定量才会发送,虽然可以减少报文发送的数量,但是会造成一定的数据延迟。Netty 为了最小化数据传输的延迟,默认禁用了 Nagle 算法

SO_SNDBUF

TCP 数据发送缓冲区大小

SO_RCVBUF

TCP数据接收缓冲区大小,TCP数据接收缓冲区大小

SO_LINGER

设置延迟关闭的时间,等待缓冲区中的数据发送完成

CONNECT_TIMEOUT_MILLIS

建立连接的超时时间

3.3 端口绑定

在完成上述 Netty 的配置之后,bind() 方法会真正触发启动,sync() 方法则会阻塞,直至整个启动过程完成,具体使用方式如下:

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

服务端的启动过程一定离不开配置线程池、Channel 初始化、端口绑定三个步骤,在 Channel 初始化的过程中最重要的就是绑定用户实现的自定义业务逻辑。是不是特别简单?你可以参考本节课的示例,自己尝试开发一个简单的程序练练手。

4. Netty核心架构

对Netty的使用详解了解后,接下来将分别从 Netty 整体结构逻辑架构源码结构三个方面对其进行深层次的分析。

4.1 Netty 整体结构

Netty 是一个设计非常用心的网络基础组件,Netty 官网给出了有关 Netty 的整体功能模块结构。从图中,我们可以清晰地看出 Netty 结构一共分为三个模块:

(1)Core 核心层

Core 核心层是 Netty 最精华的内容,它提供了底层网络通信的通用抽象和实现,包括可扩展的事件模型、通用的通信 API、支持零拷贝的 ByteBuf 等。

(2)Protocol Support 协议支持层

协议支持层基本上覆盖了主流协议的编解码实现,如 HTTP、SSL、Protobuf、压缩、大文件传输、WebSocket、文本、二进制等主流协议,此外 Netty 还支持自定义应用层协议。Netty 丰富的协议支持降低了用户的开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。

(3)Transport Service 传输服务层

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

4.2 Netty 逻辑架构

对 Netty 的整体结构已经有了一个大概的印象,下面我们一起看下 Netty 的逻辑架构,学习下 Netty 是如何做功能分解的。如下图,Netty 的逻辑处理架构为典型网络分层架构设计,共分为网络通信层、事件调度层、服务编排层,每一层各司其职。图中包含了 Netty 每一层所用到的核心组件。接下来,将为你介绍 Netty 的每个逻辑分层中的各个核心组件以及组件之间是如何协调运作的。

(1)网络通信层

网络通信层的职责是执行网络 I/O 的操作。它支持多种网络协议和 I/O 模型的连接操作。当网络数据读取到内核缓冲区后,会触发各种网络事件,这些网络事件会分发给事件调度层进行处理。

网络通信层的核心组件包含BootStrap、ServerBootStrap、Channel三个组件。

  • BootStrap & ServerBootStrap

Bootstrap 是“引导”的意思,它主要负责整个 Netty 程序的启动、初始化、服务器连接等过程,它相当于一条主线,串联了 Netty 的其他核心组件。

如下图所示,Netty 中的引导器共分为两种类型:一个为用于客户端引导的 Bootstrap,另一个为用于服务端引导的 ServerBootStrap,它们都继承自抽象类 AbstractBootstrap。

Bootstrap 和 ServerBootStrap 十分相似,两者非常重要的区别在于 Bootstrap 可用于连接远端服务器,只绑定一个 EventLoopGroup。而 ServerBootStrap 则用于服务端启动绑定本地端口,会绑定两个 EventLoopGroup,这两个 EventLoopGroup 通常称为 Boss 和 Worker。

ServerBootStrap 中的 Boss 和 Worker 是什么角色呢?它们之间又是什么关系?这里的 Boss 和 Worker 可以理解为“老板”和“员工”的关系。每个服务器中都会有一个 Boss,也会有一群做事情的 Worker。Boss 会不停地接收新的连接,然后将连接分配给一个个 Worker 处理连接。

有了 Bootstrap 组件,我们可以更加方便地配置和启动 Netty 应用程序,它是整个 Netty 的入口,串接了 Netty 所有核心组件的初始化工作。

  • Channel

Channel 的字面意思是“通道”,它是网络通信的载体。Channel 提供了基本的 API 用于网络 I/O 操作,如 register、bind、connect、read、write、flush 等。Netty 自己实现的 Channel 是以 JDK NIO Channel 为基础的,相比较于 JDK NIO,Netty 的 Channel 提供了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性,赋予了 Channel 更加强大的功能,你在使用 Netty 时基本不需要再与 Java Socket 类直接打交道。

下图是 Channel 家族的图谱。AbstractChannel 是整个家族的基类,派生出 AbstractNioChannel、AbstractOioChannel、AbstractEpollChannel 等子类,每一种都代表了不同的 I/O 模型和协议类型。

常用的 Channel 实现类有:

  • NioServerSocketChannel 异步 TCP 服务端。
  • NioSocketChannel 异步 TCP 客户端。
  • OioServerSocketChannel 同步 TCP 服务端。
  • OioSocketChannel 同步 TCP 客户端。
  • NioDatagramChannel 异步 UDP 连接。
  • OioDatagramChannel 同步 UDP 连接。

Channel 会有多种状态,如连接建立、连接注册、数据读写、连接销毁等。随着状态的变化,Channel 处于不同的生命周期,每一种状态都会绑定相应的事件回调,下面的表格列举了 Channel 最常见的状态所对应的事件回调。

事件

说明

channelRegistered

Channel 创建后被注册到 EventLoop 上

channelUnregistered

Channel 创建后未注册或者从 EventLoop 取消注册

channelActive

Channel 处于就绪状态,可以被读写

channelInactive

Channel 处于非就绪状态

channelRead

Channel 可以从远端读取到数据

channelReadComplete

Channel 读取数据完成

简单地总结一下:BootStrap 和 ServerBootStrap 分别负责客户端和服务端的启动,它们是非常强大的辅助工具类;Channel 是网络通信的载体,提供了与底层 Socket 交互的能力。那么 Channel 生命周期内的事件都是如何被处理的呢?那就是 Netty 事件调度层的工作职责了。

(2)事件调度层

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

事件调度层的核心组件包括 EventLoopGroup、EventLoop

  • EventLoopGroup & EventLoop

EventLoopGroup 本质是一个线程池,主要负责接收 I/O 请求,并分配线程执行处理请求。 EventLoopGroups、EventLoop 与 Channel 的关系如图:

从图中可以总结出 EventLoopGroup、EventLoop、Channel 的几点关系。

  • 一个 EventLoopGroup 往往包含一个或者多个 EventLoop。EventLoop 用于处理 Channel 生命周期内的所有 I/O 事件,如 accept、connect、read、write 等 I/O 事件。
  • EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责处理多个 Channel。
  • 每新建一个 Channel,EventLoopGroup 会选择一个 EventLoop 与其绑定。该 Channel 在生命周期内都可以对 EventLoop 进行多次绑定和解绑。

下图是 EventLoopGroup 的家族图谱。可以看出 Netty 提供了 EventLoopGroup 的多种实现,而且 EventLoop 则是 EventLoopGroup 的子接口,所以也可以把 EventLoop 理解为 EventLoopGroup,但是它只包含一个 EventLoop 。

EventLoopGroup 的实现类是 NioEventLoopGroup,NioEventLoopGroup 也是 Netty 中最被推荐使用的线程模型。NioEventLoopGroup 继承于 MultithreadEventLoopGroup,是基于 NIO 模型开发的,可以把 NioEventLoopGroup 理解为一个线程池,每个线程负责处理多个 Channel,而同一个 Channel 只会对应一个线程。

EventLoopGroup 是 Netty 的核心处理引擎,那么 EventLoopGroup 和 Reactor 线程模型到底是什么关系呢?其实 EventLoopGroup 是 Netty Reactor 线程模型的具体实现方式,前面也提到,Netty 通过创建不同的 EventLoopGroup 参数配置,就可以支持 Reactor 的三种线程模型:

  • 单线程模型:EventLoopGroup 只包含一个 EventLoop,Boss 和 Worker 使用同一个 EventLoopGroup;
  • 多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用同一个 EventLoopGroup;
  • 主从多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 是主 Reactor,Worker 是从 Reactor,它们分别使用不同的 EventLoopGroup,主 Reactor 负责新的网络连接 Channel 创建,然后把 Channel 注册到从 Reactor。

有了事件调度层之,可以说 Netty 的发动机已经转起来了,事件调度层负责监听网络连接和读写操作,然后触发各种类型的网络事件,需要一种机制管理这些错综复杂的事件,并有序地执行,接下来我们来看一下 Netty 服务编排层中核心组件的职责。

(3)服务编排层

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

服务编排层的核心组件包括 ChannelPipelineChannelHandler、ChannelHandlerContext

  • ChannelPipeline

ChannelPipeline 是 Netty 的核心编排组件,负责组装各种 ChannelHandler,实际数据的编解码以及加工处理操作都是由 ChannelHandler 完成的。ChannelPipeline 可以理解为ChannelHandler 的实例列表——内部通过双向链表将不同的 ChannelHandler 链接在一起。当 I/O 读写事件触发时,ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。

ChannelPipeline 是线程安全的,因为每一个新的 Channel 都会对应绑定一个新的 ChannelPipeline。一个 ChannelPipeline 关联一个 EventLoop,一个 EventLoop 仅会绑定一个线程。

ChannelPipeline、ChannelHandler 都是高度可定制的组件。开发者可以通过这两个核心组件掌握对 Channel 数据操作的控制权。下面我们看一下 ChannelPipeline 的结构图:

从上图可以看出,ChannelPipeline 中包含入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器,我们结合客户端和服务端的数据收发流程来理解 Netty 的这两个概念。

客户端和服务端都有各自的 ChannelPipeline。以客户端为例,数据从客户端发向服务端,该过程称为出站,反之则称为入站。数据入站会由一系列 InBoundHandler 处理,然后再以相反方向的 OutBoundHandler 处理后完成出站。我们经常使用的编码 Encoder 是出站操作,解码 Decoder 是入站操作。服务端接收到客户端数据后,需要先经过 Decoder 入站处理后,再通过 Encoder 出站通知客户端。所以客户端和服务端一次完整的请求应答过程可以分为三个步骤:客户端出站(请求数据)、服务端入站(解析数据并执行业务逻辑)、服务端出站(响应结果)。

  • ChannelHandler & ChannelHandlerContext

在介绍 ChannelPipeline 的过程中,想必你已经对 ChannelHandler 有了基本的概念,数据的编解码工作以及其他转换工作实际都是通过 ChannelHandler 处理的。站在开发者的角度,最需要关注的就是 ChannelHandler,我们很少会直接操作 Channel,都是通过 ChannelHandler 间接完成。

下图描述了 Channel 与 ChannelPipeline 的关系,从图中可以看出,每创建一个 Channel 都会绑定一个新的 ChannelPipeline,ChannelPipeline 中每加入一个 ChannelHandler 都会绑定一个 ChannelHandlerContext。由此可见,ChannelPipeline、ChannelHandlerContext、ChannelHandler 三个组件的关系是密切相关的,那么你一定会有疑问,每个 ChannelHandler 绑定 ChannelHandlerContext 的作用是什么呢?

ChannelHandlerContext 用于保存 ChannelHandler 上下文,通过 ChannelHandlerContext 我们可以知道 ChannelPipeline 和 ChannelHandler 的关联关系。ChannelHandlerContext 可以实现 ChannelHandler 之间的交互,ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。此外,你可以试想这样一个场景,如果每个 ChannelHandler 都有一些通用的逻辑需要实现,没有 ChannelHandlerContext 这层模型抽象,是不是需要写很多相同的代码呢?

以上便是 Netty 的逻辑处理架构,可以看出 Netty 的架构分层设计得非常合理,屏蔽了底层 NIO 以及框架层的实现细节,对于业务开发者来说,只需要关注业务逻辑的编排和实现即可。

(4)组件关系梳理

当了解每个 Netty 核心组件的概念后。那组件之间又是如何协作的呢?其内部逻辑的流转大致如下图:

  • 服务端启动初始化时有 Boss EventLoopGroup 和 Worker EventLoopGroup 两个组件,其中 Boss 负责监听网络连接事件。当有新的网络连接事件到达时,则将 Channel 注册到 Worker EventLoopGroup。
  • Worker EventLoopGroup 会被分配一个 EventLoop 负责处理该 Channel 的读写事件。每个 EventLoop 都是单线程的,通过 Selector 进行事件循环。
  • 当客户端发起 I/O 读写事件时,服务端 EventLoop 会进行数据的读取,然后通过 Pipeline 触发各种监听器进行数据的加工处理。
  • 客户端数据会被传递到 ChannelPipeline 的第一个 ChannelInboundHandler 中,数据处理完成后,将加工完成的数据传递给下一个 ChannelInboundHandler。
  • 当数据写回客户端时,会将处理结果在 ChannelPipeline 的 ChannelOutboundHandler 中传播,最后到达客户端。。

4.3 Netty 源码结构

Netty 源码分为多个模块,模块之间职责划分非常清楚。如同上文整体功能模块一样,Netty 源码模块的划分也是基本契合的。

我们不仅可以使用 Netty all-in-one 的 Jar 包,也可以单独使用其中某些工具包。下面根据 Netty 的分层结构以及实际的业务场景具体介绍 Netty 中常用的工具包。

(1)Core 核心层模块

netty-common模块是 Netty 的核心基础包,提供了丰富的工具类,其他模块都需要依赖它。在 common 模块中,常用的包括通用工具类自定义并发包

  • 通用工具类:比如定时器工具 TimerTask、时间轮 HashedWheelTimer 等。
  • 自定义并发包:比如异步模型****Future & Promise、相比 JDK 增强的 FastThreadLocal 等。

netty-buffer 模块中Netty 自己实现了的一个更加完备的ByteBuf 工具类,用于网络通信中的数据载体。由于人性化的 Buffer API 设计,它已经成为 Java ByteBuffer 的完美替代品。ByteBuf 的动态性设计不仅解决了 ByteBuffer 长度固定造成的内存浪费问题,而且更安全地更改了 Buffer 的容量。此外 Netty 针对 ByteBuf 做了很多优化,例如缓存池化、减少数据拷贝的 CompositeByteBuf 等。

netty-resover模块主要提供了一些有关基础设施的解析工具,包括 IP Address、Hostname、DNS 等。

(2)Protocol Support 协议支持层模块

netty-codec模块主要负责编解码工作,通过编解码实现原始字节数据与业务实体对象之间的相互转化。如下图所示,Netty 支持了大多数业界主流协议的编解码器,如 HTTP、HTTP2、Redis、XML 等,为开发者节省了大量的精力。此外该模块提供了抽象的编解码类 ByteToMessageDecoder 和 MessageToByteEncoder,通过继承这两个类我们可以轻松实现自定义的编解码逻辑。

netty-handler模块主要负责数据处理工作。Netty 中关于数据处理的部分,本质上是一串有序 handler 的集合。netty-handler 模块提供了开箱即用的 ChannelHandler 实现类,例如日志、IP 过滤、流量整形等,如果你需要这些功能,仅需在 pipeline 中加入相应的 ChannelHandler 即可。

(3)Transport Service 传输服务层模块

netty-transport 模块可以说是 Netty 提供数据处理和传输的核心模块。该模块提供了很多非常重要的接口,如 Bootstrap、Channel、ChannelHandler、EventLoop、EventLoopGroup、ChannelPipeline 等。其中 Bootstrap 负责客户端或服务端的启动工作,包括创建、初始化 Channel 等;EventLoop 负责向注册的 Channel 发起 I/O 读写操作;ChannelPipeline 负责 ChannelHandler 的有序编排,这些组件在介绍 Netty 逻辑架构的时候都有所涉及。

总结

本文分别从代码使用、启动流程、整体结构、逻辑架构以及源码结构等方面对 Netty 的进行了整体的整理介绍,希望可以对使用Netty的同学们有所帮助。