4.JAVA NIO深入剖析
4.1 java NIO基本介绍
- Java NIO(New IO)即java non-block IO。NIO支持面向缓冲区的,基于通道的IO操作。NIO可理解为非阻塞IO,传统IO只能阻塞读写,而NIO可配置socket为非阻塞式。
- NIO类在java.nio包下,对原io包进行改写
- NIO三大组件 《Channel通道》,《Buffer缓冲区》,《Selector选择器》
- NIO非阻塞模式使一个线程从某管道发送请求或读取数据,没有数据不会获取,不会阻塞直至数据可读。该线程可做其他事,非阻塞写也是。
- NIO可一个线程处理多个操作。1K请求可分配20~80个线程处理,而不是阻塞IO分配1000。
4.2 NIO和NIO比较
- BIO以流方式处理数据,效率低。NIO以块处理数据,效率高。
- BIO阻塞,NIO非阻塞
- BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道到缓冲区,或相反,Seletor用于监听多个通道的事件(链接请求,数据就绪),因此单线程就可监听多个客户端。
NIO | BIO |
---|---|
面向缓冲区Buffer | 面向流Stream |
非阻塞Non Blocking IO | 阻塞Blocking IO |
选择器Selector |
4.3 NIO三大核心原理示意图
Buffer:一块可读写数的内存,被包装成NIO Buffer对象,并提供一组方法来访问。相对数组更易操作管理。
Channel:可读可写的管道,流(inPut,Output)是单向。通道可支持读取写入缓冲区,也支持异步读写。
Selector:可检查多个NIO管道是否就绪。这样一个单独线程可管理多个Channel。
- 每个Channel都对应一个Buffer
- 一个线程对应一个Selector,一个Selector对应多个Channel。
- 程序切换到那个channel由事件决定。selector根据不同事件在各个管道上切换
- Buffer是内存块,底层数组实现
- 数据读写由Buffer完成双向,BIO输入输出单向。
- Java NIO核心:通道表示打开到IO设备负责传输,缓冲区表示存储数据负责存取。
4.4 NIO核心1:缓冲区
缓冲区
一个用于基本数据类型的容器。nio包定义。所有缓冲区都是Buffer抽象类的子类。主要用于与NIO通道进行交互。数据都是从通道读入缓冲区,在写入管道。
Buffer类及其子类
Buffer类似数组,保存多个相同类型的数据。根据类型不同,有以下子类:Byte,Char,Short,Int,Long,Float,Double。上述Buffer采取相同的方法管理数,只是数据类型不同。
static XxxBufer allocate(int capacity) : 创建一个容量为capacity的 XxxBuffer对象
缓冲区的基本属性
- 容量capacity:内存块的固定大小,不能为负,不能修改。
- 限制limit:缓冲区可操作数据大小,不能为负,不能超容量。写入模式,限制等于Buffer容量,读取模式,等于写入数据量。
- 位置Position:下个要读取或写入的数据索引。
- 标记mark与重置reset:标记是索引,通过Buffer中的mark方法指定Buffer中特定的position,之后可调用reset方法恢复position。标记,职位,限制,容量遵循以下不变式:0<=mark<=position<=limit<=limit<=capacity
- 图示
缓冲区常见方法
Buffer clean():清空缓冲区并返回缓冲区的引用
Buffer flip():为将缓冲区的界限设置为当前位置,并将当前位置重置为0
int capacity():返回Buffer的容量
boolean hasRemaining():判断缓冲区还有元素
int limit():返回Buffer界限位置
Buffer limit():设置缓冲区界限为n,并返回一个新的limit的缓冲区对象
Buffer mark():对缓冲区设置标记
int position():返回缓冲区当前位置
Buffer position(int n):设置缓冲区当前位置为n,并返回修改后的Buffer对象
int remaining():返回position和limit之间的元素个数
Buffer reset():将位置position转到以前的设置的mark所在的位置
Buffer rewind():位置设置为0,取消设置的mark
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.position()); //0
System.out.println(buffer.limit()); //10
System.out.println(buffer.capacity()); //10
String name = "xuy";
buffer.put(name.getBytes());
System.out.println(buffer.position()); //3
System.out.println(buffer.limit()); //10
System.out.println(buffer.capacity()); //10
buffer.flip();
System.out.println(buffer.position()); //0
System.out.println(buffer.limit()); //3
System.out.println(buffer.capacity()); //10
char c = (char) buffer.get();
System.out.println(c); //x
System.out.println(buffer.position()); //1
System.out.println(buffer.limit()); //3
System.out.println(buffer.capacity()); //10
buffer.clear();
System.out.println(buffer.position()); //0
System.out.println(buffer.limit()); //10
System.out.println(buffer.capacity()); //10
System.out.println(c); //x 只有在重复值时生效
ByteBuffer buf = ByteBuffer.allocate(10);
String n = "abcdefg";
buf.put(n.getBytes());
buf.flip();
//读取数据
byte[] b = new byte[2];
buf.get(b);
String rs = new String(b);
System.out.println(rs); //ab
System.out.println(buf.position()); //2
System.out.println(buf.limit()); //7
System.out.println(buf.capacity()); //10
buf.mark(); //标记此刻位置:2
byte[] b2 = new byte[3];
buf.get(b2);
System.out.println(new String(b2)); //cde
System.out.println(buf.position()); //5
System.out.println(buf.limit()); //7
System.out.println(buf.capacity()); //10
buf.reset(); //回到标记位置
if(buf.hasRemaining()) {
System.out.println(buf.remaining()); //5
}
缓冲区数据操作
Buffer提供两种用于数据操作方法:get,put获取Buffer中数据。
get():获取单个字节
get(byte[] dst):批量读取多个字节到dst中
get(int index):读取指定索引位置的字节(不移动position)
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将src中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不移动position)
使用Buffer读写数据一般遵循以下四个步骤
- 写入数据到Buffer
- 调用flip方法,转换为读取模式
- 从Buffer中读取数据
- 调用buffer.clear()方法或buffer.compact()方法清楚缓冲区
直接与非直接缓冲区
buteBuffer分为基于直接内存(非堆内存),直接作用于本地IO操作,高效;另一种是非直接内存(堆内存),IO操作时要先从本进程内存复制到直接内存,再利用本地IO处理。
可使用isDriect()方法判断
非直接内存作用链:本地IO - 直接内存 - 非直接内存 - 直接内存 - 本地IO
直接内存调用链:本地IO - 直接内存 - 本地IO
结论:发送大量数据,生命周期长,直接内存效率高。直接使用allocateDirect创建,耗费性能。不过数据在JVM外存储不占用应用内存。
4.5 NIO核心2:通道Channel
通道概述
java.nio.channels包定义,表示IO源与目标打开的连接。Channel类似流,但不能直接访问数据,Channel只能与Buffer进行交互。
- NIO通道类似流,可异步读写数据,流只能单向。
- Channel是在NIO中的一个接口:public interface Channel extends Closeable{}
常用的Channel实现类
FIleChannel:用于读写,映射和操作文件
DatagramChannel:通过UDP读写网络中的数据
SocketChannel:通过TCP读写网络中的数据
ServerSocketChannel:可监听新进来的TCP连接,对每一个连接创建SocketChannel。
FileChannel
获取通道一种方式是对支持通道对象调用getChannel()方法,支持通道类型:FileInputStream,FileOutputStream,RendomAccessFile,DatagramSocket,Socket,ServerSocket。
获取管道的其他方式是使用File类的静态方法newByteChannel()获取字节通道。open()打开并返回指定通道。
//写测试
try {
//1.从字节输出流写目标文件
FileOutputStream fos = new FileOutputStream("data.txt");
//2.得到字节输出流对应的Channel
FileChannel channel = fos.getChannel();
//3.分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("xuyu".getBytes());
//4.缓冲区改为写模式
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("write ok!");
}catch (Exception e){
e.printStackTrace();
}
//读测试
try {
//1.定义一个文件字节输入流说源文件联通
FileInputStream is = new FileInputStream("data.txt");
//2.需要得到文件字节输入流管道
FileChannel channel = is.getChannel();
//3.定义一个缓冲区
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
//4.读取数据到缓冲区
channel.read(buffer1);
buffer1.flip();
//5.读取缓冲区中的数据并输出
String rs = new String(buffer1.array(), 0,buffer1.remaining()); //回到首位读取
System.out.println(rs);
} catch (Exception e){
e.printStackTrace();
}
//文件复制
try{
//源文件
File srcFile = new File("C:\\Desktop\\Test.jpg");
File desFile = new File("C:\\Desktop\\Test.jpg");
//得到一个字节输出流和字节输入流
FileInputStream fis = new FileInputStream(srcFile);
//得到一个字节输出流
FileOutputStream fos = new FileOutputStream(desFile);
//得到文件通道
FileChannel isChannel = fis.getChannel();
FileChannel osChannel = fos.getChannel();
//分散与聚集
try{
//1.字节输入管道
FileInputStream is = new FileInputStream("data.txt");
FileChannel isChannel = is.getChannel();
//2.字节输出流管道
FileOutputStream fos = new FileOutputStream("data2.txt");
FileChannel fosChannel = fos.getChannel();
//3.定义多个缓冲区做数据分散
ByteBuffer buffer1 = ByteBuffer.allocate(4);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {buffer1, buffer2};
//4.从通道中读取数据分散到各个缓冲区
isChannel.read(buffers);
//5.从每个缓冲区中查询是否有数据读取到了
for (ByteBuffer buffer : buffers) {
buffer.flip(); //切换到读模式
System.out.println(new String(buffer.array(), 0, buffer.remaining()));
}
//6.聚集写入
fosChannel.write(buffers);
isChannel.close();
fosChannel.close();
System.out.println("文件复制完成");
}catch (Exception e){
e.printStackTrace();
}
//从目标通道复制原通道数据
try{
//1.字节输入管道
FileInputStream fis = new FileInputStream("data.txt");
FileChannel fisChannel = fis.getChannel();
//2.字节输出流管道
FileOutputStream fos = new FileOutputStream("data1.txt");
FileChannel fosChannel = fos.getChannel();
//3.复制
fosChannel.transferFrom(fisChannel,fosChannel.position(),fisChannel.size());
fisChannel.close();
fosChannel.close();
}catch (Exception e){
e.printStackTrace();
}
//从原通道数据复制到目标通道
try{
//1.字节输入管道
FileInputStream is = new FileInputStream("data.txt");
FileChannel isChannel = is.getChannel();
//2.字节输出管道流
FileOutputStream fos = new FileOutputStream("data1.txt");
FileChannel osChannel = fos.getChannel();
//3.复制
isChannel.transferTo(isChannel.position(), isChannel.size(),osChannel);
isChannel.close();
osChannel.close();
} catch (Exception e) {
e.printStackTrace();
}
NIO核心3:选择器
Selector是SelectableChannel对象的多路复用器,Selector可同时监听多个SelectableChannel的IO状况。利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。
- JavaNIO用非阻塞IO方式。用一个线程处理多个客户端连接,就会用到Selector选择器
- Selector能检测多个注册的通道上是否有事件发生(注:多个Channel以事件的方式可注册到同一个Selector),如果有事件发生,便获取事件然后对每个事件进行相应的处理。这样就可以只用一个线程去管理多个通道。即多个连接和请求。
- 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大较少了系统开销,不必为每个连接创建线程。
- 避免多线程之间的上下文切换。
Selector选择器应用
创建Selector:调用Selector.open()
向选择器注册通道:SelectableChannel.register(Selector sel, int ops);
//获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); //切换非阻塞模式 ssChannel.configureBlocking(false); //绑定连接 ssChannel.bind(new InetSocketAddress(9898)); //获取选择器 Selector selector = Selector.open(); //将通道注册到选择器上,并且指定“监听接收事件” ssChannel.register(selector, SelectionKey.OP_ACCEPT);
当调用register()将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可监听二点事件类型:
- 读:SelectionKey.OP_READ (1)
- 写:SelectionKey.OP_WRITE (4)
- 连接:SelectionKey.OP_CONNECT (8)
- 接收:SelectionKey.OP_ACCEPT (16)
- 若注册时不止监听一个事件,则可使用”为或“操作符连接。
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
4.7 NIO非阻塞式网络通信原理分析
Selector示意图和特点说明
selector可以实现:一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升。
服务端流程
- 1.当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel获取通道
ServerSocketChannel = ssChannel = ServerSocketChannel.open();
- 2.切换非阻塞模式
ssChannel.configureBlocking(false);
- 3.绑定连接
ssChannel.bind(new InetSocketAddress(9999));
- 4.获取选择器
ssChannel.bind(new InetSocketAddress(9999));
- 5.将通道注册到选择器上,并且指定“监听接收事件”
ssChannel.register(selector, selectionKey.OP_ACCEPT);
- 6.轮询式的获取选择器上一已经“准备就绪”的事件
while(selector.select() > 0) {
sout("第一轮");
//获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()) {
//获取准备“就绪”的事件
SelectionKey sk = it.next();
//判断具体是什么事件准备就绪
if(sk.isAcceptable()) {
//若就绪,获取客户端连接
SocketChannel sChannel = ssChannel.accpet();
//切换非阻塞模式
sChannel.configureBlocking(false);
//将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
//获取当前选择器上“读就绪”状态的通道
SocketCahnnel sChannel = (SocketChannel) sk.channel();
//读取数据
ByteBuffer buf = ByteBuffer.alocat(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0) {
buf.flip();
sout(new String(buf.array(), 0 ,len));
buf.clear();
}
}
//取消选择键 SelectionKey
it.remove();
}
}
客户端流程
- 1.获取通道
SocketChannel sChannel = socketChannel.opan(new InetSocketAddress("127.0.0.1", 9999));
- 2.切换非阻塞模式
sChannel.configureBlocking(false);
- 3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
- 4.发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()) {
String str = scan.nextLine();
buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis()) + "\n" + str).getBytes());
buf.filp();
sChannel().wriet(buf);
buf.clear();
}
4.8 NIO非阻塞式网络通信案例
需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
地址:https://gitee.com/xuyu294636185/JAVA_NIO_DEMO.git
4.9 NIO网络编程-群聊
需求:NIO非阻塞网路编程实现多人群聊
- 编写一个NIO群聊,实现客户端与客户端通信(非阻塞)。
- 服务端:可检测用户上线,离线,并实现消息转发。
- 客户端:通过channel可无阻塞发送消息给所有客户端用户,同时接收其他客户端通过服务端转发来的消息。
- 代码地址:https://gitee.com/xuyu294636185/JAVA_NIO_DEMO.git
5.JAVA AIO深度剖析
5.1 AIO编程
- java AIO(NIO2):异步非阻塞,服务器实现为一个有效请求一个线程,客户端的I/O请求都是由OS先完成通知服务器应用去启动线程进行处理。
BIO | NIO | NIO |
---|---|---|
Socket | SocketChannel | AsynChronousScoketChannel |
ServerSocket | ServerSocketChannel | AsynchronousServerSocketChannel |
与NIO不同,当进行读写操作时,只须直接调用API的read或write异步方法,当有流可读时,操作系统会将可读流传入read缓冲区,当write方法传递的流写完,操作系统主动通知应用程序。 |