文章目录
- 一、IO模型
- 1、BIO(同步阻塞)
- 2、NIO(同步非阻塞)
- 3、AIO(异步非阻塞)
- 二、Reactor模型
- 1、单Reactor单线程
- 2、单Reactor多线程
- 3、主从Reactor多线程
- 三、Netty工作模型
- 1、Netty工作模型
- 2、Netty入门案例
- (1)server
- (2)client
- 3、Netty任务队列
- 四、异步模型
- 1、Future-Listener机制
- 参考资料
一、IO模型
I/O 模型简单的理解:就是 用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO。
1、BIO(同步阻塞)
服务器实现模式为 一个连接对应一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
在JDK1.4之前,BIO是我们实现网络编程的唯一选择,虽然服务端对并发量支撑较差,但好在程序简单易理解。
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws Exception {
//线程池机制
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { //我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket 获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if(read != -1) {
System.out.println(new String(bytes, 0, read
)); //输出客户端发送的数据
} else {
break;
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("关闭和client的连接");
try {
socket.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
BIO的问题也很明显:
1.每个请求都需要创建独立的线程,与对应的客户端进行数据Read、业务处理、数据Write。
2.当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3.连接建立后,如果当前线程暂时没有数据可读,则线程会阻塞在Read操作上,造成线程资源浪费。
2、NIO(同步非阻塞)
Java NIO全称java non-blocking IO,是指JDK从1.4开始提供新的API,是同步非阻塞的,NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
由于NIO的编程复杂、API复杂等等原因,Netty应运而生。
3、AIO(异步非阻塞)
JDK7 引入了 Asynchronous I/0,即 AIO。在进行IO编程中,常用到两种模式:Reactor 和 Proactor。Java 的NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
二、Reactor模型
AIO目前并不是主流的IO模型,linux目前并不支持AIO的异步非阻塞模式,即使是epoll也是在NIO的基础上进行的优化,所以当前主流的IO模型还是NIO模型。
1、单Reactor单线程
- Select 是Java NIO多路复用模型的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求。
- Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发。
- 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接3完成后的后续业务处理。
- 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应。
- Handler 会完成 Read一业务处理一Send 的完整业务流程。
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
这种模型比较简单,没有多线程问题,所有的连接、业务处理都在一个线程中完成。
这种模型显而易见有个致命缺点,就是当业务处理比较耗时时,会造成消息积压。
2、单Reactor多线程
Reactor 对象通过 select 监控客户端请求事件,收到事件后,通过 dispatch 进行分发。
- 如果建立连接请求,则右 Acceptor 通过accept 处理连接请求,然后创建一个 Handler 对象处理完成连接后的各种事件
- 如果不是连接请求,则由 reactor 分发调用连接对应的 handler 来处理。
- handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的 worker 线程池的某个线程处理业务。
- worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler。
- handler 收到响应后,通过 send 将结果返回给 client。
这种模式充分利用了多核CPU的处理能力,使用多线程进行业务处理。但是单reactor处理所有的事件的监听和响应,在单reactor下,高并发场景很容易出现性能瓶颈。
3、主从Reactor多线程
在这种模式下,对Reactor进一步拆分。
- Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过Acceptor 处理连接事件。
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor。
- subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理。
- 当有新事件发生时,subreactor 就会调用对应的 handler 处理。
- handler 通过 read 读取数据,分发给后面的 worker 线程处理。
- worker 线程池分配独立的 worker 线程进行业务处理,并返回结果。
- handler 收到响应的结果后,再通过 send 将结果返回给 client。
- Reactor 主线程可以对应多个 Reactor 子线程、即 MainRecator 可以关联多个 SubReactor。
三、Netty工作模型
1、Netty工作模型
Netty就是在主从Reactor多线程的模型下,进行了优化产生的。
- Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写。
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup(在Epoll支持下也可以是EpollEventLoopGroup)
- NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop。
- NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 的网络通讯。
- NioEventLoopGroup 可以有多个线程、即可以含有多个 NioEventLoop。
- 每个 Boss NioEventLoop 循环执行的步骤有 3 步:
(1)轮询 accept 事件(2)处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 worker NIOEventLoop 上的 selector(3)处理任务队列的任务 ,即 runAllTasks - 每个 Worker NIOEventLoop 循环执行的步骤:
(1)轮询 read,write 事件(2)处理 i/o 事件,即 read ,write 事件,在对应 NioScocketChannel 处理(3)处理任务队列的任务,即 runAlITasks - 每个 WorkerNIOEventLoop 处理业务时,会使用pipeline(管道),pipeline 中包含了 channel,即通过pipeline可以获取到对应通道。管道中维护了很多的 处理器
2、Netty入门案例
(1)server
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 (NettyRuntime.availableProcessors() * 2)
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
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();
}
}
}
/*
说明
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(); // channel和pipeline是互相包含的关系
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();
}
}
(2)client
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();
}
}
}
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();
}
}
3、Netty任务队列
(1)用户程序自定义的普通任务
将任务推送至普通任务队列异步执行,不阻塞主线程。
(2)定时任务
将任务推送至定时任务队列中异步执行,不阻塞主线程。
//读取数据事件(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交在该channel 对应的NIOEventLoop 的 taskQueue中
//解决方案1 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
// 上面的任务执行完毕之后,才会执行该任务!taskQueue中的任务是同一个线程
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵3", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
//解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
System.out.println("go on ...");
System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
System.out.println("server ctx =" + ctx);
System.out.println("看看channel 和 pipeline的关系");
Channel channel = ctx.channel(); // channel和pipeline是互相包含的关系
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());
}
(3)非当前Reactor线程调用Channel的各种方法
将channel列表维护为一个List,可以取得该List中的数据在非当前用户下推送给其他用户。
//使用链式编程来进行设置
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 对应的管道设置处理器
Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道。NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop负责。
NioEventLoopGroup 下包含多个 NioEventLoop
- 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
- 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
- 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
- 每个 NioChannel 都绑定有一个自己的 ChannelPipeline
四、异步模型
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得IO 操作结果。
Netty 的异步模型是建立在 fuure 和 callback 的之上的。callback 就是回调。 Future是重点,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun 返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future 去监控方法 fun 的处理过程(即 : Future-Listener 机制)
1、Future-Listener机制
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态注册监听函数来执行完成后的操作。
常见有如下操作:
- 通过 isDone 方法来判断当前操作是否完成;
- 通过 isSuccess 方法来判断已完成的当前操作是否成功;
- 通过 getCause 方法来获取已完成的当前操作失败的原因;
- 通过 isCancelled 方法来判断已完成的当前操作是否被取消;通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果Future 对象已完成,则通知指定的监听器
//绑定一个端口并且同步, 生成了一个 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 失败");
}
}
});
参考资料
《Scalable IO in Java》
https://blog.csdn.net/qq_36389060/article/details/124232377
https://www.bilibili.com/video/BV1DJ411m7NR