java NIO概述:
1、在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 类, 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在 Java 堆和 Native 堆中来回复制数据。
2、 NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待
NIO和IO的区别:
1、 NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去读写数据,所以在效率上NIO比IO的效率高很多。
2、 NIO不是和IO一样使用inputStream/outputStream的形式进行操作数据,但是又是基于这种流形式。同时又采用了通道和缓冲区的形式处理数据
3、 还有一点。NIO是双向的,而IO是单向的
4、 NIO的缓冲区(字节数组)还可以进行分片处理。可以建立直接缓冲区、非直接缓冲区、直接缓冲区是为了提高IO的速度。而以一种特殊的方式进行分配内存缓冲区
以上几点可以直接概括为:IO是面向流的,NIO面向缓冲区的
NIO的三个核心:
1、缓冲区(buffer)
在java NIO中负责数据的存取,缓冲区(buffer)本质就是一个数组,用于存储不同的数据类型,根据类型的不同(boolean)除外,提供了相依的类型缓冲区,分别为:
* ByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FloatBuffer
* DoubleBuffer
其中(ByteBuffer)最为常用。
上述的缓冲区管理的方式几乎一致,通过allocate();
缓冲区存取数据的两个核心方法:
put():存入数据到缓冲区
get():获取缓冲区的数据
缓冲区中的四个核心属性:
capacity:容量,表示缓冲区最大的存储数量,一旦声明不能改变
limit:界限,表示缓冲区可以操作数据的大小,limit后面的数据不能操作
position:位置,表示缓冲区正在操作数据的位置。
mark:标记,用于记录当前position的位置,可以使用rest()恢复到mark的位置。
其中:capatity >= limit >= position >= mark
2、通道(channel):
概念:用于源节点与目标节点的连接。在 java NIO中负责缓存区中数据的传输,channel本身不存储数据,因此需要配合缓冲区进去传输。通道(channel)可以
简单的理解为火车的轨道,火车相当是缓冲区。
通道的主要类:
* java.nio.channels.Channel 接口:
* |--FileChannel
* |--Sockethannel
* |--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()
通道之间的数据传输
* transferFrom()
* transfetTo()
分散(Scatter)与聚集(Gather)
* 分散读取(Scattering Reads) :将通道中的数据分散到多个缓冲区
* 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
3、选择器(selector)
1、通道(channel)和缓冲区(Buffer)的机制,使得线程无阻塞的等待IO事件的就绪,但是要有人来监管它,这个时候就需要选择器(selector)完成,这就是所谓的同步。
2、选择器(selector)允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便
3、要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件。
Selector中注册的感兴趣事件有:
OP_ACCEPT
OP_CONNECT
OP_READ
OP_WRITE
优化:一种优化方式是:将Selector进一步分解为Reactor,将不同的感兴趣事件分开,每一个Reactor只负责一种感兴趣的事件。这样做的好处是:1、分离阻塞级别,减少了轮询的时间;2、线程无需遍历set以找到自己感兴趣的事件,因为得到的set中仅包含自己感兴趣的事件
NIO缓冲区使用实例:
@Test public void test() throws Exception { String str = "abcd"; //创建缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //获取缓冲区的容量 capatity System.out.println("缓冲区的容量为:"+buf.capacity()); //获取缓冲区的界限 limit System.out.println("缓冲区的界限为:"+buf.limit()); //获取正则操作数据的位置 position System.out.println("缓冲区操作数据的位置:"+buf.position()); System.out.println("----------------------------------------------"); //向缓冲区添加数据 buf.put(str.getBytes()); //添加数据后注意观察 postion 的变化 //获取缓冲区的容量 capatity System.out.println("缓冲区的容量为:"+buf.capacity()); //获取缓冲区的界限 limit System.out.println("缓冲区的界限为:"+buf.limit()); //获取正则操作数据的位置 position System.out.println("缓冲区操作数据的位置:"+buf.position()); System.out.println("-----------------------------------------------"); //切换至读取模式 如果想要获取缓冲区中的数据需要切换至读模式 buf.flip(); //注意观察切换至读模式后 其 缓冲区的操作界限 System.out.println("切换至读取模式缓冲区的界限为:"+buf.limit()); byte [] b = new byte[buf.limit()]; //使用get获取缓冲区中的数据 buf.get(b,0,2); System.out.println(new String(b,0,2)); //获取正则操作数据的位置 position System.out.println("缓冲区操作数据的位置:"+buf.position()); //使用mark标记位置 buf.mark(); buf.get(b,2,2); System.out.println(new String(b,2,2)); System.out.println("缓冲区操作数据的位置:"+buf.position()); //使用rest重置位置 buf.reset(); System.out.println("使用rest重置后缓冲区操作数据的位置:"+buf.position()); }
NIO管道(channel)实例(利用通道完成文件的复制(使用非直接缓冲区)
@Test public void test01() //1261 1244 { long start = System.currentTimeMillis(); FileInputStream ins = null; FileOutputStream out = null; //1、获取通道 FileChannel inChannel = null; FileChannel outChannel= null; try { ins = new FileInputStream("C:\temp\test\1.zip"); out = new FileOutputStream("C:\temp\test\2.zip"); inChannel = ins.getChannel(); outChannel = out.getChannel(); //2、分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //3、将通道中的数据存入缓冲区 while(inChannel.read(buf) != -1) { //切换读取数据模式 buf.flip(); //将缓冲区的数据写入通道 outChannel.write(buf); //清空缓冲区 buf.clear(); } } catch(Exception e) { e.printStackTrace(); } finally { if(outChannel != null) { try { outChannel.close(); } catch (IOException e) { // TODO Auto-generated catch block } } if(inChannel != null) { try { inChannel.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(ins != null) { try { ins.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(out != null) { try { out.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } long end = System.currentTimeMillis(); System.out.println("耗时:"+(end - start)); }
NIO管道(channel)实例(利用通道完成文件的复制(使用直接缓冲区)
@Test public void test02() throws Exception { long start = System.currentTimeMillis(); FileChannel inChannel = FileChannel.open(Paths.get("C:\temp\test\1.zip"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("C:\temp\test\2.zip"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); //内存映射文件 MappedByteBuffer inMapperBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outMapperBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size()); //直接对缓冲区进行数据操作 byte [] dst = new byte[inMapperBuf.limit()]; //获取直接缓冲区的数据 inMapperBuf.get(dst); //将获取到的数据写入内存映射文件中 outMapperBuf.put(dst); outChannel.close(); inChannel.close(); long end = System.currentTimeMillis(); System.out.println("耗时:"+(end - start)); }
总结:使用直接缓冲区完成文件复制的时间比非直接缓冲区快。
NIO通道(channel)实例(使用分散读取,聚集写入)
@Test public void test04() throws IOException { RandomAccessFile inRaf = new RandomAccessFile("C:\temp\test\1.zip", "rw"); //获取通道 51845754 155537264 FileChannel channel = inRaf.getChannel(); int l = (int) (channel.size() % 3); int len = (int) Math.floor((channel.size() * 1.0) / 3.0); ByteBuffer [] bytes = new ByteBuffer[3]; for(int i = 0; i < bytes.length; i++) { if(i == bytes.length+1) len = len + l; bytes[i] = ByteBuffer.allocate(len); } //分散读取 channel.read(bytes); //切换数据读取模式 for(ByteBuffer byteBuffer:bytes) { byteBuffer.flip(); } //聚集写入 RandomAccessFile outRaf = new RandomAccessFile("C:\temp\test\2.zip", "rw"); FileChannel outChannel = outRaf.getChannel(); outChannel.write(bytes); outRaf.close(); inRaf.close(); }
NIO通道(channel)实例(SocketChannel和ServerSocketChannel)的使用:
//客户端 @Test public void client() throws Exception { //1、获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898)); FileChannel inChannel = FileChannel.open(Paths.get("C:\temp\test\1.zip"), StandardOpenOption.READ); //2、分配指定大小的缓冲区 ByteBuffer dst = ByteBuffer.allocate(1024); //读取本地文件,并发送服务端 while(inChannel.read(dst) > -1) { dst.flip(); //切换读取模式 sChannel.write(dst); dst.clear(); } //关闭通道 inChannel.close(); sChannel.close(); } //服务端 @Test public void server() throws Exception { //1、获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); FileChannel outChannel = FileChannel.open(Paths.get("C:\temp\test\2.zip"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE); //绑定连接 ssChannel.bind(new InetSocketAddress(9898)); //获取客户端连接通道 SocketChannel sChannel = ssChannel.accept(); //分配指定的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //接收客户端的数据,并保存到本地 while(sChannel.read(buf) > -1) { buf.flip(); outChannel.write(buf); buf.clear(); } //关闭通道 sChannel.close(); ssChannel.close(); outChannel.close(); }
NIO选择器(selector)实例
//客户端 @Test public void client() throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898)); //设置非阻塞模式 sChannel.configureBlocking(false); //设置缓冲区大小 ByteBuffer buf = ByteBuffer.allocate(1024); Scanner scan = new Scanner(System.in); while(scan.hasNext()) { String str = scan.next(); //System.out.println(str); buf.put((sdf.format(new Date())+" "+str).getBytes()); //buf.put(str.getBytes()); buf.flip(); sChannel.write(buf); buf.clear(); } sChannel.close(); //scan.close(); } //服务端 @Test public void server() throws Exception { //1、获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); //2、设置非阻塞模式 ssChannel.configureBlocking(false); //3、绑定连接 ssChannel.bind(new InetSocketAddress("127.0.0.1",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 sChannel = (SocketChannel)sk.channel(); //14、读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); int len = 0; while((len = sChannel.read(buf)) > 0) { buf.flip(); System.out.println(new String(buf.array(),0,len)); buf.clear(); } } //15、取消选择键SelectorKey it.remove(); } } }
NIO实例 DatagrampChannel类的使用:
在java NIO 中DatagramChannel 是一个只能收发UDP包的通道。因为UDP是一个无连接的网络协议,所以不能像其他通道一样读取和写入它发送和接收的是数据包
代码示例:
//发送数据包 @Test public void send() throws Exception { DatagramChannel dc = DatagramChannel.open(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置无阻塞模式 dc.configureBlocking(false); ByteBuffer buf = ByteBuffer.allocate(1024); Scanner scan = new Scanner(System.in); while(scan.hasNext()) { String str = scan.next(); buf.put((sdf.format(new Date())+" "+str).getBytes()); buf.flip(); dc.send(buf, new InetSocketAddress("127.0.0.1",9898)); buf.clear(); } dc.close(); } //接收数据包 @Test public void receive() throws Exception { DatagramChannel dc = DatagramChannel.open(); //设置非阻塞模式 dc.configureBlocking(false); dc.bind(new InetSocketAddress(9898)); Selector selector = Selector.open(); dc.register(selector, SelectionKey.OP_READ); while(selector.select() > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while(it.hasNext()) { SelectionKey sk = it.next(); if(sk.isReadable()) { ByteBuffer buf = ByteBuffer.allocate(1024); //接收数据包 dc.receive(buf); //切换至读模式 buf.flip(); System.out.println(new String(buf.array(),0,buf.limit())); buf.clear(); } } } }