一、Netty介绍
1.原生NIO存在的问题
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
- 需要具备其他的额外技能:要熟悉Java多线程变成,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
- JDK NIO的Bug:Epoll Bug,它会导致Selector空轮询,最终导致CPU100%。直到JDK1.7版本该问题仍然存在,没有被根本解决
2.Netty概述
Netty是有JBOSS提供的一个Java开源框架。Netty提供异步的、急基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络IO程序。Netty是一个基于NIO的网络编程框架,使用Netty可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了NIO的开发过程。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的Elasticsearch、Dubbo框架内部都采用了Netty。
具备如下优点:
- 设计优雅,提供阻塞和非阻塞的Socket;提供灵活可拓展的事件模型;提供高度可定制的线程模型
- 具备更高的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制,减少资源的消耗。
- 提供安全传输特性
- 支持多种主流协议;预置多种编解码功能,支持用户开发私有协议
二、线程模型
1.线程模型基本介绍
不同的线程模式,对程序的性能有很大影响。
目前存在的线程模型有:
- 传统阻塞I/O服务模型
- Reactor模型,根据Reactor的数量和处理资源池线程的数量不同,有3中典型的实现
- 单Reactor 单线程
- 单Reactor 多线程
- 主从Reactor 多线程
2.传统阻塞I/O服务模型
采用阻塞IO模式获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理和数据返回工作。
存在问题:
- 当并发数很大,就会创建大量的线程,占用很大系统资源
- 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费
3.Reactor 模型
Reactor模型,通过一个或多个输入同时传递给服务处理器的模式,服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此Reactor模式也叫Dispatcher模式。Reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理的关键。
1)单Reactor单线程
- Selector是可以实现应用程序通过一个阻塞对象监听多路连接请求
- Reactor对象通过Selector监控客户端请求事件,收到事件后通过Dispatch进行分发
- 是建立连接请求事件,则由Acceptor通过Accpet处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理
- Handler会完成Read–>业务处理–>Send的完整业务流程
优点:
- 模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点:
- 性能问题:只有一个线程,无法完全发挥多核CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
- 可靠性问题:线程意外终止或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
2)单Reactor多线程
- Reactor对象通过Selector监控客户端请求事件,收到事件后,通过dispatch进行分发
- 如果建立连接请求,则右Acceptor通过accept处理连接请求
- 如果不是连接请求,则由reactor分发调用连接对应的handler来处理
- handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分给后面的worker线程池的某个线程处理业务
- worker线程池会分配独立线程完成真正的业务,并将结果返回给handler
- handler收到响应后,通过send将结果返回给client
优点:
- 可以充分的利用多核CPU的处理能力
缺点:
- 多线程数据共享和访问比较复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈
3)主从Reactor多线程
- Reactor主线程MainReactor对象通过select监听客户端连接事件,收到事件后,通过Acceptor处理客户端连接事件
- 当Acceptor处理完成客户端连接事件之后(与客户端建立好Socket连接),MainReactor将连接分配给SubReactor。(即MainReactor只负责监听客户端连接请求,和客户端建立连接之后将连接交由SubReactor监听后面的IO事件)
- SubReactor将连接加入到自己的连接队列进行监听,并创建Handler对各种事件进行处理
- 当连接上有新事件发生的时候,SubReactor就会调用对应的Handler处理
- Handler通过read从连接上读取请求数据,将请求数据分发给Worker线程池进行业务处理
- Worker线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给Handler。Handler通过send向客户端发送响应数据
- 一个MainReactor可以对应多个SubReactor,即一个MainReactor线程可以对应多个SubReactor线程
优点:
- MainReactor线程与SubReactor线程的数据交互简单职责明确,MainReactor线程只需要接收新连接,SubReactor线程完成后续的业务处理
- MainReactor线程与SubReactor线程的数据交互简单,MainReactor线程只需要把新连接传给SubReactor线程,SubReactor线程无需返回数据
- 多个SubReactor线程能够应对更高的并发请求
缺点:
- 变成复杂度高
4.Netty线程模型
Netty的设计主要基于主从Reactor多线程模式,并做了一定的改进
1)简单版Netty模型
- BossGroup线程维护Selector,ServerSocketChannel注册到这个Selector上,只关注连接建立请求事件(主Reactor)
- 当接收到来自客户端的连接建立请求事件的时候,通过ServerSocketChannel.accept方法获得对应的SocketChannel,并封装成NioSocketChannel注册到WorkerGroup线程中的Selector,每个Selector运行在一个线程中(从Reactor)
- 当WorkerGroup线程中的Selector监听到自己感兴趣的IO事件后,就调用Handler进行处理
2)进阶版Netty模型
- 有两组线程池:BossGroup和WorkerGroup,BossGroup中的线程专门负责和客户端建立连接,WorkerGroup中的线程专门负责处理连接上的读写
- BossGroup和WorkerGroup含有多个不断循环的执行事件处理的线程,每个线程都包含一个Selector,用于监听注册在其上的Channel
- 每个BossGroup中的线程循环执行以下三个步骤
- 轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT事件)
- 处理accept事件,与客户端建立连接,生成一个NioSocketChannel,并将其注册到WorkerGroup中某个线程上的Selector上
- 再去依次循环处理任务队列中的下一个事件
- 每个WorkerGroupGroup中的线程循环执行以下三个步骤
- 轮询注册在其上的NioSocketChannel的read/write事件(OP_READ/OP_WRITE事件)
- 在对应的NioSocketChannel上处理的read/write事件
- 再去依次循环处理任务队列中的下一个事件
3)详细版Netty模型
- Netty抽象出两组线程池:BossGroup和WorkerGroup,也可以叫做BossNioEventLoopGroup和WorkerNioEventLoopGroup。每个线程池中都有NioEventLoop线程。BossGroup中的线程专门负责和客户端建立连接,WorkerGroup中的线程专门负责处理连接上的读写。BossGroup和WorkerGroup的类型都是NioEventLoopGroup
- NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个NioEventLoop
- NioEventLoop表示一个不断循环的执行事件处理的线程,每个NioEventLoop都包含一个Selector,用于监听注册在其上的Socket网络连接(Channel)
- NioEventLoopGroup可以含有多个线程,即可以含有多个NioEventLoop
- 每个BossNioEventLoop中循环执行以下三个步骤
- select:轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT事件)
- processSelectedKeys:处理accept事件,与客户端建立连接,生成一个NioSocketChannel,并将其注册到某个WorkerNioEventLoop上的Selector上
- runAllTasks:再去依次循环处理任务队列中的其他任务
- 每个WorkerNioEventLoop中循环执行以下三个步骤
- select:轮询注册在其上的NioSocketChannel的read/write事件(OP_READ/OP_WRITE事件)
- processSelectedKeys:在对应的NioSocketChannel上处理read/write事件
- runAllTasks:再去依次循环处理任务队列中的其他任务
- 在以上两个processSelectedKeys步骤中,会使用Pipeline(管道),Pipeline中引用了Channel,即通过Pipeline可以获取到对应的Channel,Pipeline中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)
三、核心API介绍
1.ChannelHandler及其实现类
ChannelHandler接口定义了许多事件处理的方法,我们可以通过重写这些方法去实现具体的业务逻辑。
API关系如下图所示:
Netty开发中需要自定义一个Handler类去实现ChannelHandler接口或其子接口或其实现类,然后通过重写相应方法实现业务逻辑,我们接下来看看一般需要重写哪些方法
方法 | 解释 |
---|---|
public void channelActive(ChannelHandler Context ctx) | 通道就绪事件 |
public void channelRead(ChannelHandlerContext ctx,Object msg) | 通道读取数据事件 |
public void channelReadComplete(ChannelHandlerContext ctx) | 数据读取完毕事件 |
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) | 通道发生异常事件 |
2.ChannelPipeline
ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的责任链。
如果客户端和服务器的Handler是一样的,消息从客户端到服务端或者反过来,每个Inbound类型或OutBound类型的Handler只会经过一次,混合类型的Handler(实现了Inbound和OutBound的Handler)会经过两次。准确的说ChannelPipeline中是一个ChannelHandlerContext,每个上下文对象中有ChannelHandler。InboundHandler是按照Pipeline的加载顺序的顺序执行,OutboundHandler是按照Pipeline的加载顺序,逆序执行。
3.ChannelHandlerContext
这是事件处理器上下文对象,Pipeline链中的实际处理节点。每个处理节点ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的ChannelPipeline和Channel的信息,方便对ChannelHandler进行调用。常用方法如下所示:
- ChannelFuture clos(),关闭通道
- ChannelOutboundInvoker flush(),刷新
- ChannelFuture writeAndFlush(Object msg),将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)
4.ChannelOption
Netty在创建Channel实例后,一般都需要设置ChannelOption参数。ChannelOption是Socket的标准参数,而非Netty独创的。常用的参数配置有:
- ChannelOption.SO_BACKLOG
- 对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
- ChannelOption.SO_KEEPALIVE,一直保持连接活动状态。该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
5.ChannelFuture
表示Channel中异步I/O操作的结果,在Netty中所有的I/O操作都是异步的,I/O的调用会直接返回,调用并不能立刻获得结果,但是可以通过ChannelFuture来获取I/O操作的处理状态。
常用方法如下所示:
- Channel channel(),返回当前正在进行IO操作的通道
- ChannelFuture sync(),等待异步操作执行完毕,将异步改为同步
6.EventLoopGroup和实现类NioEventLoopGroup
EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。
EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如:BossEventLoopGroup和WorkerEventLoopGroup。通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理,如下图所示:
BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel的Selector实例,BossEventLoop不断轮询Selector将连接事件分离出来,通常是OP_ACCEPT事件,然后将接收到的SocketChannel交给WorkerEventLoopGroup,WorkerEventLoopGroup会由next选择其中一个EventLoopGroup来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件进行处理。
一般情况下我们都是用实现类NioEventLoopGroup
常用方法如下所示:
- public NioEventLoopGroup(),构造方法,创建线程组
- public Future<?> shutdownGracefully(),断开连接,关闭线程
7.ServerBootstrap和Bootstrap
ServerBootStrap是Netty中的服务器端启动助手,通过它可以完成服务器端的各种配置;Bootstrap是Netty中的客户端启动助手,通过它可以完成客户端的各种配置。常用方法如下所示:
方法 | 作用 |
---|---|
public ServerBootstrap group(EventLoopGroup parentGroup,EventLoopGroup childGroup) | 该反复噶用于服务器端,用来设置两个EventLoop |
public B group(EventLoopGroup group) | 该方法用于客户端,用来设置一个EventLoop |
public B channel(Class<? extends C> channelClass) | 该方法用来设置一个服务器端的通道实现 |
public B option(ChannelOption option,T value) | 用来给ServerChannel添加配置 |
public ServerBootstrap childOption(ChnnelOption childOption,T value) | 用来给接收到的通道添加配置 |
8.Unpooled类
这是Netty提供的一个专门用来操作缓冲区的工具类,常用方法如下所示:
- public static ByteBuf copiedBuffer(CharSequence string,Charset charset),通过给定的数据和字符编码返回一个ByteBuf对象(类似于NIO中的ByteBuffer对象)
四、Netty入门案例
Netty是由JBOSS提供的一个Java开源框架,所以在使用的时候首先得导入Netty的maven坐标
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
1.服务端
步骤:
- 创建bossGroup线程组:处理网络事件–连接事件
- 创建workerGroup线程组:处理网络事件–读写事件
- 创建服务端启动助手
- 设置bossGroup线程组和workerGroup线程组
- 设置服务端通道实现为NIO
- 参数设置
- 创建一个通道初始化对象
- 向Pipeline中添加自定义业务处理handler
- 启动服务端并绑定端口,同时将异步改为同步
- 关闭通道和关闭连接池
1)服务端代码
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//1. 创建bossGroup线程组:处理网络事件--连接事件
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//2. 创建workerGroup线程组:处理网络事件--读写事件 2*处理器线程数
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//3. 创建服务端启动助手
ServerBootstrap serverBootstrap = new ServerBootstrap();
//4. 设置bossGroup线程组和workerGroup线程组
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)//5. 设置服务端通道实现为NIO
.option(ChannelOption.SO_BACKLOG,128)//6. 参数设置
.childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)//6. 参数设置
.childHandler(new ChannelInitializer<SocketChannel>() {//7. 创建一个通道初始化对象
@Override
protected void initChannel(SocketChannel channel) throws Exception {
//8. 向Pipeline中添加自定义业务处理handler
channel.pipeline().addLast(new NettyServerHandler());
}
});
//9. 启动服务端并绑定端口,同时将异步改为同步
ChannelFuture future = serverBootstrap.bind(9999).sync();
System.out.println("服务端启动成功");
// 10. 关闭通道(并不是真正意义上的关闭,而是监听通道关闭的状态)和关闭连接池
future.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.isShuttingDown();
}
}
2)自定义服务端handler
public class NettyServerHandler implements ChannelInboundHandler {
/**
* @Author liufj
* @Description 通道读取事件
* @Date 10:05 2024/6/10
**/
@Override
public void channelRead(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("客户端发送过来的消息:"+byteBuf);
}
/**
* @Author liufj
* @Description 通道读取完毕事件
* @Date 10:05 2024/6/10
**/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("这里是Netty服务端", CharsetUtil.UTF_8));//消息出站
}
/**
* @Author liufj
* @Description 通道异常事件
* @Date 10:05 2024/6/10
**/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception {
}
}
2.客户端
步骤:
- 创建线程组
- 创建客户端启动助手
- 设置线程组
- 创建客户端通道实现为NIO
- 创建一个通道初始化对象
- 向Pipeline中添加自定义业务处理handler
- 启动客户端,等待连接服务端,同时将异步改为同步
- 关闭通道和关闭连接池
1)客户端代码
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//1. 创建线程组
NioEventLoopGroup group = new NioEventLoopGroup();
//2. 创建客户端启动助手
Bootstrap bootstrap = new Bootstrap();
//3. 设置线程组
bootstrap.group(group)
.channel(NioSocketChannel.class)//4. 创建客户端通道实现为NIO
.handler(new ChannelInitializer<SocketChannel>() {//5. 创建一个通道初始化对象
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//6. 向Pipeline中添加自定义业务处理handler
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
//7. 启动客户端,等待连接服务端,同时将异步改为同步
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",9999).sync();
//8. 关闭通道和关闭连接池
channelFuture.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
2)自定义客户端handler
public class NettyClientHandler implements ChannelInboundHandler {
/**
* @Author liufj
* @Description 通道就绪事件
* @Date 10:05 2024/6/10
**/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("这里是Netty客户端", CharsetUtil.UTF_8));
}
/**
* @Author liufj
* @Description 通道读就绪事件
* @Date 10:05 2024/6/10
**/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务端发送的消息:"+byteBuf.toString(CharsetUtil.UTF_8));
}
@Override
public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) throws Exception {
}
}
五、Netty异步模型
1.基本介绍
异步的概念和同步相对。当一个线程过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
Netty中的I/O操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture。调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。Netty的异步模型是建立在future和callback之上的。callback就是回调。
Future的核心思想:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,后续可以通过Future去监控fun的处理过程(即Future-Listener机制)
2.Future和Future-Listener
1)Future
表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,ChannelFuture是他的一个子接口。ChannelFuture是一个接口,可以添加监听器,当监听的事件发生时,就会通知到监听器。
当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
常用的方法:
方法 | 介绍 |
---|---|
sync | 阻塞等待程序结果返回 |
isDone | 判断当前操作是否完成 |
isSuccess | 判断已完成的当前操作是否成功 |
getCause | 获取已完成的当前操作失败的原因 |
isCancelled | 判断已完成的当前操作是否被取消 |
addListener | 注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则通知知道的监听器 |
2)Future-Listener机制
给Future添加监听器,监听操作结果
在服务端代码中修改如下:
//9. 启动服务端并绑定端口,使用异步
ChannelFuture future = serverBootstrap.bind(9999);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("端口绑定成功");
}else {
System.out.println("端口绑定失败");
}
}
});
在客户端handler中修改如下:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ChannelFuture future = ctx.writeAndFlush(Unpooled.copiedBuffer("这里是Netty客户端", CharsetUtil.UTF_8));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("数据发送成功");
}else {
System.out.println("数据发送失败");
}
}
});
}