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 操作的处理。Channel
和EventLoop
的联系: Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的 Channel 处理 I/O 操作,两者配置参与 I/O 操作ChannelFulter:
Netty 是异步非阻塞的,所有 I/O 操作都为异步的。因此不能立即得到操作是否执行成功。 可以通过
Channel
接口的addListener()
方法注册一个ChannelFutureListener
,当操作执行成功或者失败时,监听就会自动触发返回结果。 并且还可以通过ChannelFulture
的channel()
方法获取关联的Channel
ChannelHandler
和ChannelPipeLine
bootstrap.group(eventLoopGroup)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(newNettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(newNettyKryoEncoder(kryoSerializer, RpcRequest.class));
ch.pipeline().addLast(new KryoClientHandler());
}
});
ChannelHandler
是消息的具体处理器。它负责处理读写操作、客户端连接等事情。ChannelPipeline
为ChannelHandler
的链,提供了一个容器并定义了用于沿着链传播入站和出战事件流的 API,当Channel
被创建时,它会被自动地分配到它专属地ChannelPipeline
可以在ChannelPipeline
上通过addLast()
方法添加一个或多个ChannelHandler
,因为一个数据或者事件可能会被多个Handler
处理,当一个ChannelHandler
处理之后就会把数据交给下一个ChannelHandler
EventLoopGroup
和 EventLoop
:
EventLoopGroup 包含多个 EventLoop(每一个
EventLoop
通常内部包含一个线程),EventLoop
的主要作用实际就是负责监听网络事件并调用事件处理器进行 I/0 操作的处理并且
EventLoop
处理的 I/O 事件都将在它专业的Thread
上被处理,即Thread
和EventLoop
属于 1 : 1 的关系,从而保证线程安全当客户端通过
connect
方法连接服务端时,bossGroup
处理客户端连接请求。 当客户端处理完成后,会将这个连接提交给workerGroup
来处理,然后workerGroup
负责处理其 I/O 相关操作
Bootstrap
和 ServerBootstrap
:
Bootstrap
是客户端的启动引导类 / 辅助类ServerBootstrap
客户端的引导启动类 / 辅助类- Bootstrap 通常使用 conect() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外 Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端
- ServerBootstrap 通常使用 bind() 方法绑定到本地的端口上,然后等待客户端的连接
- Bootstrap 只需要配置一个线程组—— EventLoopGroup,而 ServerBootstrap 需要配置两个线程组——EventLoopGroup,一个用于接收连接,一个用于具体的处理
NioEventLoopGroup 默认的构造函数实际会起的线程数为 CPU * 2
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {
......
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(
1,
SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)
);
......
}
......
}
Netty 线程模型:
大部分网络框架都是基于 Reactor 模式开发的。 Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 I/O 的场景
Netty 主要靠 NioEventLoopGroup
线程池来实现具体的线程模型。
在实现服务端的适合,一般会初始化两个线程组:
bossGroup
:接收连接workerGroup
:负责具体的处理,交由对应的 Handler 处理
线程模型:
单线程模型:
一个线程需要执行处理所有的
accpet
、read
、decode
、process
、encode
、send
事件。对与高负债、高并发,并且性能要求比比较高的场景不适用使用
NioEventLoopGroup
类的无参构造函数设置线程数量的默认值就是 CPU 核心数 * 2多线程模型
一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理:
accept
、read
、decode
、process
、encode
、send
事件。 满足绝大部分应用场景,但是遇到并发连接大的时候就可能出现问题,成为性能瓶颈。主从多线程模型
从一个主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其它线程负责后续的接入认证等工作。 连接建立完成后, Sub NIO 线程池负责具体处理 IO 读写。如果多线程模型无法满足具体需求的时候,可以考虑使用主从线程模型。
Netty 服务端和客户端的启动过程:
服务端:
- 首先创建两个 NioEventLoopGroup 对象实例:
bossGroup
:用于处理客户端的 TCP 连接请求workerGroup
:负责每一条连接的具体读写数据的处理逻辑,真正负责 IO 读写操作,交友对应的 Handler 处理。
- 创建一个服务端启动引导/辅助类:
ServerBootstrap
,这个类将引导我们进行服务端的启动工作 - 通过
.group()
方法黑引导类ServerBootstrap
配置两大线程组,确定了线程模型 - 通过
channel()
方法给引导类ServerBootstrap
指定了 IO 模型为 NIONioServerSocketChannel
:指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket
对应NioSocketChannel
:指定客户端的 IO 模型为 NIO,与 BIO 编程模型中Socket
对应
- 通过
.childHandler()
给引导类创建一个ChannelInitializer
,然后指定了服务端消息的业务处理逻辑HelloServerHandler
对象 - 调用
ServerBootstrap
类的bind()
方法绑定端口
客户端:
- 创建一个
NioEventLoopGroup
对象实例 - 创建客户端启动的引导类是
Bootstrap
- 通过
.group
方法给引导类Bootstrap
配置一个线程组 - 通过
channel()
方法给引导类Bootstrap
指定了 IO 模型为 NIO - 通过
.childHandler()
给引导类创建一个ChannelInitializer
,然后指定了客户端消息的业务处理逻辑HelloClientHandler
对象 - 调用
Bootstrap
类的connect()
方法进行连接,需要的参数:inetHost
、inetPost
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) |
最大连接 | 有上限 | 无上限 | 无上限 |