什么是NIO?
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
Java NIO: Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Java NIO: Non-blocking IO(非阻塞IO)
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。IO是阻塞的
Java NIO: Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。(IO没有选择器这个概念),说直白一点,NIO之所以能非阻塞就是因为选择器的作用,流程当客户端数据已经就绪后,是客户端注册通道到选择器中,然后将数据分发给服务器,这样就可以避免服务器没有收到数据等待而阻塞
IO:
NIO:
Buffer的数据存取
一个用于特定基本数据类行的容器。有java.nio包定义的,所有缓冲区都是抽象类Buffer的子类。
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的。
Buffer就像一个数组,可以保存多个相同类型的数据。根据类型不同(boolean除外),有以下Buffer常用子类:
ByteBuffer(最常用)
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer的概述
1)容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改。
2)限制(limit):第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity)。
3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit)。
4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
package com.example.demo.nio;
import java.nio.ByteBuffer;
/**
* @author:qsc
* @date: 2019/5/7
* @time: 9:55
* @Describe:
*/
public class Buffer {
/**
* buffer中4个参数
private int mark = -1;
private int position = 0; //缓冲区正在操作的位置,默认从0开始
private int limit; //界面的缓冲区可用大小
private int capacity //缓冲区最大容量,一旦声明,就不能改变,指的是总共大小,不是剩余大小
核心方法:
put(),往buffer存放数据
get(),获取数据
*/
public static void main(String[] args) {
//初始化ByteBuffer大小
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.mark());
System.out.println("往byteBuffer存放数据。。。。");
byteBuffer.put("asda".getBytes());
System.out.println("存放后"+byteBuffer.position());
System.out.println("存放后"+byteBuffer.limit());
System.out.println("存放后"+byteBuffer.capacity());
System.out.println("存放后"+byteBuffer.mark());
System.out.println("读取值");
//开启读取模式
byteBuffer.flip();
System.out.println("position:"+byteBuffer.position());
System.out.println("limit;"+byteBuffer.limit());
System.out.println("capacity:"+byteBuffer.capacity());
System.out.println("mark;"+byteBuffer.mark());
byte[] bytes=new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes,0,bytes.length));
/**
* 如果想要重复读取的话,要调用这个方法,否则,position会从上一次的结束位置开始读,不是从0重新开始
*/
byteBuffer.rewind();//重复读取
System.out.println("+----------重复读取------------");
System.out.println("position:"+byteBuffer.position());
System.out.println("limit;"+byteBuffer.limit());
System.out.println("capacity:"+byteBuffer.capacity());
System.out.println("mark;"+byteBuffer.mark());
byte[] bytes2=new byte[byteBuffer.limit()];
byteBuffer.get(bytes2);
System.out.println(new String(bytes2,0,bytes2.length));
System.out.println("*--------清空缓冲区-------------");
byteBuffer.clear();
System.out.println("position:"+byteBuffer.position());
System.out.println("limit;"+byteBuffer.limit());
System.out.println("capacity:"+byteBuffer.capacity());
System.out.println("mark;"+byteBuffer.mark());
//清空缓冲区,是把界限还原,数据下标遗忘,并不是数据真的清空
System.out.println((char)byteBuffer.get());
}
}
缓冲区分为直接缓冲区与非直接缓冲区
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
通道(Channel)的原理获取:
通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
java.nio.channels.Channel 接口:
FileChannel (本地文件)
SocketChannel (网络)
ServerSocketChannel(网络)
DatagramChannel(网络)
获取通道
1. Java 针对支持通道的类提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
2. 在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3. 在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
直接缓冲区与非直接缓冲区时间比较:
// 使用直接缓冲区完成文件的复制(内存映射文件)
static public void test2() throws IOException {
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("f://1.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("f://2.mp4"), StandardOpenOption.WRITE,
StandardOpenOption.READ, StandardOpenOption.CREATE);
// 内存映射文件
MappedByteBuffer inMappedByteBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区进行数据的读写操作
byte[] dsf = new byte[inMappedByteBuf.limit()];
inMappedByteBuf.get(dsf);
outMappedByteBuffer.put(dsf);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 1.利用通道完成文件的复制(非直接缓冲区)
static public void test1() throws IOException { // 4400
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("f://1.mp4");
FileOutputStream fos = new FileOutputStream("f://2.mp4");
// ①获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// ②分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
while (inChannel.read(buf) != -1) {
buf.flip();// 切换为读取数据
// ③将缓冲区中的数据写入通道中
outChannel.write(buf);
buf.clear();
}
outChannel.close();
inChannel.close();
fos.close();
fis.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
分散读取与聚集写入
分散读取(scattering Reads):将通道中的数据分散到多个缓冲区中
聚集写入(gathering Writes):将多个缓冲区的数据聚集到通道中
RandomAccessFile raf1 = new RandomAccessFile("test.txt", "rw");
// 1.获取通道
FileChannel channel = raf1.getChannel();
// 2.分配指定大小的指定缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
// 3.分散读取
ByteBuffer[] bufs = { buf1, buf2 };
channel.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
// 切换为读取模式
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("------------------分算读取线分割--------------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
// 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
阻塞IO和非阻塞IO:
BIO与NIO
IO(BIO)和NIO区别:其本质就是阻塞和非阻塞的区别
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。
IO为同步阻塞形式,NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞
BIO(IO.2):同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪:或者采用轮训的策略实时检查数据的就绪状态,如果就绪则获取数据.
异步时,则所有的IO读写操作交给操作系统,与我们的应用程序没有直接关系,我们程序不需要关系IO读写,当操作系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据极即可。
伪异步:
由于BIO一个客户端需要一个线程去处理,因此我们进行优化,后端使用线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大的线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
原理:
当有新的客户端接入时,将客户端的Socket封装成一个Task(该Task任务实现了java的Runnable接口)投递到后端的线程池中进行处理,由于线程池可以设置消息队列的大小以及线程池的最大值,因此,它的资源占用是可控的,无论多少个客户端的并发访问,都不会导致资源的耗尽或宕机。
//tcp服务器端...
class TcpServer {
public static void main(String[] args) throws IOException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
System.out.println("socket tcp服务器端启动....");
ServerSocket serverSocket = new ServerSocket(8080);
// 等待客户端请求
try {
while (true) {
Socket accept = serverSocket.accept();
//使用线程
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = accept.getInputStream();
// 转换成string类型
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String str = new String(buf, 0, len);
System.out.println("服务器接受客户端内容:" + str);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
serverSocket.close();
}
}
}
public class TcpClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("socket tcp 客户端启动....");
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("客户端进行传递数据".getBytes());
socket.close();
}
}
NIO:非阻塞代码:(开发中会用Netty,知道大概流程即可)
package com.example.demo.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
/**
* @author:qsc
* @date: 2019/5/8
* @time: 8:53
* @Describe:
*/
class NioClient{
public static void main(String[] args) throws IOException {
System.out.println("客户端已经被启动.......");
//1.创建socket通道
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
//2.切换为异步非阻塞
socketChannel.configureBlocking(false); //1.7以后才有
//3.指定缓冲区大小
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//buffer中存放数据
byteBuffer.put(new Date().toString().getBytes());
//4.切换到读取模式
byteBuffer.flip();
//把buffer写入通道
socketChannel.write(byteBuffer);
//清除数据
byteBuffer.clear();
//关闭通道
socketChannel.close();
}
}
//NIO服务器端
class NioServer{
public static void main(String[] args) throws IOException {
System.out.println("服务器端被启动");
//1.创建服务器通道
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//2.设置成异步读取数据
serverSocketChannel.configureBlocking(false); //1.7以后才有
//3.绑定连接
serverSocketChannel.bind(new InetSocketAddress(8080));
//4.获取选择器
Selector selector=Selector.open();
//5.将通道注册到选择器中,并且监听已经接收到的事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6.轮询换取已经准备就绪的事件
while (selector.select()>0){
//7.获取当前选择器有注册已经监听到的事件
Iterator<SelectionKey> it=selector.selectedKeys().iterator();
while (it.hasNext()){
//8.获取准备事件
SelectionKey sk=it.next();
// 9.判断事件准备就绪
if(sk.isAcceptable()){
//10.如果接受就绪,获取客户端连接
SocketChannel socketChannel=serverSocketChannel.accept();
//11.设置为阻塞模式
socketChannel.configureBlocking(false);//异步非阻塞IO
//12.将该通道注册到服务器上
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13.获取当前选择器就绪状态的通道
SocketChannel socketChannel=(SocketChannel)sk.channel();
//14.读取数据
int len=0;
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
while ((len=socketChannel.read(byteBuffer))>0){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,len));
byteBuffer.clear();
}
}
it.remove();
}
}
}
}
public class Client {
}
在浏览器输入127.0.0.1:8080时:控制台效果如下:
选择KEY
1、SelectionKey.OP_CONNECT
2、SelectionKey.OP_ACCEPT
3、SelectionKey.OP_READ
4、SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
在SelectionKey类的源码中我们可以看到如下的4中属性,四个变量用来表示四种不同类型的事件:可读、可写、可连接、可接受连接