一、Java NIO简介
Java NIO(New IO | Non Blocking IO)是从java1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
二、Java NIO和IO的主要区别
传统的IO是面向流的,数据是放在流里面的,并且流是单向的
NIO是面向缓冲区的,通道只负责连接,可以把通道理解为铁路,缓冲区理解为火车,数据是放在缓冲区当中的,并且是双向的 .
三、通道与缓冲区
● Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer).通道表示打开到IO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
简而言之,Channel负责传输、Buffer负责存储
● 缓冲区(Buffer):一个用于特定基本数据类型的容器。由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类
● Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
初始化一个容量为10的缓冲区,初始化状态如下
向缓冲区存放五个字节的数据,状态如下
调用filp()方法 从写数据模式就变为读数据模式,相应的limit值发生变化,状态如下
下面有一段程序来理解Buffer里面的四个核心属性以及常用的方法
package com.buffer;
import java.nio.ByteBuffer;
import org.junit.Test;
/**
*
* 一、缓冲区(Buffer): 在java NIO 中负责数据的存储。 缓冲区就是数组。用于存储不同数据类型的数据
*
* 根据数据类型不同(boolean 除外),提供了相应的缓冲区:
* ByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FloatBuffer
* DoubleBuffer
*
*上述缓冲区的管理方式几乎一致,通过allocate()获取缓冲区
*
*二、缓冲区存取数据的核心方法:
* put():存入数据到缓冲区中
* get(): 获取缓冲区中的数据
*
*
*三、缓冲区Buffer的四个核心属性
* capacity :容量,表示缓冲区中最大存储数据的容量。一旦声明,不可改变
* limit : 界限,表示缓冲区中可以操作数据的大小(limit 后面的数据是不能进行读取的)
* position: 位置,表示缓冲区中正在操作数据的位置。
*
*
* mark: 标记,用户记录当前positiond的位置。可以通过reset()恢复到mark标记的位置
*
* 0<= mark <= position <= limit <=capacity
*
*/
public class TestBuffer {
@Test
public void test2()
{
String str = "abcde";
//1.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst,0,2);
System.out.println(new String(dst,0,2));
System.out.println("此时position的位置"+buf.position());
//mark()标记一下
buf.mark();
buf.get(dst,2,2);
System.out.println(new String(dst,2,2));
System.out.println("此时position的位置"+buf.position());
//reset() 恢复到mark标记的位置
buf.reset();
System.out.println("此时position的位置"+buf.position());
//判断缓冲区中是否还有剩余的数据
if(buf.hasRemaining())
{
//获取缓冲区中可以操作的s数量
System.out.println(buf.remaining());
}
}
@Test
public void test1()
{
String str = "abcde";
//1.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("---------allocate()------------");
System.out.println(buf.position()); //0
System.out.println(buf.limit()); //1024
System.out.println(buf.capacity()); //1024
//2.利用put()方法存入数据到缓冲区中
buf.put(str.getBytes());
System.out.println("---------put()------------");
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); //1024
System.out.println(buf.capacity()); //1024
//3.切换成读取数据的模式
buf.flip();
System.out.println("---------flip()------------");
System.out.println(buf.position()); // 0
System.out.println(buf.limit()); //5
System.out.println(buf.capacity()); //1024
//4.利用get()方法读取缓冲区中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("---------get()------------");
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); //5
System.out.println(buf.capacity()); //1024
//5.rewind() :可重复读数据
buf.rewind();
System.out.println("---------rewind()------------");
System.out.println(buf.position()); //0
System.out.println(buf.limit()); //5
System.out.println(buf.capacity()); //1024
//6.clear():清空缓冲区. 但是缓冲区中的数据依然存在,但是处于被遗忘状态,指针(limit position)回去到了最初状态
buf.clear();
System.out.println("---------clear()------------");
System.out.println(buf.position()); //0
System.out.println(buf.limit()); //1024
System.out.println(buf.capacity()); //1024
System.out.println((char)buf.get());
}
}
直接缓冲区和非直接缓冲区的区别
* 非直接缓冲区:通过allocate() 方法分配缓冲区,将缓冲区建立在JVM的内存中。
* 直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在操作系统的物理内存中。某种情况是可以提高效率的
当应用程序要读取数据,那么要向操作系统底层发起读取数据的操作。由于数据不能直接传输,首先读取到内核地址空间,copy到用户地址空间(jvm内存),最后读取到应用程序当中。那么那段copy操作时耗费时间和资源的。
当应用程序要写数据到磁盘的时候也是一样,先写入到用户地址空间,然后复制到内核空间,最后写入到磁盘当中
当应用程序对直接缓冲区操作的时候,面对的是操作系统的物理内存,因为直接缓冲区是分配在物理内存中的,在读写数据的时候,不需要进行copy操作。但是呢,我们将数据写入到物理内存的映射文件中呢,数据多会写入到磁盘是由os系统来控制的。
并且直接缓冲区的分配和销毁是耗资源的和时间的,销毁的时候需要由垃圾回收机制来销毁,但是又不确定垃圾回收机制多会进行。
package com.buffer;
import java.nio.ByteBuffer;
import org.junit.Test;
/*
* 一、直接缓冲区和非直接缓冲区
*
* 非直接缓冲区:通过allocate() 方法分配缓冲区,将缓冲区建立在JVM的内存中。
* 直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在操作系统的物理内存中。某种情况是可以提高效率的
*
*/
public class TestBuffer2 {
@Test
public void testBuffer()
{
//分配直接缓冲区
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
//判断是不是直接缓冲区
System.out.println(buf.isDirect());
}
}
看一下allocate()方法和allocateDirect()方法的源码。
很明显,allocate分配时分配到jvm的堆上面。
allocateDirect分配是在操作系统的物理内存上分配的。
● 通道:由java.nio.chaneels包定义的,Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
package com.buffer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import org.junit.Test;
/**
*
* 一、通道:用于源节点与目标节点的连接。在Java NIO中,负责缓冲区中数据的传输,通道本身不存储任何数据
* 因此需要配合缓冲区进行传输。
*
* 二、通道的一些主要实现类
* java.nio.chaneels.Chaneel 接口:
* |--FileChannel :用于本地
* |--SocketChannel :用于tcp
* |--ServerSocketChannel :用于tcp
* |--DatagramChannel :用于udp传输
*
* 三、获取通道
* 1、java 针对支持通道的类提供了getChannel()方法
* 本地io:
* FileInputStream/FileOutputStream
* RandomAccessFile
*
* 网络io:
* Socket
* ServerSocket
* DatagramScoket
*
* 2、在JDK1.7中的NIO.2针对各个通道提供了一个静态方法open()
* 3、在JDK1.7中的NIO.2的Files工具类的newByteChannel()
*
*四、通道之间的数据传输
*transferFrom()
*transferto()
*
*
*五、分散(Scatter)与聚集(Gather)
*
*分散读取(Scatter Reads) : 将通道中的数据分散到多个缓冲区中
*聚集写入(Gather writes) : 将多个缓冲区中的数据聚集到通道中
*
*
*六、字符集 :CharSet
*
*编码: 字符串- 》字节数组
*解码: 字节数组 -》 字符串
*/
public class TestChaneel {
//字符集
@Test
public void test6() throws CharacterCodingException
{
Charset cs1 = Charset.forName("GBK");
//获取编码器
CharsetEncoder ce = cs1.newEncoder();
//获取解码器
CharsetDecoder de = cs1.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("威威");
charBuffer.flip();
//编码
ByteBuffer bbuf = ce.encode(charBuffer);
for(int i = 0;i<4;i++)
{
System.out.println(bbuf.get());
}
//解码
bbuf.flip();
CharBuffer dbuf = de.decode(bbuf);
System.out.println(dbuf.toString());
}
@Test
public void test5()
{
SortedMap<String,Charset> availableCharsets = Charset.availableCharsets();
Set<Entry<String,Charset>> entrySet = availableCharsets.entrySet();
for(Entry<String,Charset> entry : entrySet)
{
System.out.println(entry.getKey()+"="+entry.getValue());
}
}
@Test
public void test4() throws IOException
{
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
//1.获取通道
FileChannel channel1 = raf1.getChannel();
//2.分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3.分散读取
ByteBuffer[] bufs = {buf1,buf2};
channel1.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);
}
//通道之间的数据传输(直接缓冲区的方式)
@Test
public void test3() throws IOException
{
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.avi"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.avi"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
//2.使用直接缓冲区完成文件的复制(内存映射文件)
@Test
public void test2() throws IOException //耗费的时间125
{
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.avi"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.avi"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMappedBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuffer.limit()];
inMappedBuffer.get(dst);
outMappedBuffer.put(dst);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println("耗费的时间"+(end-start));
}
//1.利用通道完成文件的复制(非直接缓冲区)
@Test
public void test1() //耗费的时间1396
{
long start = System.currentTimeMillis();
FileInputStream fis=null;
FileOutputStream fos=null;
//1.获取通道
FileChannel inChannel =null;
FileChannel outChannel =null;
try {
fis = new FileInputStream("d:/1.avi");
fos = new FileOutputStream("d:/2.avi");
inChannel = fis.getChannel();
outChannel = fos.getChannel();
//2.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3.将通道中的数据存入到缓冲区中
while(inChannel.read(buf) != -1)
{
buf.flip(); //切换成读取数据的模式
//4.将缓冲区中的数据写入到通道
outChannel.write(buf);
buf.clear(); //清空缓冲区
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(outChannel!=null)
{
try {
outChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(inChannel !=null)
{
try {
inChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fos!=null)
{
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fis !=null)
{
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
long end = System.currentTimeMillis();
System.out.println("耗费的时间"+(end-start));
}
}
四、NIO的非阻塞式网络通信
● 传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或write()时,该线程阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
● Java NIO时非阻塞式的,当线程从某通道进行读写数据时,若没有数据可用时,该线程进行其他的任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO可以让服务区端使用一个或有限个线程来同时处理连接到服务器端的所有客户端。
每一个通道都会注册到选择器当中,选择器会监控每一个通道,让发现通道的数据准备好的时候,才会让服务器端分配给一个或者多个线程去执行这个通道的任务。当这个通道数据没有准备好的时候,其他线程可以干其他的事情。这样就大大的提高了CPU的利用率
接下来先来一个阻塞的NIO程序
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.junit.Test;
public class TestBlockingNIO2 {
//客户端
@Test
public void Client() throws IOException{
//1.获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
FileChannel inChanner = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
//2.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3.读取本地文件并发送到服务端去
while(inChanner.read(buf)!=-1)
{
buf.flip();
sChannel.write(buf);
buf.clear();
}
sChannel.shutdownOutput();
//4.接收服务端的反馈
int len = 0;
while((len = sChannel.read(buf))!=-1)
{
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
//关闭通道
inChanner.close();
sChannel.close();
}
//服务区端
@Test
public void Server() throws IOException{
//获取通道
ServerSocketChannel ss = ServerSocketChannel.open();
FileChannel outChanner = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//绑定连接
ss.bind(new InetSocketAddress(9898));
//获取客户端连接的通道
SocketChannel socketChannel = ss.accept();
//分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//读取客户端传过来的数据 并保存到本地
while(socketChannel.read(buf)!=-1)
{
buf.flip();
outChanner.write(buf);
buf.clear();
}
//发送反馈给客户端
buf.put("服务端接收数据成功".getBytes());
buf.flip();
socketChannel.write(buf);
//关闭通道
socketChannel.close();
outChanner.close();
ss.close();
}
}
非阻塞式的NIO
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import org.junit.Test;
/**
*
* 一、使用NIO完成网络通信的三个核心
*
* 1.通道:负责连接
*
* java.nio.channels.Channel 接口:
* |--SelectableChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
*
* |--pipe.SinkChannel
* |--pipe.SourceChannel
*
*
*
*
* 2.缓冲区:负责存储数据
*
* 3.选择器:是SelectableChannel的多路复用器,用于监控SelectableChannel的IO状况
*
*/
public class TestNonBlockingNIO {
//客户端
@Test
public void Client() throws IOException{
//1.获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2.切换成非阻塞模式
sChannel.configureBlocking(false);
//3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.发送数据到服务器端
buf.put(new Date().toString().getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
//关闭通道
sChannel.close();
}
//服务区端
@Test
public void Server() throws IOException{
//1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2.切换成非阻塞模式
ssChannel.configureBlocking(false);
//3.绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4.获取选择器
Selector selector = Selector.open();
//5.将通道注册到选择器上 ,并且指定监听接收事件, 第二个参数 是监测状态 有四种常量
ssChannel.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 sChannel = ssChannel.accept();
//11. 切换成非阻塞模式
sChannel.configureBlocking(false);
//12.将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable())
{
//13.获取当前选择器上 “读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel)sk.channel();
//14.读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = socketChannel.read(buf))>0)
{
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
}
//取消选择键 SelectionKey
it.remove();
}
}
}
}