JAVA NIO基础
原创者:文思
一、NIO介绍
来源于1.4,改进于1.7
NIO:New IO。互联网环境下可以理解成Non Bolcking IO,非阻塞IO
传统IO:面向流(单向的)、阻塞
NIO:面向缓冲(双向)、非阻塞(针对网络通信)、选择器(针对网络通信)
NIO的核心:通道+缓冲区,通道表示打开倒IO设备的连接,若需要使用NIO系统需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。
与传统IO区别:
NIO和IO最大的区别是数据打包和传输方式。IO是以流的方式处理数据,而NIO是以块的方式处理数据。
面向流的IO一次一个字节的处理数据,一个输入流产生一个字节,一个输出流就消费一个字节。为流式数据创建过滤器就变得非常容易,链接几个过滤器,以便对数据进行处理非常方便而简单,但是面向流的IO通常处理的很慢。
面向块的IO系统以块的形式处理数据。每一个操作都在一步中产生或消费一个数据块。按块要比按流快的多,但面向块的IO缺少了面向流IO所具有的有雅兴和简单性。
以前的IO可以理解成接一个水管道,然后送水,送水结束后才断开。现在的NIO可以理解成火车车厢(缓冲区)在铁道上运行传输(管道)。
在Java
API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO
Buffer(缓冲区)和Channel(通道)是标准NIO中的核心对象(网络NIO中还有个Selector选择器核心对象)
Channel是对原IO中流的模拟,任何来源和目的数据都必须通过一个Channel对象。
Buffer实质上是一个容器对象,发给Channel的所有对象都必须先放到Buffer中,同样的,从Channel中读取的任何数据都要读到Buffer中。
在NIO中,所有数据都是Buffer处理的,Buffer实质上是一个数组,通常是一个字节数据,也可是其它类型,。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,可跟踪读写进程。
查看Buffer类:
public abstractclassBuffer {
SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit<= capacity
privateintmark= -1;
privateintposition= 0;
privateintlimit;
private int capacity;
可以发现缓冲区的四个核心属性:
capactiy:缓冲区最大存储数据的容量,一旦声明无法改变
limit:界线,缓冲区中可以操作的数据的大小,limit后面的数据不能读写
position:当前数据位置。Position<=limit<=capactiy
mark:标记
新建NIO工程测试Buffer:
/**
*一、缓冲区:在java NIO中负责数据的存取,可以看成数组,用于存储不同类型的数据
*根据数据类型不同,提供了相应类型的缓冲区(boolean类型除外):
* ByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FlatBuffer
* DoubleBuffer
*上述缓冲区都是通过allocate()获取缓冲区
*二、缓冲区存取数据的两个方法:put()、get()
*三、flip()从写数据切换成读数据模式
*四、rewind()可重复读
*五、clear()清空缓冲区
*/
public classTestBuffer {
public void testBuffer1(){
Strings = "abcde";
//第1步:分配指定大小的缓冲区
ByteBufferbuf= ByteBuffer.allocate(1024);
System.out.println("----------第1步allocate()--------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//第2步:利用put()方法存入数据到缓冲区
buf.put(s.getBytes());
System.out.println("----------第2步put()--------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//第3步:切换成读取数据的模式
buf.flip();
System.out.println("----------第3步flip()--------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//第4步:利用get()读取缓冲区数据
byte[] b = new byte[buf.limit()];
buf.get(b);
System.out.println("----------第4步get()--------");
System.out.println(new String(b,0,b.length));
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//第5步:rewind():可重复度
buf.rewind();
System.out.println("----------第5步rewind()--------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//第6步:clear():清空缓冲区
System.out.println("----------第6步clear()--------");
buf.clear();
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//第7步:测试clear()后的清空结果
System.out.println("-------第7步:测试clear()后的清空结-----");
System.out.println((char)buf.get());;
}
public static void main(String[] args){
TestBuffert = new TestBuffer();
t.testBuffer1();
}
运行结果:
----------第1步allocate()--------
0
1024
1024
----------第2步put()--------
5
1024
1024
----------第3步flip()--------
0
5
1024
----------第4步get()--------
abcde
5
5
1024
----------第5步rewind()--------
0
5
1024
----------第6步clear()--------
0
1024
1024
----------第7步:测试clear()后的清空结果--------
a
1
1024
1024
由此可见clear()并不是删除数据,是清空缓冲区操作,但缓冲区的数据依然存在,但是处于“被遗忘状态”。被遗忘数据是否有潜在影响待进一步分析。
测试mark():
/**
*测试mark()
*/
public void testMark(){
String s = "abcde";
ByteBuffer buf= ByteBuffer.allocate(1024);
buf.put(s.getBytes());
buf.flip();
byte[] b = new byte[buf.limit()];
buf.get(b,0,2);//缓冲区读取两位长度内容
System.out.println("读取的两位长度内容的显示:"+new String(b,0,2));//读取的两位长度内容的显示
System.out.println("读取数据后的当前位置:"+buf.position());//读取数据后的当前位置
buf.mark();
buf.get(b,2,2);//缓冲区从位置2处再读取两位长度内容
System.out.println("从位置2处再读取两位长度内容的显示:"+new String(b,2,2));//从位置2处再读取两位长度内容的显示
System.out.println("mark的当前位置:"+buf.position());//mark的当前位置
buf.reset();
System.out.println("重置恢复到mark的当前位置:"+buf.position());//重置恢复到mark的当
}
运行结果:
读取的两位长度内容的显示:ab
读取数据后的当前位置:2
从位置2处再读取两位长度内容的显示:cd
mark的当前位置:4
重置恢复到mark的当前位置:2
其它API:hasRemaining()缓冲区是否还有剩余数据,remaining()获取缓冲区中剩余数据的数量。
直接缓冲区与非直接缓冲区
非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中
应用程序发起读请求,物理磁盘将内容读到内核地址空间,copy内核地址内容到用户地址,程序读取。前面的例子都是适用的非直接缓冲区。如上图看到一些copy操作,和下图比较在性能上应该处于劣势。
直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。
直接缓冲区通过调用此类的allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显,但任何事情都是矛盾的,直接操作物理内存,则对内存消耗比较大,而且写到物理内存后物理内存中的数据不归程序管理了,何时写到磁盘则由操作系统控制了,应用程序对内存的引用的回收依靠垃圾回收机制来控制,这样就存在风险。
从两图可以看出,直接缓冲区比非直接缓冲区效率要高。
使用选择:建议将直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。直接操作物理内存的事一般情况下敢玩吗?所以常用的还是非直接缓冲区方式.
直接缓冲区示例:
/**
*测试直接缓冲区
*/
public void testDirect(){
ByteBuffer buf= ByteBuffer.allocateDirect(1024);
System.out.println(buf.isDirect());
}
运行结果:true
通道Channel是一个对象,可以通过它读取和写入数据。可以把它看做IO中的流。但是它和流相比还有一些不同:
1. Channel是双向的,既可以读又可以写,而流是单向的
2. Channel可以进行异步的读写
3. Channel 本身不能直接访问数据,对Channel的读写必须通过buffer对象
最开始的操作系统都是cpu直接负责IO接口,这样当有大量的IO请求时cpu的负荷压力变大,影响整个操作系统。后来演变成内存+DMA来代理控制IO请求,DMA向cpu申请权限,然后IO操作全权由DMA总线来负责,这也是传统的IO流的方式。但是当有大量的流请求时,DMA总线会过多,易造成总线冲突,也会影响性能。最后就演变成上图的模式,通道的方式,通道是完全独立的处理器(虽然仍附属于cpu)可独立负责IO操作。
利用通道完成文件的复制:
/**
*通道:用于源节点与目标节点的连接。在java NIO中负责缓冲区中数据的传输。
*主要实现类java.nio.channels.Channel接口
* |FileChannel本地文件通道,用于读取、写入、映射和操作文件的通道
* |SocketChannel网络相关,通过TCP 读写网络中的数据
* |ServerScocketChannel可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个SocketChannel
* |DatagramChannel通过UDP读写网络中的数据通道
*获取通道:
*1、java针对支持通道的类提供了getChannel()方法
*FileInputStream/FileOutputStrem,RandonAccdessFile
*网络IO:Socket,ServderSocket,DatagramSocket
*2、jdk1.7中的NIO2针对各通道提供了静态方法open()
*3、jdk1.7中的NIO2的Files工具类的newByteChannel()
*/
public classTestChannel {
//1利用通道完成文件的赋值
public void test1() throws IOException{
FileInputStream fis = new FileInputStream("1.jpg");
FileOutputStream fos = new FileOutputStream("2.jpg");
//1得到通道
FileChannel c1 = fis.getChannel();
FileChannel c2 = fos.getChannel();
//2分配缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3将数据写入缓冲区
while(c1.read(buf) != -1){//数据写入缓冲区
//4将缓冲区写入通道
buf.flip();
c2.write(buf);//将缓冲区写入通道
buf.clear();
}
c2.close();c1.close();fos.close();fis.close();
}
//使用直接缓冲区完成文件复制(内存直接映射文件)
public void test2() throws IOException{
FileChannel c1 = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
FileChannel c2 = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);//因为写文件时也需要先读,所以这里也要配置StandardOpenOption.READ
//内存映射文件
MappedByteBuffer inMbuf = c1.map(MapMode.READ_ONLY,0,c1.size());
MappedByteBuffer outMbuf = c2.map(MapMode.READ_WRITE,0,c1.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMbuf.limit()];
inMbuf.get(dst);
outMbuf.put(dst);
c1.close();c2.close();
}
//通道之间的数据传输(直接缓冲区)
public void test3() throws IOException{
FileChannel c1 = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
FileChannel c2 = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
c1.transferTo(0, c1.size(), c2);//从c1通道将数据传至c2
// c2.transferFrom(c1,0,c1.size());//从方法的命名可推出transferFrom的用法。
c1.close();c2.close();
}
分散与聚集:
分散读取:从channel中读取的数据分散到多个buffer中
聚集写入:将多个buffer中的数据聚集到channel
//分散与聚集
public void test4() throws IOException{
//分散读取
RandomAccessFile raf = new RandomAccessFile("1.txt","rw");
FileChannel channel = raf.getChannel();
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1600);
ByteBuffer[] bufs = {buf1,buf2};
channel.read(bufs);
//聚集写入
for(ByteBuffer buf : bufs){
buf.flip();
}
RandomAccessFile raf2 = new RandomAccessFile("1.txt","rw");
FileChannel channel2 = raf2.getChannel();
ByteBuffer bufWrite = ByteBuffer.allocate(1800);
channel2.write(bufs);
}
二、阻塞与非阻塞式网络通信(重点)
传统的IO 流都是阻塞式的。也就是说,当一个线程调用read() 或write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
阻塞式网络通信示例:
/**
*使用NIO完成网络通信的三个核心:
* 1、通道(Channel):负责连接
* java.nio.channels.Channel接口:
* |--SelecttableChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
* |--Pipe.SinkChannel
* |--Pipe.SourceChannel
* 2、缓冲区(Buffer):负责数据的存取
* 3、选择器(Selector):是SelectableChannel的多路复用器,用于监控SelectableChannel的IO状况和事件
*/
public class TestBlockingNIO {
/**客户端*/
public void client() throws IOException{
//1获取网络通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2创建获取本地通道用于读取要发送的文件
FileChannel inLocalChannel = FileChannel.open(Paths.get("test.jpg"),StandardOpenOption.READ);
//3分配制定缓冲
ByteBuffer buf = ByteBuffer.allocate(1024);
//4读取本地文件,并发送到服务器
while(inLocalChannel.read(buf)!=-1){
buf.flip();
socketChannel.write(buf);
buf.clear();
}
//5接收服务端的反馈
int len = 0;
while((len = socketChannel.read(buf))!=-1){
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
inLocalChannel.close();
socketChannel.close();
}
/**服务端
*@throws IOException
*/
public void server() throws IOException{
//1建立获取网络通道并绑定监听端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9898));
//2激活添加对客户端通道的监听
SocketChannel socketChannel = serverSocketChannel.accept();
//3建立本地通道用于写文件
FileChannel outLocalChannel = FileChannel.open(Paths.get("des.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE_NEW);
//4建立缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//5接收读取网络文件
while(socketChannel.read(buf)!=-1){
buf.flip();
outLocalChannel.write(buf);
buf.clear();
}
//6发送反馈给客户端
buf.put("服务端接收数据成功".getBytes());
buf.flip();
socketChannel.write(buf);
}
}
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞IO 的空闲时间用于在其他通道上执行IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
java NIO的服务端只需启动一个专门的线程来处理所有的 IO 事件。java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:
服务端接收客户端连接事件SelectionKey.OP_ACCEPT(16)
客户端连接服务端事件SelectionKey.OP_CONNECT(8)
读事件SelectionKey.OP_READ(1)
写事件SelectionKey.OP_WRITE(4)
服务端和客户端各自维护一个管理通道的对象,我们称之为selector(选择器),该对象能检测一个或多个通道(channel) 上的事件。Selector可以同时监控多个SelectableChannel的IO 状况,也就是说利用Selector可使一个单独的线程管理多个Channel。Selector 是非阻塞IO 的核心。若注册时不止监听一个事件,则可以使用“位或”操作符连接,例如: int interestSet = SelectionKey.OP_READ| SelectionKey.OP_WRITE
非阻塞网络通信示例(重点):
public classTestNoBlockingNIO{
//客户端
public void client() throws IOException{
//1获取客户端通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2切换成非阻塞模式
socketChannel.configureBlocking(false);
//3分配缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4发送数据给服务端
buf.put(new Date().toString().getBytes());
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put(("\n"+str).getBytes());
buf.flip();
socketChannel.write(buf);
buf.clear();
}
socketChannel.close();
}
//服务端
public void server() throws IOException{
//1获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2切换成非阻塞模式
serverSocketChannel.configureBlocking(false);
//3绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//4获取选择器
Selector selector = Selector.open();
//5将通道注册到选择器上,并且指定"监听接收事件"
//这里的ACCEPT是靠注册进来的监听,所以可以推论出此方式为非阻塞
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);//SelectionKey选择键
//6轮询式的获取选择器上已经"准备就绪"的事件
while(selector.select()>0){
//获取当前选择器中所有注册的"选择键(已就绪的监听事件)"
Iterator it = selector.selectedKeys().iterator();
//迭代获取选择器并根据不同的选择键做不同的操作
while(it.hasNext()){
SelectionKey sk = it.next();
if(sk.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(sk.isReadable()){
SocketChannel socketChannel = (SocketChannel) sk.channel();
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();
// socketChannel.close();
}
}
//取消选择键
it.remove();
}
}
}
测试:
首先运行服务端:
TestNoBlockingNIO t = newTestNoBlockingNIO();
t.server();
再次运行客户端
TestNoBlockingNIO t = newTestNoBlockingNIO();
t.client();
客户端输入hello,服务端显示hello:
三、NIO.2–Path、Paths、Files
随着JDK7 的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为NIO.2
Path 与Paths
java.nio.file.Path 接口代表一个平台无关的平台路径,描述了目录结构中文件的位置。
Paths 提供的get() 方法用来获取Path 对象:
Path get(String first, String …more) : 用于将多个字符串串连成路径。
Path常用方法:
boolean endsWith(String path) : 判断是否以path 路径结束
boolean startsWith(String path) : 判断是否以path 路径开始
boolean isAbsolute() : 判断是否是绝对路径
Path getFileName() : 返回与调用Path 对象关联的文件名
Path getName(int idx) : 返回的指定索引位置idx 的路径名称
int getNameCount() : 返回Path 根目录后面元素的数量
Path getParent() :返回Path对象包含整个路径,不包含Path 对象指定的文件路径
Path getRoot() :返回调用Path 对象的根路径
Path resolve(Path p) :将相对路径解析为绝对路径
Path toAbsolutePath() : 作为绝对路径返回调用Path 对象
String toString() :返回调用Path 对象的字符串表示形式
java.nio.file.Files 用于操作文件或目录的工具类。
Files常用方法:
Path copy(Path src, Path dest,CopyOption … how) : 文件的复制
PathcreateDirectory(Path path,FileAttribute … attr) : 创建一个目录
Path createFile(Path path,FileAttribute … arr) : 创建一个文件
void delete(Path path) : 删除一个文件
Path move(Path src, Path dest,CopyOption…how) : 将src 移动到dest位置
long size(Path path) : 返回path 指定文件的大小
Files常用方法:用于判断
boolean exists(Path path,
LinkOption … opts) : 判断文件是否存在
boolean isDirectory(Path path,LinkOption … opts) : 判断是否是目录
boolean isExecutable(Path path) : 判断是否是可执行文件
boolean isHidden(Path path) : 判断是否是隐藏文件
boolean isReadable(Path path) : 判断文件是否可读
boolean isWritable(Path path) : 判断文件是否可写
boolean notExists(Path path,LinkOption … opts) : 判断文件是否不存在
public static BasicFileAttributes> A readAttributes(Path path,Class type,LinkOption... options) : 获取与path 指定的文件相关联的属性。
Files常用方法:用于操作内容
SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
DirectoryStream newDirectoryStream(Path path) : 打开path 指定的目录
InputStream newInputStream(Path path, OpenOption…how):获取InputStream 对象
OutputStream newOutputStream(Path path, OpenOption…how) : 获取OutputStream 对象