声明:本文许多素材取自教程课件。也加入了一些自己的理解。
一 为什么使用netty
1.1 nio存在的问题
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、 ServerSocketChannelSocketChannel、ByteBuffer 等。
- 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
- JDK NIO的Bug:例如臭名昭著的EpollBug,它会导致Selector空轮询,最终导致CPU100%。直到JDK1.7版本该问题仍旧存在,没有被根本解决。
二 netty简介
2.1 简介
- Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、基于
事件驱动
的网络应用程序框架,用以快速开发高性能、高可靠性的网络IO程序 - Netty 可以帮助你快速、简单的开发出一个网络应用,相当于
简化和流程化了NIO
的开发过程
3.Netty 是目前最流行的NIO框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的Elasticsearch、Dubbo 框架内部都采用了Netty。
2.2 优点
- 设计优雅:适用于各种传输类型的统–
API阻塞和非阻塞Socke
t;基于灵活且可扩展的事件模型
,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池
- 使用方便:详细记录的Javadoc, 用户指南和示例;没有其他依赖项,JDK5 (Netty3.x) 或6 (Netty4.x) 就足够了。
- 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
- 安全:完整的SSL/TLS和StartTLS支持。
- 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的Bug可以被及时修复,同时,更多的新功能会被加入
2.3 版本说明
- netty版本分为netty3.x 和netty4.x、 netty5.x
- Netty5已经被官网废弃了,目前推荐使用的是Netty4.x的稳定版本
三 线程模型
3.1 Netty的线程模式
先抛出结论:
Netty线程模式(Netty主要基于主从Reactor多线程模型
做了一定的改进
,其中主从Reactor多线程模型有多个Reactor)
不同的线程模式,对程序的性能有很大影响,为了搞清Netty线程模式,我们来系统的讲解下各个线程模式,最后看看Netty线程模型有什么优越性.
目前存在的线程模型有:
- 传统阻塞I/O服务模型
- Reactor模式
根据Reactor的数量
和处理资源池线程的数量不
同,有3种典型的实现- 单Reactor单线程;
- 单Reactor多线程;
- 主从Reactor多线程
3.2 传统阻塞I/O服务模型
一个线程处理一个请求。当并发数很大,就会创建大量的线程,占用很大系统资源;并且大部分时候线程都不是在工作,而是处于阻塞状态(比如等数据)
3.3 Reactor模型
- 基于
I/O 复用模型
:多个连接共用一个阻塞对象
,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理 - Reactor 对应的叫法: 1. 反应器模式 2. 分发者模式(Dispatcher) 3. 通知者模式(notifier)
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。
核心组成
- Reactor:Reactor 在
一个单独的线程中运行
,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移
到适当的联系人; - Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,
处理程序执行非阻塞操作
。
3.4 单Reactor单线程模型
所有的处理都是由一个线程
来完成,我们学习nio时最简单的demo就是这种模型
方案优缺点分析
- 优点:模型简单,
没有多线程、进程通信、竞争
的问题,全部都在一个线程中完成 - 缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
- 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
使用场景
客户端的数量有限
,业务处理非常快速,比如 Redis在业务处理的时间复杂度 O(1) 的情况
3.5 单Reactor多线程模型
- 一个
专门的NIO线程
–acceptor线程 用于监听服务端,接收客户端的TCP连接请求; - 网络I/O操作—读、写等由
一个NIO线程池
负责·,线程池可以采用标准的JDK线程池实现,它包含·一个任务队列和N个可用的线程·,由这些NIO线程负责消息的读取、解码、编码和发送;(本质还是只有一个Reactor
) - 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。
优缺点
- 优点:可以充分的利用多核cpu 的处理能力
- 缺点:在极特殊应用场景中,
一个NIO线程负责监听和处理所有的客户端连接
可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手信息进行安全认证,认证本身非常损耗性能。这类场景下,单独一个Acceptor线程可能会存在性能不足问题
虽然读写处理得逻辑由线程池负责,但Reactor还是得负责监听读写事件!!!
3.6 主从 Reactor 多线程
- 服务端用于接收客户端连接的
不再是1个单独的NIO线程,而是一个独立的NIO线程池
。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)
的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。 - Acceptor线程池只用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端
subReactor线程池的I/O线程上
,有I/O线程负责后续的I/O操作。
第三种模型比起第二种模型,是将Reactor分成两部分
,mainReactor负责监听server socket,accept新连接,并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket
,读写网络数据,对业务处理功能,其扔给worker线程池完成
。通常,subReactor个数上可与CPU个数等同。
3.7 netty模型
- Netty抽象出
两组线程池
BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写 - BossGroup 和 WorkerGroup
类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个
事件循环是 NioEventLoop
- NioEventLoop 表示一个
不断循环的执行处理任务的线程
, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯 - NioEventLoopGroup 可以有多个线程, 即可以
含有多个NioEventLoop
- 每个Boss NioEventLoop 循环执行的步骤有3步
- 轮询accept 事件
- 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将
其注册到某个worker NIOEventLoop
上的 selector - 处理任务队列的任务 , 即 runAllTasks
- 每个 Worker NIOEventLoop 循环执行的步骤
- 轮询read, write 事件
- 处理i/o事件, 即read , write 事件
,在对应NioScocketChannel 处理
- 处理任务队列的任务 , 即 runAllTasks
- 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中
维护了很多的处理器
小结
- BossGroup 线程维护Selector ,
只关注Accecpt
- 当接收到Accept事件,获取到对应的SocketChannel,
封装
成 NIOScoketChannel并注册到Worker 线程(事件循环)
, 并进行维护
和主从Reactor多线程模型对比
就是去掉线程池的第三种形式
的变种,这也 是Netty NIO的默认模式(说明模式可以切换)。在实现上,Netty中的Boss类充当mainReactor
,NioWorker类充当subReactor
(默认 NioWorker的个数Runtime.getRuntime().availableProcessors()
)。在处理新来的请求 时,NioWorker读完已收到的数据到ChannelBuffer
中,之后触发ChannelPipeline中的ChannelHandler流。(责任链模式)
注意代码是同步的(没有线程池时)
Netty是事件驱动的,可以通过ChannelHandler链来控制执行流向。因为ChannelHandler链的执行过程是在 subReactor中同步的
,所以如果业务处理handler耗时长,将严重影响可支持的并发数
。这种模型适合于像Memcache这样的应用场景,但 对需要操作数据库或者和其他模块阻塞交互的系统就不是很合适
。
Netty的可扩展性非常好
,而像ChannelHandler线程池化的需要!!!
,可以通过在 ChannelPipeline中添加Netty内置的ChannelHandler实现类–ExecutionHandler实现,对使用者来说只是 添加一行代码而已。对于ExecutionHandler需要的线程池模型
,Netty提供了两种可 选:
- MemoryAwareThreadPoolExecutor 可控制Executor中待处理任务的上限(超过上限时,后续进来的任务将被
阻塞
),并可控制单个Channel待处理任务的上限; - OrderedMemoryAwareThreadPoolExecutor 是 MemoryAwareThreadPoolExecutor 的子类,它还可以保证
同一Channel
中处理的事件流的顺序性,这主要是控制事件在异步处理模式下可能出现的错误的事件顺
序,但它并不保证同一Channel中的事件都在一个线程中执行
(通常也没必要)。一般来 说,OrderedMemoryAwareThreadPoolExecutor 是个很不错的选择,当然,如果有需要,也可以DIY一个。
四 demo
需求:编写Netty 服务器。 监听6668 端口,客户端能发送消息给服务器 “hello, 服务器”,服务器可以回复消息给客户端 “hello, 客户端”
4.1 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.20.Final</version>
<scope>compile</scope>
</dependency>
4.2 服务器-启动类
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建BossGroup 和 WorkerGroup
//说明
//1. 创建两个线程组 bossGroup 和 workerGroup
//2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
//3. 两个都是无限循环
//4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
// 默认实际 cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
}); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
System.out.println(".....服务器 is ready...");
//绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
说明
- SO_BACKLOG:参考:Java网络编程之Netty服务端ChannelOption.SO_BACKLOG配置
childHandler(...)
对应的是accept处理之后的SocketChannel 的初始化,每次由连接进来都会执行initChannel(...)
方法- 请求都有对应的SocketChannel ,初始化(
initChannel(...)方法里面
)时关联到一个pipeline。pipeline里面由多个handler,依次处理读写事件
4.3 服务器-自定义Handler
package com.atguigu.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
/*
说明
1. 我们自定义一个Handler 需要继续netty 规定好的某个HandlerAdapter(规范)
2. 这时我们自定义一个Handler , 才能称为一个handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
System.out.println("server ctx =" + ctx);
System.out.println("看看channel 和 pipeline的关系");
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站
//将 msg 转成一个 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + channel.remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//将数据写入到缓存,并刷新
//一般讲,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
}
//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
4.4 客户端-启动类
客户端的线程模型其实是单Reactor-单线程
package com.atguigu.netty.simple;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
}
});
System.out.println("客户端 ok..");
//启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
4.5 客户端-handler
package com.atguigu.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client " + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
//当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址: "+ ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
4.6 小结
NioEventLoopGroup调用无参构造方法,到底有几个线程?
debug构造方法
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
继续查看该值,发现在静态代码块种初始化
每个SocketChanle都有自己的pipeline对象和handler对象
在handler里面debug
我怀疑也和启动类有关,里面hander都是直接new的(就是如下这段代码
)
ch.pipeline().addLast(new NettyServerHandler());
netty无法关闭,调用了shutdownGracefully还是在执行
netty版本需要升级,我这里升级到了4.1.22.Final
4.7 NioEventLoop说明
- Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。
- NioEventLoop 表示一个不断循环执行处理任务的线程,
每个 NioEventLoop 都有一个 selector
,用于监听
绑定在其上的 socket 网络通道。 - NioEventLoop 内部采用
串行化设计
,从消息的读取->解码->处理->编码->发送,始终由IO 线程 NioEventLoop 负责
NioEventLoopGroup 下包含多个 NioEventLoop
- 每个 NioEventLoop 中包含有
一个
Selector,一个
taskQueue - 每个 NioEventLoop 的 Selector 上可以
注册监听多个
NioChannel - 每个 NioChannel 只会
绑定在唯一
的 NioEventLoop 上 - 每个 NioChannel 都绑定有
一个自己的
ChannelPipeline
五 netty中的异步模型
5.1 基本介绍
1). 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后
,通过状态、通知和回调来通知调用者。
2). Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个ChannelFuture
。
3. 调用者并不能立刻
获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制
获得 IO 操作结果
4. Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调
Future 说明
- 表示异步的执行结果, 可以通过它提供的方法来检测执行是否完成,比如检索计算等等.
- ChannelFuture 是一个接口 :
public interface ChannelFuture extends Future<Void>
我们可以添加监听器,当监听的事件发生时,就会通知到监听器.
5.2 Future-Listener 机制
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态
,注册监听函数
来执行完成后的操作。
常见有如下操作
- 通过 isDone 方法来判断当前操作是否完成;
- 通过 isSuccess 方法来判断已完成的当前操作是否成功;
- 通过 getCause 方法来获取已完成的当前操作失败的原因;
- 通过 isCancelled 方法来判断已完成的当前操作是否被取消;
- 通过 addListener 方法来注册监听器,当操作已完成(
isDone 方法返回完成
),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器
示例
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
测试得出:这个异步的代码,也是由nioEventLoopGroup
中的线程来执行的
参考
- 尚硅谷韩顺平Netty视频教程
- Netty中的三种Reactor(反应堆)