概述
一、NIO简介
- NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- NIO基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
- Selector用于监听多个通道的事件(比如:连接打开,数据到达),单个线程可以监听多个数据通道
二、NIO VS 传统IO
- IO是面向流的,NIO是面向缓冲区的:NIO中的缓冲区的存在使我们可以在其中对数据进行操作,增加了灵活性
- IO的各种流是阻塞的,NIO是非阻塞模式:Selector用于监听多个通道的事件,从而实现一个线程管理多个输入和输出通道
三、Channel & Buffer & Selector
1、channel
- 传统IO中的stream是单向的,而 NIO 中的channel是双向的,既可读又可写;
- NIO中的Channel的主要实现有:这几个实现分别对应 IO、UDP、TCP
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
2、buffer
- NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应几种基本的数据类型
- NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等
3、selector
- 要使用Selector, 得向Selector注册Channel,然后调用select()方法,这个方法会一直阻塞到某个注册的通道有事件就绪
- 事件就绪后这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等
FileChannel
一、NIO的实例
public static void method1(){ RandomAccessFile aFile = null; try{ aFile = new RandomAccessFile("d:\\123.txt","rw"); FileChannel fileChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(1024); int bytesRead = fileChannel.read(buf); System.out.println(bytesRead); while(bytesRead != -1) { buf.flip(); while(buf.hasRemaining()) { System.out.print((char)buf.get()); } buf.compact(); bytesRead = fileChannel.read(buf); } }catch (IOException e){ e.printStackTrace(); }finally{ try{ if(aFile != null){ aFile.close(); } }catch (IOException e){ e.printStackTrace(); } } }
二、buffer 使用
1、基本原理
- buffer 的本质是一个容器,是一个连续的数组;
- NIO 数据传递的基本过程如图:
- 向Buffer中写数据:
- 从Channel写到Buffer (fileChannel.read(buf))
- 通过Buffer的put()方法 (buf.put(…))
- 从Buffer中读数据:
- 从Buffer读取到Channel (channel.write(buf))
- 使用get()方法从Buffer中读取数据 (buf.get())
2、详细使用步骤描述
- buffer的几个参数
- capacity:指定了可以存储在缓冲区中的最大数据容量
- position:指的是下一个要被读写的元素的数组下标索引,该值会随get()和put()的调用自动更新
- limit:指的是缓冲区中第一个不能读写的元素的数组下标索引,也可以认为是缓冲区中实际元素的数量
- mark:一个备忘位置,调用mark()方法的话,mark值将存储当前position的值,等下次调用reset()方法时,会设定position的值为之前的标记值
- 四个参数之间的大小关系为:0 <= mark <= position <= limit <= capacity
- buffer的实际使用过程
- 创建一个容量大小为10的字符缓冲区:
ByteBuffer bf = ByteBuffer.allocate(10);
- 创建一个容量大小为10的字符缓冲区:
-
- 往缓冲区中put()五个字节:
bf.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'0');
- 往缓冲区中put()五个字节:
-
- 调用flip()方法,切换为读就绪状态:bf.flip()
-
- 读取两个元素:
System.out.println("" + (char) bf.get() + (char) bf.get());
- 读取两个元素:
-
- 标记此时的position位置:bf.mark()
-
- 读取两个元素后,恢复到之前mark的位置处:
System.out.println("" + (char) bf.get() + (char) bf.get()); bf.reset();
- 读取两个元素后,恢复到之前mark的位置处:
-
- 调用compact()方法,释放已读数据的空间,准备重新填充缓存区:bf.compact() ; 这里要是调用 clear 方法的话,position将被设回0,limit设置成capacity
SocketChannel
一、概述
- NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道
channel.configureBlocking(false)
二、TCP示例
-
client 使用 NIO
public static void client(){ ByteBuffer buffer = ByteBuffer.allocate(1024); SocketChannel socketChannel = null; try { socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("127.0.0.1",8080)); if(socketChannel.finishConnect()) { int i=0; while(true) { TimeUnit.SECONDS.sleep(1); String info = "I'm "+i+++"-th information from client"; buffer.clear(); buffer.put(info.getBytes()); buffer.flip(); while(buffer.hasRemaining()){ System.out.println(buffer); socketChannel.write(buffer); } } } } catch (IOException | InterruptedException e) { e.printStackTrace(); } finally{ try{ if(socketChannel!=null){ socketChannel.close(); } }catch(IOException e){ e.printStackTrace(); } } }
-
server 使用 BIO
public static void server(){ ServerSocket serverSocket = null; InputStream in = null; try { serverSocket = new ServerSocket(8080); int recvMsgSize = 0; byte[] recvBuf = new byte[1024]; while(true){ Socket clntSocket = serverSocket.accept(); SocketAddress clientAddress = clntSocket.getRemoteSocketAddress(); System.out.println("Handling client at "+clientAddress); in = clntSocket.getInputStream(); while((recvMsgSize=in.read(recvBuf))!=-1){ byte[] temp = new byte[recvMsgSize]; System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize); System.out.println(new String(temp)); } } } catch (IOException e) { e.printStackTrace(); } finally{ try{ if(serverSocket!=null){ serverSocket.close(); } if(in!=null){ in.close(); } }catch(IOException e){ e.printStackTrace(); } } }
-
示例说明:channel 的 write()方法无法保证能写多少字节到SocketChannel,所以在死循环里重复调用write()直到Buffer没有要写的字节为止
TCP服务端NIO写法
一、TCP服务端 NIO 代码
public class ServerConnect { private static final int BUF_SIZE=1024; private static final int PORT = 8080; private static final int TIMEOUT = 3000; public static void main(String[] args) { selector(); } public static void handleAccept(SelectionKey key) throws IOException{ ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel(); SocketChannel sc = ssChannel.accept(); sc.configureBlocking(false); sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE)); } public static void handleRead(SelectionKey key) throws IOException{ SocketChannel sc = (SocketChannel)key.channel(); ByteBuffer buf = (ByteBuffer)key.attachment(); long bytesRead = sc.read(buf); while(bytesRead>0){ buf.flip(); while(buf.hasRemaining()){ System.out.print((char)buf.get()); } System.out.println(); buf.clear(); bytesRead = sc.read(buf); } if(bytesRead == -1){ sc.close(); } } public static void handleWrite(SelectionKey key) throws IOException{ ByteBuffer buf = (ByteBuffer)key.attachment(); buf.flip(); SocketChannel sc = (SocketChannel) key.channel(); while(buf.hasRemaining()){ sc.write(buf); } buf.compact(); } public static void selector() { Selector selector = null; ServerSocketChannel ssc = null; try{ selector = Selector.open(); ssc= ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress(PORT)); ssc.configureBlocking(false); ssc.register(selector, SelectionKey.OP_ACCEPT); while(true){ if(selector.select(TIMEOUT) == 0){ System.out.println("=="); continue; } Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); while(iter.hasNext()){ SelectionKey key = iter.next(); if(key.isAcceptable()){ handleAccept(key); } if(key.isReadable()){ handleRead(key); } if(key.isWritable() && key.isValid()){ handleWrite(key); } if(key.isConnectable()){ System.out.println("isConnectable = true"); } iter.remove(); } } }catch(IOException e){ e.printStackTrace(); }finally{ try{ if(selector!=null){ selector.close(); } if(ssc!=null){ ssc.close(); } }catch(IOException e){ e.printStackTrace(); } } } }
二、示例代码说明
1、ServerSocketChannel
- ServerSocketChannel 的打开、关闭和监听(open、close、accept)
- 在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null,因此需要进行非空检查
2、selector
- 管理多信道的机制(主动通知)
- Selector 在内部可以同时管理多个channel,当一个信道有I/O操作的时候,他会通知Selector,Selector就知道这个信道有I/O操作以及具体是何种操作;
- Selector 的 select 方法返回的结果为 0,代表在你调用的时刻没有任何客户端需要I/O操作,不为 0 代表有客户端准备就绪了,返回的就是可操作的信道数量;
- Channel和Selector配合使用
- 二者配合使用必须将Channel注册到Selector上,通过SelectableChannel.register()方法来实现,注册 channel 的时候可以指定监听事件的类型;
- 监听事件的类型无外乎就四种(连接、接受、读、写)这四种事件用SelectionKey的四个常量来表示:
1. SelectionKey.OP_CONNECT 2. SelectionKey.OP_ACCEPT 3. SelectionKey.OP_READ 4. SelectionKey.OP_WRITE
-
selecet 方法返回非 0 ,通过调用如下的方法可以返回一个 SelectionKey 的集合,每一个SK对象都代表着注册到 selector 上的信道,遍历此集合获得SK对象后即可进行后续处理
Set selectedKeys = selector.selectedKeys();
-
Selector不会自己从已选择键集中移除SelectionKey实例,所以每次迭代末尾都要keyIterator.remove()
3、SelectionKey
- ServerSocketChannel 的 register()方法会返回一个SelectionKey对象,这个对象包含如下属性:
- interest集合:监听事件的集合,SK对象的 interestOps 方法可以得到这个集合
- ready集合:是通道已经准备就绪的操作的集合,readyOps方法可以得到这个集合
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
- Channel 和 Selector:通过 channel 和 selector 方法即可获得
- 附加的对象(可选):有两种添加附加信息的方法
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment(); ----------------------------------------------- SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
内存映射文件
一、byteBuffer的两种模式
- 间接模式:就是HeapByteBuffer,即操作堆内存 (byte[]),但是如果文件很大的话可能会出现内存溢出的情况;
- 直接模式:MappedByteBuffer 将文件直接映射到虚拟内存,这种模式读写性能很高;
二、示例代码
public static void method4(){ RandomAccessFile aFile = null; FileChannel fc = null; try{ aFile = new RandomAccessFile("src/1.ppt","rw"); fc = aFile.getChannel(); long timeBegin = System.currentTimeMillis(); ByteBuffer buff = ByteBuffer.allocate((int) aFile.length()); buff.clear(); fc.read(buff); //System.out.println((char)buff.get((int)(aFile.length()/2-1))); //System.out.println((char)buff.get((int)(aFile.length()/2))); //System.out.println((char)buff.get((int)(aFile.length()/2)+1)); long timeEnd = System.currentTimeMillis(); System.out.println("Read time: "+(timeEnd-timeBegin)+"ms"); }catch(IOException e){ e.printStackTrace(); }finally{ try{ if(aFile!=null){ aFile.close(); } if(fc!=null){ fc.close(); } }catch(IOException e){ e.printStackTrace(); } } } public static void method3(){ RandomAccessFile aFile = null; FileChannel fc = null; try{ aFile = new RandomAccessFile("src/1.ppt","rw"); fc = aFile.getChannel(); long timeBegin = System.currentTimeMillis(); MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, aFile.length()); // System.out.println((char)mbb.get((int)(aFile.length()/2-1))); // System.out.println((char)mbb.get((int)(aFile.length()/2))); //System.out.println((char)mbb.get((int)(aFile.length()/2)+1)); long timeEnd = System.currentTimeMillis(); System.out.println("Read time: "+(timeEnd-timeBegin)+"ms"); }catch(IOException e){ e.printStackTrace(); }finally{ try{ if(aFile!=null){ aFile.close(); } if(fc!=null){ fc.close(); } }catch(IOException e){ e.printStackTrace(); } } }
1、示例说明
- 示例代码通过 FileChannel 的 map 方法实现,这个方法有三个参数(模式,position,size)
- map方法的模式参数有三个选项:READ_ONLY(只读)、READ_WRITE(读/写:缓冲区修改会传播到文件)、PRIVATE(专用:修改不会传播到文件,会建立一个专用副本)
2、MappedByteBuffer的特有方法
- force():缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件;
- load():将缓冲区的内容载入内存,并返回该缓冲区的引用;
- isLoaded():如果缓冲区的内容在物理内存中,则返回真,否则返回假;
知识点补充
一、scatter & gatter
1、简介
- 分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中
- 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel
2、示例代码
- 这是一个 gather 的示例代码,实际上是通过 buffer 数组来实现的聚集,channel 调用 write 方法直接使用 buffer 数组作为参数
public class ScattingAndGather { public static void main(String args[]){ gather(); } public static void gather() { ByteBuffer header = ByteBuffer.allocate(10); ByteBuffer body = ByteBuffer.allocate(10); byte [] b1 = {'0', '1'}; byte [] b2 = {'2', '3'}; header.put(b1); body.put(b2); ByteBuffer [] buffs = {header, body}; try { FileOutputStream os = new FileOutputStream("src/scattingAndGather.txt"); FileChannel channel = os.getChannel(); channel.write(buffs); } catch (IOException e) { e.printStackTrace(); } } }
二、transferFrom & transferTo
1、transferFrom
- FileChannel的transferFrom()方法可以将数据从其他通道传输到FileChannel中;
- SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中,只会传输此刻准备好的数据(可能不足count字节);
public static void method1(){ RandomAccessFile fromFile = null; RandomAccessFile toFile = null; try { fromFile = new RandomAccessFile("src/fromFile.xml","rw"); FileChannel fromChannel = fromFile.getChannel(); toFile = new RandomAccessFile("src/toFile.txt","rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); System.out.println(count); toChannel.transferFrom(fromChannel, position, count); } catch (IOException e) { e.printStackTrace(); } finally{ try{ if(fromFile != null){ fromFile.close(); } if(toFile != null){ toFile.close(); } } catch(IOException e){ e.printStackTrace(); } } }
2、transferTo
- transferTo()方法将数据从FileChannel传输到其他的channel中
public static void method2() { RandomAccessFile fromFile = null; RandomAccessFile toFile = null; try { fromFile = new RandomAccessFile("src/fromFile.txt","rw"); FileChannel fromChannel = fromFile.getChannel(); toFile = new RandomAccessFile("src/toFile.txt","rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); System.out.println(count); fromChannel.transferTo(position, count,toChannel); } catch (IOException e) { e.printStackTrace(); } finally{ try{ if(fromFile != null){ fromFile.close(); } if(toFile != null){ toFile.close(); } } catch(IOException e){ e.printStackTrace(); } } }
三、pipe
1、示例代码
- Java NIO 管道是2个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取
public static void method1(){ Pipe pipe = null; ExecutorService exec = Executors.newFixedThreadPool(2); try{ pipe = Pipe.open(); final Pipe pipeTemp = pipe; exec.submit(new Callable<Object>(){ @Override public Object call() throws Exception { Pipe.SinkChannel sinkChannel = pipeTemp.sink();//向通道中写数据 while(true){ TimeUnit.SECONDS.sleep(1); String newData = "Pipe Test At Time "+System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(1024); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()){ System.out.println(buf); sinkChannel.write(buf); } } } }); exec.submit(new Callable<Object>(){ @Override public Object call() throws Exception { Pipe.SourceChannel sourceChannel = pipeTemp.source();//向通道中读数据 while(true){ TimeUnit.SECONDS.sleep(1); ByteBuffer buf = ByteBuffer.allocate(1024); buf.clear(); int bytesRead = sourceChannel.read(buf); System.out.println("bytesRead="+bytesRead); while(bytesRead >0 ){ buf.flip(); byte b[] = new byte[bytesRead]; int i=0; while(buf.hasRemaining()){ b[i]=buf.get(); System.out.printf("%X",b[i]); i++; } String s = new String(b); System.out.println("=================||"+s); bytesRead = sourceChannel.read(buf); } } } }); }catch(IOException e){ e.printStackTrace(); }finally{ exec.shutdown(); } }
四、datagramChannel
1、示例代码
- Java NIO中的DatagramChannel是一个能收发UDP包的通道
- 因为UDP是无连接,所以不能像其它通道那样读取和写入,它发送和接收的是数据包
public static void reveive(){ DatagramChannel channel = null; try{ channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(8888)); ByteBuffer buf = ByteBuffer.allocate(1024); buf.clear(); channel.receive(buf); buf.flip(); while(buf.hasRemaining()){ System.out.print((char)buf.get()); } System.out.println(); }catch(IOException e){ e.printStackTrace(); }finally{ try{ if(channel!=null){ channel.close(); } }catch(IOException e){ e.printStackTrace(); } } } public static void send(){ DatagramChannel channel = null; try{ channel = DatagramChannel.open(); String info = "I'm the Sender!"; ByteBuffer buf = ByteBuffer.allocate(1024); buf.clear(); buf.put(info.getBytes()); buf.flip(); int bytesSent = channel.send(buf, new InetSocketAddress("10.10.195.115",8888)); System.out.println(bytesSent); }catch(IOException e){ e.printStackTrace(); }finally{ try{ if(channel!=null){ channel.close(); } }catch(IOException e){ e.printStackTrace(); } } }
*******************参考**********************