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));