声明:本文许多素材取自教程课件。也加入了一些自己的理解。
事件驱动
的网络应用程序框架,用以快速开发高性能、高可靠性的网络IO程序简化和流程化了NIO
的开发过程API阻塞和非阻塞Socke
t;基于灵活且可扩展的事件模型
,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池
先抛出结论:
Netty线程模式(Netty主要基于主从Reactor多线程模型
做了一定的改进
,其中主从Reactor多线程模型有多个Reactor)
不同的线程模式,对程序的性能有很大影响,为了搞清Netty线程模式,我们来系统的讲解下各个线程模式,最后看看Netty线程模型有什么优越性.
目前存在的线程模型有:
Reactor的数量
和处理资源池线程的数量不
同,有3种典型的实现
一个线程处理一个请求。当并发数很大,就会创建大量的线程,占用很大系统资源;并且大部分时候线程都不是在工作,而是处于阻塞状态(比如等数据)
I/O 复用模型
:多个连接共用一个阻塞对象
,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理一个单独的线程中运行
,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移
到适当的联系人;处理程序执行非阻塞操作
。
所有的处理都是由一个线程
来完成,我们学习nio时最简单的demo就是这种模型
没有多线程、进程通信、竞争
的问题,全部都在一个线程中完成使用场景
客户端的数量有限
,业务处理非常快速,比如 Redis在业务处理的时间复杂度 O(1) 的情况
专门的NIO线程
–acceptor线程 用于监听服务端,接收客户端的TCP连接请求;一个NIO线程池
负责·,线程池可以采用标准的JDK线程池实现,它包含·一个任务队列和N个可用的线程·,由这些NIO线程负责消息的读取、解码、编码和发送;(本质还是只有一个Reactor
)一个NIO线程负责监听和处理所有的客户端连接
可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手信息进行安全认证,认证本身非常损耗性能。这类场景下,单独一个Acceptor线程可能会存在性能不足问题虽然读写处理得逻辑由线程池负责,但Reactor还是得负责监听读写事件!!!
不再是1个单独的NIO线程,而是一个独立的NIO线程池
。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)
的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。subReactor线程池的I/O线程上
,有I/O线程负责后续的I/O操作。第三种模型比起第二种模型,是将Reactor分成两部分
,mainReactor负责监听server socket,accept新连接,并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket
,读写网络数据,对业务处理功能,其扔给worker线程池完成
。通常,subReactor个数上可与CPU个数等同。
两组线程池
BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写类型都是 NioEventLoopGroup
事件循环是 NioEventLoop
不断循环的执行处理任务的线程
, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯含有多个NioEventLoop
其注册到某个worker NIOEventLoop
上的 selector,在对应NioScocketChannel 处理
维护了很多的处理器
小结
只关注Accecpt
封装
成 NIOScoketChannel并注册到Worker 线程(事件循环)
, 并进行维护就是去掉线程池的第三种形式
的变种,这也 是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提供了两种可 选:
阻塞
),并可控制单个Channel待处理任务的上限;同一Channel
中处理的事件流的顺序性,这主要是控制事件在异步处理模式下可能出现的错误的事件顺
序,但它并不保证同一Channel中的事件都在一个线程中执行
(通常也没必要)。一般来 说,OrderedMemoryAwareThreadPoolExecutor 是个很不错的选择,当然,如果有需要,也可以DIY一个。需求:编写Netty 服务器。 监听6668 端口,客户端能发送消息给服务器 “hello, 服务器”,服务器可以回复消息给客户端 “hello, 客户端”
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.20.Finalversion>
<scope>compilescope>
dependency>
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();
}
}
}
说明
childHandler(...)
对应的是accept处理之后的SocketChannel 的初始化,每次由连接进来都会执行initChannel(...)
方法initChannel(...)方法里面
)时关联到一个pipeline。pipeline里面由多个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();
}
}
客户端的线程模型其实是单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();
}
}
}
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();
}
}
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
每个 NioEventLoop 都有一个 selector
,用于监听
绑定在其上的 socket 网络通道。串行化设计
,从消息的读取->解码->处理->编码->发送,始终由IO 线程 NioEventLoop 负责
NioEventLoopGroup 下包含多个 NioEventLoop
一个
Selector,一个
taskQueue注册监听多个
NioChannel绑定在唯一
的 NioEventLoop 上一个自己的
ChannelPipeline1). 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后
,通过状态、通知和回调来通知调用者。
2). Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个ChannelFuture
。
3. 调用者并不能立刻
获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制
获得 IO 操作结果
4. Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调
Future 说明
public interface ChannelFuture extends Future
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态
,注册监听函数
来执行完成后的操作。
常见有如下操作
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
中的线程来执行的