io編程中存在兩個問題,io是阻塞的,並且保持多個鏈接的時候須要加入多線程來保持socket鏈接。這種方式比較浪費資源,由於每一個鏈接都須要一個線程來保持,這在鏈接比較多的時候是一個浪費的資源是至關嚴重的。而兩個問題在nio編程中就很好的解決這個問題,可是相對來講,對於編程的複雜度也相對有點提升。java
這裏不去深刻的討論nio編程的原理,只是基於實用的角度上作一些簡單的介紹。在nio編程中,主要有三個組件比較重要:channel、buffer、selector。編程
channel:網絡
通道,它和io中流有點類似,因此的源數據或者目標數據都是直接和channel打交道的。java nio中channel有如下幾種實現,這些通道涵蓋了TCP和UDP網絡io:多線程
buffer:socket
緩衝區 , 他是直接與channel交互的,咱們若是要讀取channle中的數據時,都是先把數據從channle中讀入到buffer中;一樣的,若是咱們要向channel中寫數據的時候,也要先把數據寫入到buffer中。java nio中有也有多個實現,他們不要爲了針對不一樣的數據類型:函數
注意:這裏要介紹一個buffer中三個比較重要的方法,這三個方法在buffer讀寫的過程當中都頗有用。這三個方法都跟buffer中三個標記有關係:position、limit、capacity。position至關於指針,表示當前正在讀寫的位置;limit指的是界限,表示讀寫的時候界限;capacity表示容量,也就是buffer緩衝區的大小。爲了弄明白這些方法,瞭解這三個標記是前提,說了這麼多,這三個方法是:clear(); rewind(); flip()。這三個方法也就是調整這些標記位置,下面是這三個方法的源碼,相信很好理解:post
java nio緩衝區中標誌mark標記,使緩衝區可以記住一個位置並在以後將其返回。緩衝區的標記在mark( )函數被調用以前是未定義的,調用時標記被設爲當前位置的值。reset( )函數將位置設爲當前的標記值。若是標記值未定義,調用reset( )將致使InvalidMarkException異常。一些緩衝區函數會拋棄已經設定的標記(rewind( ),clear( ),以及flip( )老是拋棄標記)。若是新設定的值比當前的標記小,調用limit( )或position( )帶有索引參數的版本會拋棄標記。如調用mark( )來設定mark = postion。調用reset( )設定position = mark。標記在設定前是未定義的(undefined)。這四個屬性之間老是遵循如下關係: 0 <= mark <= position <= limit <= capacitythis
selector:線程
選擇器,這個就是nio爲了解決io中多鏈接必需要多線程來處理的問題。selector容許單線程處理多個channel,你打開多個鏈接就會有多個channel,而selector能夠管理多個channel。值得注意的是,selector必需要和非阻塞的通道配合使用,也就是說selector不能和fileChannel一塊兒使用,由於fileChannel不能配置非阻塞的模式。指針
java io和java nio的主要區別:
一、io是面向流的,nio是面向緩衝區的。這就意味着io只能從流中讀入一個或者或者多個字節。而nio由於是面向緩衝區的,數據都是先讀入到緩衝區,也就是上面buffer中,並且在須要的時候,還能夠在緩衝區中先後移動來獲取不一樣位置的數據。可是也有個麻煩的地方,就是要檢查緩衝區中是否含有本身要處理的數據。
二、io是阻塞的,nio是非阻塞的。io在數據讀入或者寫入的時候是阻塞的,直到數據處理完成爲止。而nio則是非阻塞的,咱們從buffer中讀取數據的時候是當即返回的,可是這裏有個問題就是咱們沒法保證緩衝區中是有數據的。可是好處就是,咱們讀取數據的時候不會阻塞,能夠幹別的事情。
下圖能夠說明這個問題(左邊的是nio,右邊的是io):
下面就是一個基於NIO的socket編程的例子:
public class NioSocketServer { public static void main(String[] args) { try { ServerSocketChannel server = ServerSocketChannel.open(); //新建NIO通道 server.configureBlocking(false); //配置通道爲非阻塞的 ServerSocket socket = server.socket(); //建立socket服務 socket.bind(new InetSocketAddress(9123)); //綁定端口號 Selector selector = Selector.open(); //建立選擇器 server.register(selector, SelectionKey.OP_ACCEPT); //將服務註冊到選擇器上,而且配置關注的事件 //循環獲取通道內是否有關注的事件 while(true) { //判斷通道中事發後有關注的事件。若是 >1 則表示通道中有關注的事件 int num = selector.select(); if(num < 1) { continue; } Set selectKeys = selector.selectedKeys();//得到事件的key值 //遍歷全部的事件 Iterator iterator = selectKeys.iterator(); //得到迭代器 while(iterator.hasNext()) { SelectionKey key = (SelectionKey) iterator.next(); //移走這次事件 iterator.remove(); //判斷該事件是不是請求鏈接的事件 if(key.isAcceptable()) { //得到相應的socket channel SocketChannel client = server.accept(); System.out.println("接受來自" + client + "的請求!"); client.configureBlocking(false); //將通道配置成非阻塞的 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[1024]); //建立緩衝區 //在此通道上註冊寫事件 SelectionKey keyClient = client.register(selector, SelectionKey.OP_WRITE); //通道執行事件 keyClient.attach(byteBuffer); }else if(key.isWritable()) { //得到此通道上的socket channel SocketChannel client = (SocketChannel) key.channel(); //得到當前事件下的socketChannel ByteBuffer outByteBuffer = (ByteBuffer)key.attachment(); //得到通道上綁定的byteBuffer //若是緩衝區存在數據,清除掉,防止數據粘連,重置一下 if(outByteBuffer.hasRemaining()) { outByteBuffer.clear(); } //往此通道上寫數據,先寫入byteBuffer中 outByteBuffer.put("hello world".getBytes()); outByteBuffer.flip(); client.write(outByteBuffer); } key.channel().close(); } } } catch (IOException e) { e.printStackTrace(); } } }