Netty
Netty 是一个高性能、异步事件驱动的 NIO 框架,基于 JAVA NIO 提供的 API 实现。它提供了对TCP、 UDP 和文件传输的支持,作为一个异步 NIO 框架, Netty 的所有 IO 操作都是异步非阻塞的, 通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。
Netty 特性
- 支持多种传输类型(阻塞、非阻塞),统一的API,易于使用。
- 高性能,单线程、IO多路复用、池化技术、直接内存零拷贝。
- 安全性,支持SSL/TLS。
Netty 核心组件
Channel
Channel 是 Java NIO 的一个基本构造。它代表一个到实体( 如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件) 的开放连接,如读操作和写操作。
ChannelFuture
Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
Netty实现的ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(), 将会在对应的操作完成时被调用。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。
简而言之,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。
ChannelHandler
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。
入站事件:
- 连接被激活或失活
- 数据读取
- 用户事件
- 错误事件
出站事件:
- 打开或关闭到远程节点的连接
- 将数据写入或冲刷到socket
事件将流过整个ChannelPipeline。ChannelHandler 是消息的具体处理器,负责处理读写操作、客户端连接等事情。
ChannelPipeline是ChannelHandler的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
EventLoop
EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
EventLoopGroup
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程)。
并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
服务端的EventLoopGroup 通常包含两个,一个BossEventLoopGroup,一个WorkerEventLoopGroup。
BossEventLoopGroup 处理客户端的连接,处理完成后交由WorkerEventLoopGroup 处理。
WorkerEventLoopGroup 负责处理与客户端之间的 IO 操作。
Bootstrap/ServerBootstrap
Bootstrap 是客户端引导类。ServerBootstrap 是服务端引导类。
Bootstrap 主要用于设置EventLoopGroup ,channel,远程主机地址,ChannelInitializer 以及连接远程主机。
public void start() throws InterruptedException {
//事件处理
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
//用于引导和初始化客户端
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
//连接到服务器
ChannelFuture future = bootstrap.connect().sync();
future.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully().sync();
}
}
ServerBootstrap 主要用于设置EventLoopGroup ,channel,本地监听端口,ChannelInitializer 以及绑定本地端口。
public void start() throws InterruptedException {
//事件处理
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
//用于引导和绑定服务器
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
//绑定服务器
ChannelFuture future = bootstrap.bind().sync();
future.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully().sync();
}
}
Netty 线程模型
大部分网络框架都是基于 Reactor 模式设计开发的,Netty 也不例外。
Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。
常用的 Reactor 线程模型有三种, Reactor 单线程模型, Reactor 多线程模型, 主从 Reactor 多线程模型。
Reactor 单线程模型
Reactor 单线程模型,指的是所有的 IO 操作都在同一个 NIO 线程上面完成, NIO 线程的职责如下:
- 作为 NIO 服务端,接收客户端的 TCP 连接;
- 作为 NIO 客户端,向服务端发起 TCP 连接;
- 读取通信对端的请求或者应答消息;
- 向通信对端发送消息请求或者应答消息。
总结:连接服务端(处理客户端连接)的工作和与远程节点进行其他请求响应操作(数据读写、编解码)都由一个线程完成。
Reactor模式使用的是同步非阻塞IO(NIO),所有的IO操作都不会导致阻塞,理论上一个线程可以独立的处理所有的IO操作(selector会主动去轮询哪些IO操作就绪)。从架构层次看,一个NIO线程确实可以完成其承担的职责,比如上图的Acceptor类接收客户端的TCP请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer转发到指定的handler上,进行消息的处理。
但是不适合高负载、大并发的应用场景,主要原因如下:
- 一个NIO线程处理成千上万的链路,性能无法支撑
- 当NIO线程负载过重,处理性能就会变慢,导致大量客户端连接超时然后重发请求,导致更多堆积未处理的请求
- 可靠性低,只有一个NIO线程,万一线程假死或则进入死循环,就完全不可用了
Reactor 多线程模型
Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操作。 有专门一个NIO 线程-Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求;网络 IO 操作-读、写等由一个 NIO 线程池负责, 线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送。
但如果连接请求包含安全认证的需求,在高负载的情况下可能导致Acceptor 线程性能无法支撑。
主从 Reactor 多线程模型
服务端用于接收客户端连接的不再是个 1 个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责SocketChannel 的读写和编解码工作。 Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。
Netty 高性能
Netty 高性能主要体现在以下几个方面:
IO多路复用和NIO
Netty 通过Reactor 模式进行线程的设计和实现。Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。
零拷贝
- Netty 的接收和发送 ByteBuf 支持 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty 提供了组合 Buffer 对象(CompositeByteBuf ),可以聚合多个 ByteBuf 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的Buffer。
- Netty的文件传输采用了FileChannel.transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
无锁设计、线程绑定
Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。
表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
高性能序列化
Netty 默认提供了对 Google Protobuf 的支持,通过扩展 Netty 的编解码接口,用户可以实现其它的高性能序列化框架,例如 Thrift 的压缩二进制编解码框架。
protocol buffer 是 google 的一个开源项目,它是用于结构化数据串行化的灵活、高效、自动的方法,例如 XML, 不过它比 XML 更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。