一、Netty 简介
Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 I/O
程序。它是一个基于 NIO
的网络编程框架,相比于 Java 原生的 NIO
,它具有如下的优势:
- 支持三种
I/O
模型同时支持三种 Reactor 模型。 - 支持很多应用层的协议,提供了很多编解码器。
- 能够很好地解决 TCP 长连接所带来的缺陷(粘包、半包等)。
- 提供了应用层的 Keep Alive 机制。
- 有更好的性能。
此外,Netty 具有如下特性:
- 统一的 API,支持多种传输类型,阻塞的和非阻塞的。
- 真正的无连接数据报套接字支持。
- 链接逻辑组件以支持复用。
- 得益于池化和复用,拥有更低的资源消耗。
- 简单而强大的线程模型,即主从 Reactor 多线程模型。
二、Netty 线程模型
在介绍 Netty 的线程模型之前,首先需要了解下 Reactor 模型,Reactor 模型基于事件驱动,特别适合处理海量的 I/O
事件。根据 Reactor 的数量和处理资源池的线程数量的不同,Reactor 模型有以下 3 种不同的实现:
- 单 Reactor 单线程模型。
- 单 Reactor 多线程模型。
- 主从 Reactor 多线程模型。
2.1 单 Reactor 单线程模型
单 Reactor 单线程模型指所有的 I/O
操作都在同一个 NIO
线程上完成,NIO
线程的职责如下:
- 作为
NIO
服务端,接收客户端的 TCP 连接。 - 作为
NIO
客户端,向服务端发起 TCP 连接。 - 读取通信对端的请求或者应答消息。
- 向通信对端发送消息请求或者应答消息。
单 Reactor 单线程模型图如下所示:
通过 Reactor 的 select
监听客户端的请求事件,若是连接请求事件,则由 Acceptor
通过 accept
处理连接请求,然后创建一个 Handler
对象处理后续的业务请求。若不是连接请求事件,则通过 Reactor 的 dispatch
调用该连接对应的 Handler
进行业务处理,即通过 dispatch
将对应的 ByteBuffer
分发到连接对应的 Handler
上进行消息解码,最后 NIO
线程将需要回送的消息进行编码发送给客户端。
单 Reactor 单线程不存在多线程、进程通信、竞争的问题。但因为只有一个线程同时处理成百上千的链路,无法发挥多核 CPU 的优势,且若 Handler
在处理一个耗时的业务时,会导致整个进程无法处理其他的连接事件,造成客户端连接超时和超时重发,最终导致大量消息积压和处理超时。此外,若线程意外终止,或陷入死循环,会导致整个系统的通信模块不可用,不能接收和处理外部消息,从而造成结点故障。
综上所述,单 Reactor 单线程适合小容量应用场景,如客户端连接数较少、业务处理迅速。
2.2 单 Reactor 多线程模型
单 Reactor 多线程模型如下图所示,其具有如下特点:
- 有专门的一个
NIO
线程Acceptor
线程用于监听服务端口,接收客户端的 TCP 连接请求。 - 网络
I/O
操作(读或写等)由一个NIO
线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些NIO
线程负责消息的读取、解码、编码和发送。 - 1 个
NIO
线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个NIO
线程,避免发生并发操作问题。 - Reactor 依然是以单线程的方式运行,在客户端高并发连接场景或者服务端需要对客户端握手进行安全认证的情况下容易出现性能瓶颈。
- Worker 线程池会存在多线程的数据共享和竞争。
根据上图,单 Reactor 多线程模型的工作过程如下:
1)Reactor
对象通过 select
监控客户端的请求事件,根据收到的事件类型,使用 dispatch
进行分发。
2)若是请求连接事件,则分发给 Acceptor
通过 accept
处理连接请求,然后创建一个 Handler
用于处理后续的业务。
3)若不是请求连接事件,则分发给连接对应的 Handler
进行处理。
4)Handler
只负责响应事件,它通过 read
读取数据后,会分发给 Worker
线程池中的任意一个线程进行业务处理。
5)Worker
线程池在线程可用的情况下,分配独立的线程完成业务过程,并将处理结果返回给 Handler
。
6)Handler
收到响应后,通过 send
将结果返回给 client。
2.3 主从 Reactor 多线程模型
主从 Reactor 多线程模型如下图所示,其具有如下特点:
- 服务端用于接收客户端连接的不再是个 1 个单独的
NIO
线程,而是一个独立的NIO
线程池。 Acceptor
接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel
注册到 SubReactor 的某个I/O
线程上,由它负责SocketChannel
的读写和编解码工作。Acceptor
线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到 SubReactor 线程池的I/O
线程上,由I/O
线程负责后续的I/O
操作。- Reactor 主线程即 MainReactor,它可以对应多个 Reactor 子线程即 SubReactor。
根据上图,主从 Reactor 多线程模型的工作过程如下:
1)Reactor 主线程的 MainReactor 对象通过 select
监听客户端的连接请求事件,通过 Acceptor
处理连接请求事件。
2)当 Acceptor 处理连接事件后,MainReactor 创建 SocketChannel
,并将其注册到 SubReactor。
3)SubReactor 监听连接队列中的连接,并创建对应的 Handler
进行各种事件的处理。
4)当连接队列中的连接有新事件发生时,SubReactor 会调用对应的 Handler
处理。
5)Handler
通过 read
读取数据,并分发给 Work 线程池中的任意一个 Worker 线程处理。
6)Worker 线程池分配独立的 Worker 线程进行业务处理,并返回结果给 Handler
。
7)Handler
收到响应结果后,再通过 send
将结果返回给客户端。
2.4 Reactor 模型小结
Reactor 可类比为生活中的前台接待员和服务员。
- 单 Reactor 单线程模型,可类比为:前台接待员和服务员是同一个人,全程提供一条龙服务。
- 单 Reactor 多线程模型,可类比为:只有一个前台接待员,多个服务员,前台接待员只负责接待。
- 主从 Reactor 多线程模型,可类比为多个前台接待员,多个服务员。
Reactor 模型具有如下优点:
- 响应快,不必为单个同步时间所阻塞,即使 Reactor 本身是同步的。
- 可以最大程度地避免复杂的多线程与同步问题,且避免了多线程/进程间的上下文切换。
- 扩展性好,可方便地通过增加 Reactor 实例的个数来充分利用 CPU 资源。
- 复用性好,Reactor 模型本身与具体事件的处理逻辑无关。
三、Netty 线程模型
Netty 线程模型是在主从 Reactor 多线程模型的基础上进行改进和优化的。其模型图如下图所示:
从上图可以看到,Netty 抽象出两组线程池,其中 BossGroup
负责接收客户端的连接请求,WorkerGroup
负责网络读写请求,即具体的业务处理。BossGroup
和 WorkerGroup
的类型都是 NioEventLoopGroup
,每个 NioEventLoopGroup
相当于一个事件循环组,在这个事件循环组中包含多个事件循环,每个事件循环都是 NioEventLoop
,即一个不断循环地执行任务的线程。此外,每个 NioEventLoop
都有一个 Selector
,用于监听绑定在其上的网络通讯。
BossGroup
中的 NioEventLoop
循环执行的步骤如下:
1)轮询 Accept
事件,即客户端请求连接事件。
2)处理 Accept
事件,与 client 建立通讯连接,并生成 SocketChannel
,进而将其包装成 NioSocketChannel
,再 NioSocketChannel
注册到 WorkGroup
中的任意一个 NioEventLoop
的 Selector
中。
3)处理任务队列中的任务,即 runAllTasks
。
WorkGroup
中的 NioEventLoop
循环执行的步骤如下:
1)轮询 read
和 write
事件。
2)处理 I/O
事件,即 read
或 write
事件,在该连接对应的 NioSocketChannel 中处理。
3)处理任务队列中的任务,即 runAllTasks
。
在图中还存在一个 ChannelPipeline
即处理器管道。每个 WorkGroup
在处理业务时,都会使用它。ChannelPipeline
是一个包含了 ChannelHandlerContext
的双向链表,每个 ChannelHandlerContext
都关联着一个 ChannelHandler 处理器。WorkerGroup
在进行业务处理时,会经过 ChannelPipeline
中的 ChannelHandler
的一系列处理(解码,处理业务,编码)。
四、Netty 入门程序
项目的目录结构如下所示:
首先编写服务端程序。
/**
* 配置 Netty 服务端信息
*/
public class MyServer {
// 服务端需要监听的端口
private static final int port = 9999;
public static void main(String[] args) {
// 创建两个线程组 bossGroup 和 workGroup
// bossGroup 只负责 accept 客户端的请求,即只处理连接请求
// workGroup 处理客户端的业务请求
// 两者都是无限循环的线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
// 服务端启动引导类 ServerBootstrap
ServerBootstrap bootstrap = new ServerBootstrap();
try {
// 配置服务端启动引导类
bootstrap.group(bossGroup,workGroup) // 配置两个线程组 bossGroup 和 workGroup
.channel(NioServerSocketChannel.class) // 使用 NioServerSocketChannel 作为服务端的通道实现
// 服务端处理客户端连接请求是顺序处理的,即同一时间只能处理一个客户端连接
// 当多个客户端到来时,将不能处理的客户端连接请求放在等待队列中,所以 ChannelOption.SO_BACKLOG 用于配置该等待队列的大小
.option(ChannelOption.SO_BACKLOG,128)
// 若配置该选项,若建立的连接在 2 小时内没有数据通信,TCP 会自动发送一个活动探测数据报文来测试连接状态
// 因此,该选项用于长时间没有数据交流的连接
.childOption(ChannelOption.SO_KEEPALIVE,true)
// 配置 workGroup 线程组的处理器,即实际上是配置 ChannelPipeline 中的 ChannelHandler 链
.childHandler(new MyServerChannelInitializer());
// 异步绑定需要监听的服务端口,并生成一个 ChannelFuture 对象
ChannelFuture channelFuture = bootstrap.bind(port).sync();
// 为 ChannelFuture 添加监听器,即端口绑定成功时的操作,重写 operationComplete 方法回调即可
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("Myserver is ready............");
}
}
});
// 对通过 Channel 关闭的动作进行异步监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("throw exceptions: " + e.getMessage());
} finally {
// 关闭连接
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
/**
* 配置客户端和服务端的通道连接对象为 SocketChannel 和 ChannelPipeline 中的 ChannelHandler 链
*/
public class MyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 配置 ChannelPipeline 的 handler,从 ChannelPipeline 的 tail 前插入
pipeline.addLast(new MyServerHandler());
}
}
/**
* 配置处理器 ChannelHandler
*/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/*
* @MethodName: channelRead
* @param: ctx 上下文对象,即 ChannelPipeline 中实际存储的对象,通过该上下文对象可以得到对象的 channelHandler
* @param: msg 客户端发送的消息,默认是 Object
* @Description: TODO 每当有消息产生时,都会回调该方法,即服务端实际的业务处理过程
* @Throws:
**/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 将 msg 转换成 ByteBuf
ByteBuf byteBuf = (ByteBuf) msg;
String clientMsg = byteBuf.toString(StandardCharsets.UTF_8);
System.out.println("客户端的远程地址:" + ctx.channel().remoteAddress());
System.out.println("客户端:" + clientMsg);
}
/*
* @MethodName: channelReadComplete
* @param: ctx
* @Description: TODO 服务端数据读取完毕后的回调执行
* @Throws:
**/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 等价于 write() + flush() 方法的结合,即将数据写入通道,并刷新到客户端
// 此时通过一个非池化对象 Unpooled,可视为 Netty 提供的一个专门操作缓冲区的工具类
// copiedBuffer(CharSequence string, Charset charset) 通过给定的数据和字符编码集返回一个 ByteBuf 对象
ctx.channel().writeAndFlush(
Unpooled.copiedBuffer("Hello, client, I am server", StandardCharsets.UTF_8)
);
}
/*
* @MethodName: exceptionCaught
* @param: ctx
* @param: cause
* @Description: TODO 异常发生时的方法回调,若不处理,会将异常传递给 ChannelPipeline 中的下一个 ChannelHandler,若一直没有处理,则会传递到 ChannelPipeline 的尾部,所以程序中应至少提供一个重写了 exceptionCaught() 方法的 ChannelHandler
* @Throws:
**/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("throw exception:" + cause.getMessage());
ctx.close();
}
}
接下来是客户端程序。
/**
* 配置 Netty 客户端信息
*/
public class MyClient {
private static final String serverHost = "127.0.0.1";
private static final int port = 9999;
private static final InetSocketAddress serverAddress = new InetSocketAddress(serverHost, port);
public static void main(String[] args) {
EventLoopGroup clientGroup = new NioEventLoopGroup();
// 客户端启动的引导类 Bootstrap,与服务端的不同
Bootstrap bootstrap = new Bootstrap();
try {
// 因为只有一个 EventLoopGroup 线程组,所以不存在 childHandler 和 ChildOption
bootstrap.group(clientGroup)
.channel(NioSocketChannel.class)
.handler(new MyClientChannelInitializer());
ChannelFuture channelFuture = bootstrap.connect(serverAddress).sync();
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
System.out.println("Myclient is ready...............");
}
});
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("throw exceptions: " + e.getMessage());
} finally {
clientGroup.shutdownGracefully();
}
}
}
public class MyClientChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MyClientHandler());
}
}
public class MyClientHandler extends ChannelInboundHandlerAdapter {
/*
* @MethodName: channelActive
* @param: ctx
* @Description: TODO 当 Channel 就绪时,会回调该方法
* @Throws:
**/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 给服务端回送消息
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello, server, I am client",StandardCharsets.UTF_8));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
String serverMsg = byteBuf.toString(StandardCharsets.UTF_8);
System.out.println("服务端的远程地址:" + ctx.channel().remoteAddress());
System.out.println("服务端:" + serverMsg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("throw exception:" + cause.getMessage());
ctx.close();
}
}
分别启动 MyServer 和 MyClient,测试结果如下图所示: