NIO中主要包括几大组件,selector、channel、buffer。selector后面介绍,channel则相似于BIO中的流,可是流的读取是单向的,例如只能读,或只能写,可是channel则是双向的。数据能够从channel读到buffer中,也能够从buffer中写入到channel中。网络
针对于客户端请求服务端的场景,NIO实现的结构图以下:dom
禁止盗图,画了很久。。。socket
channel主要包括如下几类性能
基本示例:学习
//建立能访问任意位置的file文件 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); //获取fileChannel对象 FileChannel inChannel = aFile.getChannel(); //分配48字节大小的byteBuffer ByteBuffer buf = ByteBuffer.allocate(48); //读取channel中的数据到buffer中 int bytesRead = inChannel.read(buf); while (bytesRead != -1) { System.out.println("Read " + bytesRead); //将buffer从写模式切换到读模式 buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get()); } //清空整个缓冲区 buf.clear(); //缓冲区已经所有读完,返回-1退出循环 bytesRead = inChannel.read(buf); } aFile.close();
另外channel不只能够读取数据到buffer中,当存在多个channel而且其中有一个channel为fileChannel时,channel之间能够互相传输数据spa
transferFrom() :能够将数据从源通道传输到FileChannel中线程
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); toChannel.transferFrom(position, count, fromChannel);
transferTo()3d
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); fromChannel.transferTo(position, count, toChannel);
flip()
方法clear()
方法或者compact()
方法------------>clear()方法会清空整个缓冲区,compact()只会清除已经读过的数据为了理解Buffer的工做原理,须要熟悉它的三个属性:rest
position和limit的含义取决于Buffer处在读模式仍是写模式。无论Buffer处在什么模式,capacity的含义老是同样的。code
做为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,须要将其清空(经过读数据或者清除数据)才能继续写数据往里写数据。
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。所以,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到以前写入的全部数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
//分配48字节大小的byteBuffer ByteBuffer buf = ByteBuffer.allocate(48);
//分配2014字符大小的charBuffer CharBuffer buf = CharBuffer.allocate(1024);
包括两种方式:一是从channel中读数据到buffer中,另一种就是调用buffer的put()方法
//1、channel读取数据到buffer int bytesRead = inChannel.read(buf); //2、调用channel的put()方法 buf.put(127);
一样包括两种方式:一是写入数据到channel中,另一种就是调用buffer的get()方法
//1、将数据写入到channel中 int bytesWritten = inChannel.write(buf); //2、调用buffer的get()方法读取数据 byte aByte = buf.get();
buffer.flip()
一旦读完Buffer中的数据,须要让Buffer准备好再次被写入。能够经过clear()或compact()方法来完成。
若是调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉咱们能够从哪里开始往Buffer里写数据。
若是Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着再也不有任何标记会告诉你哪些数据被读过,哪些尚未。
若是Buffer中仍有未读的数据,且后续还须要这些数据,可是此时想要先先写些数据,那么使用compact()方法。
compact()方法将全部未读的数据拷贝到Buffer起始处。而后将position设到最后一个未读元素正后面。limit属性依然像clear()方法同样,设置成capacity。如今Buffer准备好写数据了,可是不会覆盖未读的数据。
另外NIO支持scatter/gather,说白了,就是一个channel能够读取数据到多个buffer中去,或者多个buffer能够写入到channel中。当第一个buffer写满以后,会紧接着读取到第二个buffer中去,以下图:
channel-->buffer
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
buffer-->channel
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
为啥使用selector?在传统的BIO当中,监听每一个客户端的请求都须要一个线程去处理,线程数的上升会涉及到大量的上下文切换的操做,这也是很是浪费性能的。NIO中基于事件驱动的理念,使用selector监听各类事件的发生,能够实现只开启一个线程
就能够管理全部的请求,固然实际状况合理的增长线程数能够大大提升性能。
注意:与Selector一块儿使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一块儿使用,由于FileChannel不能切换到非阻塞模式。
channel.configureBlocking(false); SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
selectionKey用来描述事件,包括事件类型,以及对应的selector与channel等等。
第二个入参为事件类型,主要包括四种:
分别用常量表示为:
固然selector监听channel时,能够对多个事件感兴趣,写法以下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
可能对selector、channel、事件三者的关系有点乱,用故事来总结一下:selector是父母,channel是孩子,父母监督孩子学习,孩子有不少同窗,有些同窗学习好,有些同窗学习差,父母欢迎学习好的学生来家里玩,排斥成绩差的。那么特定的事件就能够理解
成那些成绩差的同窗来家中,父母监听到了,开始行动,将他们赶走。
当channel注册到selector中时,会返回一个selectionKey对象,能够理解成事件的描述或是对注册的描述,主要包括这几个部分:
int interestSet = selectionKey.interestOps(); //判断事件是否在集合中 boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
int readySet = selectionKey.readyOps(); //事件是否在已准备就绪的集合中,selectionKey提供了以下方法 selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
用户也能够将buffer等其余对象加到selectionKey上,方便后续操做
添加对象:
//添加对象到selectionKey有两种方式 selectionKey.attach(theObject); SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject); //获取此附加对象 Object attachedObj = selectionKey.attachment();
当把channel注册到selector中去以后,能够经过select()方法来监听对应channel的特定事件。主要有三种select方法:
Set selectedKeys = selector.selectedKeys();
完整的示例:
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } }
前面的示例都是基于TCP链接,如今讲述一下UDP的示例.
DatagramChannel是一个能收发UDP包的通道。由于UDP是无链接的网络协议,因此不能像其它通道那样读取和写入。它发送和接收的是数据包。
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999));
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));