Netty 概括:

  • Netty 是一个基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
  • 它极大地简化并优化了 TCP、UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面都要更好。
  • 支持多种协议,如 FTP、STMP、HTTP 以及各种二进制和基于文本的传统协议

    Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,稳定性和灵活性的方法

很多开源醒目比如:Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty

优点:

  • 统一的 API,支持多种传输类型,阻塞和非阻塞的
  • 简单而强大的线程模型
  • 自带编解码器解决 TCP 粘包 / 拆包问题
  • 自带各种协议栈
  • 真正的无连接数据包套接字支持
  • 比直接使用 Java 核心 API 有 更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制
  • 安全性不错,有完整的 SSL / TLS 以及 StartTLS 支持
    社区活跃
  • 成熟稳定,经历了大型项目的使用和考研

Netty 应用场景:
Netty 主要用来做网络通信:

  • 作为 RPC 框架的网络通信工具

    我们在分布式系统中,不同服务节点之间经常需要相互调⽤,这个时候就需要 RPC 框架了。 不同服务节点之间的通信是如何做的呢?可以使⽤ Netty来做。

  • 实现自己的 HTTP 服务器:

    ⼀个最基本的 HTTP 服务器可要以处理常⻅的 HTTP Method 的请求,⽐如 POST 请求、GET 请求等等

  • 实现即时通讯系统

  • 实现消息推送系统

Netty 核心组件:

  • Channel:

    Channel 接口时 Netty 对网络操作抽象类,它包括基本的 I/O 操作,如 bind()connect()read()write()

    比较常用的 Channel 接口实现类是 > NioServerSocketChannel (服务端)和 > NioSocketChannel (客户端),这两个 > Channel 可以和 BIO 编程模型中的 > ServerSocket 以及 > Socket 两个概念对应上, Netty 的 > Channel 接口所提供的 API,大大降低了直接使用 Socket 类的复杂性

  • EventLoop:

    EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。 ChannelEventLoop 的联系: Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的 Channel 处理 I/O 操作,两者配置参与 I/O 操作

  • ChannelFulter:

    Netty 是异步非阻塞的,所有 I/O 操作都为异步的。因此不能立即得到操作是否执行成功。 可以通过Channel接口的 addListener() 方法注册一个 ChannelFutureListener ,当操作执行成功或者失败时,监听就会自动触发返回结果。 并且还可以通过 ChannelFulturechannel() 方法获取关联的 Channel

  • ChannelHandlerChannelPipeLine

    1. bootstrap.group(eventLoopGroup)
    2. .handler(new ChannelInitializer<SocketChannel>() {
    3. @Override
    4. protected void initChannel(SocketChannel ch) {
    5. ch.pipeline().addLast(newNettyKryoDecoder(kryoSerializer, RpcResponse.class));
    6. ch.pipeline().addLast(newNettyKryoEncoder(kryoSerializer, RpcRequest.class));
    7. ch.pipeline().addLast(new KryoClientHandler());
    8. }
    9. });

    ChannelHandler 是消息的具体处理器。它负责处理读写操作、客户端连接等事情。 ChannelPipelineChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出战事件流的 API,当 Channel 被创建时,它会被自动地分配到它专属地 ChannelPipeline 可以在 ChannelPipeline 上通过 addLast() 方法添加一个或多个 ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理,当一个 ChannelHandler 处理之后就会把数据交给下一个 ChannelHandler

EventLoopGroupEventLoop

EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程), EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行 I/0 操作的处理

并且 EventLoop 处理的 I/O 事件都将在它专业的 Thread 上被处理,即 ThreadEventLoop 属于 1 : 1 的关系,从而保证线程安全

当客户端通过connect方法连接服务端时, bossGroup 处理客户端连接请求。 当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 I/O 相关操作

BootstrapServerBootstrap

  • Bootstrap 是客户端的启动引导类 / 辅助类
  • ServerBootstrap 客户端的引导启动类 / 辅助类
    • Bootstrap 通常使用 conect() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外 Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端
    • ServerBootstrap 通常使用 bind() 方法绑定到本地的端口上,然后等待客户端的连接
    • Bootstrap 只需要配置一个线程组—— EventLoopGroup,而 ServerBootstrap 需要配置两个线程组——EventLoopGroup,一个用于接收连接,一个用于具体的处理

NioEventLoopGroup 默认的构造函数实际会起的线程数为 CPU * 2

  1. public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {
  2. ......
  3. private static final int DEFAULT_EVENT_LOOP_THREADS;
  4. static {
  5. DEFAULT_EVENT_LOOP_THREADS = Math.max(
  6. 1,
  7. SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)
  8. );
  9. ......
  10. }
  11. ......
  12. }

Netty 线程模型:

大部分网络框架都是基于 Reactor 模式开发的。 Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 I/O 的场景

Netty 主要靠 NioEventLoopGroup线程池来实现具体的线程模型。

在实现服务端的适合,一般会初始化两个线程组:

  • bossGroup :接收连接
  • workerGroup :负责具体的处理,交由对应的 Handler 处理

线程模型:

  • 单线程模型:

    一个线程需要执行处理所有的 accpetreaddecodeprocessencodesend 事件。对与高负债、高并发,并且性能要求比比较高的场景不适用

    使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 * 2

  • 多线程模型

    一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理: acceptreaddecodeprocessencodesend 事件。 满足绝大部分应用场景,但是遇到并发连接大的时候就可能出现问题,成为性能瓶颈。

  • 主从多线程模型

    从一个主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其它线程负责后续的接入认证等工作。 连接建立完成后, Sub NIO 线程池负责具体处理 IO 读写。如果多线程模型无法满足具体需求的时候,可以考虑使用主从线程模型。

Netty 服务端和客户端的启动过程:
服务端:

  1. 首先创建两个 NioEventLoopGroup 对象实例:
    • bossGroup :用于处理客户端的 TCP 连接请求
    • workerGroup :负责每一条连接的具体读写数据的处理逻辑,真正负责 IO 读写操作,交友对应的 Handler 处理。
  2. 创建一个服务端启动引导/辅助类: ServerBootstrap ,这个类将引导我们进行服务端的启动工作
  3. 通过 .group() 方法黑引导类 ServerBootstrap 配置两大线程组,确定了线程模型
  4. 通过 channel() 方法给引导类 ServerBootstrap 指定了 IO 模型为 NIO
    • NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的 ServerSocket 对应
    • NioSocketChannel :指定客户端的 IO 模型为 NIO,与 BIO 编程模型中 Socket 对应
  5. 通过 .childHandler() 给引导类创建一个 ChannelInitializer ,然后指定了服务端消息的业务处理逻辑 HelloServerHandler 对象
  6. 调用 ServerBootstrap 类的 bind() 方法绑定端口

客户端:

  1. 创建一个 NioEventLoopGroup 对象实例
  2. 创建客户端启动的引导类是 Bootstrap
  3. 通过 .group 方法给引导类 Bootstrap 配置一个线程组
  4. 通过 channel() 方法给引导类 Bootstrap 指定了 IO 模型为 NIO
  5. 通过 .childHandler() 给引导类创建一个 ChannelInitializer ,然后指定了客户端消息的业务处理逻辑 HelloClientHandler 对象
  6. 调用 Bootstrap 类的 connect() 方法进行连接,需要的参数: inetHostinetPost

TCP 粘包/拆包:

TCP 粘包/拆包 就是你基于 TCP 发送数据的时候,出现了多个字符串“粘”在了⼀起或者⼀个字符串被“拆”开的问题。

解决办法:

  • 使用 Netty 自带的解码器:
    • LineBasedFrameDecoder :发送端发送数据包的时候,每个数据包之间以换行符作为分割,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取
    • DelimiterBasedFrameDecoder :可以自定义分隔符解码器, LineBasedFrameDecoder 实际上一种特殊的 DelimiterBasedFrameDecoder 解码器
    • FixedLengthFrameDecoder :固定长度解码器,它能够安装指定大的长度对消息进行相应的拆包
    • LengthFieldBasedFrameDecoder

心跳机制:

原因:在 TCP 保持⻓连接的过程中,可能会出现断⽹等⽹络异常出现,异常发⽣的时候, client 与server 之间如果没有交互的话,它们是⽆法发现对⽅已经掉线的。 工作原理:在 client 与 server 之间在⼀定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送⼀个特殊的数据包给对⽅, 当接收⽅收到这个数据报⽂后, 也⽴即发送⼀个特殊的数据报⽂, 回应发送⽅, 此即⼀个 PING-PONG 交互

Netty 长连接、心跳机制:

TCP 实际上⾃带的就有⻓连接选项,本身是也有⼼跳包机制,也就是 TCP 的选项: SO_KEEPALIVE 。 但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。 通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler

Netty 的零拷贝:

零拷贝(Zero-copy)技术是指计算机执⾏操作时,CPU 不需要先将数据从某处内存复制到另⼀个特定区域。这种技术通常⽤于通过⽹络传输⽂件时节省 CPU 周期和内存带宽

在 OS 层面上的 Zero-copy 通常指避免用户态与内核态之间来回拷贝数据,而在 Netty 层面,零拷贝主要体现在对于数据操作的优化。

Netty 中的零拷贝体现在以下几个方面:

  • 使用 Netty 提供的 CompositeBuf 类,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf ,避免了各个 ByteBuf 之间的拷贝
  • ByteBuf 支持 slice 操作,因此可以将 ByteBuf 分解为多个共享同一存储区域的ByteBuf,避免了内存的拷贝
  • 通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输,可以直接将文件缓冲区的实际发送到目标 Channel ,避免了传统通过循环 write 方式导致的内存拷贝问题

IO 多路复用:
IO 多路复用底层主要用的 Linux 内核函数(select、poll、epoll)来实现,windows 不支持 epoll 实现,windows底层是基于 winsock2的select 函数(不开源)

select poll epoll (JDK1.5 及以上)
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO 效率 每次调用都进行线性遍历,时间复杂度为 O(n) 每次调用都进行线性遍历,时间复杂度为 O(n) 事件通知方式,每当有 IO 事件就绪,系统注册的回调函数就会被调用,时间复杂度为 O(1)
最大连接 有上限 无上限 无上限

epoll