Java NIO API詳解
http://www.blogjava.net/19851985lili/articles/93524.html
這篇文章對nio的api講解比較全,可以協助在宏觀上把握nio。
BIO 方式使得整個處理過程和串連是綁定的,只要串連建立,無論用戶端是否有訊息發送,都要進行等待處理,一定程度上浪費了伺服器端的硬體資源,因此就有了NIO 方式。Java 對於 NIO 方式的支援是通過 Channel和 Selector 方式來實現,採用的方法為向 Channel註冊感興趣的事件,然後通過 Selector 來擷取到發生了事件的 key,如發生了相應的事件,則進行相應的處理,否則則不做任何處理,是典型的Reactor 模式,按照這樣的方式,就不用像 BIO 方式一樣,即使在沒有訊息的情況下也需要佔據一個線程來阻塞讀取訊息,從而提升伺服器的使用效率, 為實現 TCP/IP+NIO 方式的系統間通訊, Java 提供了 SocketChannel和 ServerSocketChannel兩個關鍵的類,網路 IO 的操作則改為通過ByteBuffer 來實現,具體的基於 java實現TCP/IP+NIO 方式的通訊的方法如下所示。
伺服器端:
package com.flyoung;import java.io.IOException;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.util.Iterator;import java.util.Set;import java.nio.channels.SocketChannel;public class NIOServer { /*標誌數字*/ private static int flag = 0; /*定義緩衝區大小*/ private static int block = 4096; /*接收緩衝區*/ private static ByteBuffer receiveBuffer = ByteBuffer.allocate(block); /*發送緩衝區*/ private static ByteBuffer sendBuffer = ByteBuffer.allocate(block); /*定義Selector*/ private Selector selector; public NIOServer(int port) throws IOException{ //開啟伺服器通訊端通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //伺服器配置為非阻塞 serverSocketChannel.configureBlocking(false); //檢索與此伺服器通訊端通道關聯的通訊端 ServerSocket serverSocket = serverSocketChannel.socket(); //進行服務的綁定 serverSocket.bind(new InetSocketAddress(port)); //通過open()方法找到Selector selector = Selector.open(); //註冊到selector serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("Server Start -----8888:"); } //監聽 public void listen() throws IOException{ while(true){ //監控所有註冊的 channel ,當其中有註冊的 IO 操作可以進行時,該函數返回,並將對應的 SelectionKey 加入 selected-key set selector.select(); //Selected-key set 代表了所有通過 select() 方法監測到可以進行 IO 操作的 channel ,這個集合可以通過 selectedKeys() 拿到 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while(iterator.hasNext()){ SelectionKey selectionKey = iterator.next(); handleKey(selectionKey); iterator.remove(); } } } //處理請求 public void handleKey(SelectionKey selectionKey) throws IOException{ //接受請求 ServerSocketChannel serverSocketChannel = null; SocketChannel socketChannel = null; String receiveText; String sendText; int count; //測試此鍵的通道是否準備好接受新的通訊端串連 if(selectionKey.isAcceptable()){ //返回建立此鍵的通道 serverSocketChannel = (ServerSocketChannel)selectionKey.channel(); //接受用戶端建立串連的請求,並返回 SocketChannel 對象 socketChannel = serverSocketChannel.accept(); //配置為非阻塞 socketChannel.configureBlocking(false); //註冊到selector socketChannel.register(selector, SelectionKey.OP_READ); }else if(selectionKey.isReadable()){ //返回為之建立此鍵的通道 socketChannel = (SocketChannel)selectionKey.channel(); //將緩衝區清空,以備下次讀取 receiveBuffer.clear(); //將發送來的資料讀取到緩衝區 count = socketChannel.read(receiveBuffer); if(count>0){ receiveText = new String(receiveBuffer.array(),0,count); System.out.println("伺服器端接受到的資料---"+receiveText); socketChannel.register(selector, SelectionKey.OP_WRITE); } }else if (selectionKey.isWritable()) { //將緩衝區清空以備下次寫入 sendBuffer.clear(); // 返回為之建立此鍵的通道。 socketChannel = (SocketChannel) selectionKey.channel(); sendText="message from server--" + flag++; //向緩衝區中輸入資料 sendBuffer.put(sendText.getBytes()); //將緩衝區各標誌複位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要複位 sendBuffer.flip(); //輸出到通道 socketChannel.write(sendBuffer); System.out.println("伺服器端向用戶端發送資料--:"+sendText); socketChannel.register(selector, SelectionKey.OP_READ); } } public static void main(String[] args) throws IOException { int port = 8888; NIOServer server = new NIOServer(port); server.listen(); }}
用戶端
package com.flyoung;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.SocketChannel;import java.util.Set;public class NIOClient { /*標識數字*/ private static int flag = 0; /*緩衝區大小*/ private static int BLOCK = 4096; /*接受資料緩衝區*/ private static ByteBuffer sendBuffer = ByteBuffer.allocate(BLOCK); /*發送資料緩衝區*/ private static ByteBuffer receiveBuffer = ByteBuffer.allocate(BLOCK); /*伺服器端地址*/ private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress( "localhost", 8888); public static void main(String[] args) throws IOException { // 開啟socket通道 SocketChannel clientChannel = SocketChannel.open(); // 設定為非阻塞方式 clientChannel.configureBlocking(false); // 開啟選取器 Selector selector = Selector.open(); // 註冊串連服務端socket動作 clientChannel.register(selector, SelectionKey.OP_CONNECT); // 串連 clientChannel.connect(SERVER_ADDRESS); SocketChannel socketChannel; Set<SelectionKey> selectionKeys; String receiveText; String sendText; int count=0; while (true) { //選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。 //監控所有註冊的 channel ,當其中有註冊的 IO 操作可以進行時,該函數返回,並將對應的 SelectionKey 加入 selected-key set selector.select(); //返回此選取器的已選擇鍵集。 selectionKeys = selector.selectedKeys(); //System.out.println(selectionKeys.size()); for(SelectionKey selectionKey:selectionKeys){ //判斷是否為建立串連的事件 if (selectionKey.isConnectable()) { System.out.println("client connect"); socketChannel = (SocketChannel) selectionKey.channel(); // // 判斷此通道上是否進行中串連操作。 // 完成通訊端通道的串連過程。 if (socketChannel.isConnectionPending()) { //完成串連的建立(TCP三向交握) socketChannel.finishConnect(); System.out.println("完成串連!"); sendBuffer.clear(); sendBuffer.put("Hello,Server".getBytes()); sendBuffer.flip(); socketChannel.write(sendBuffer); } socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { socketChannel = (SocketChannel) selectionKey.channel(); //將緩衝區清空以備下次讀取 receiveBuffer.clear(); //讀取伺服器發送來的資料到緩衝區中 count=socketChannel.read(receiveBuffer); if(count>0){ receiveText = new String( receiveBuffer.array(),0,count); System.out.println("用戶端接受伺服器端資料--:"+receiveText); socketChannel.register(selector, SelectionKey.OP_WRITE); } } else if (selectionKey.isWritable()) { sendBuffer.clear(); socketChannel = (SocketChannel) selectionKey.channel(); sendText = "message from client--" + (flag++); sendBuffer.put(sendText.getBytes()); //將緩衝區各標誌複位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要複位 sendBuffer.flip(); socketChannel.write(sendBuffer); System.out.println("用戶端向伺服器端發送資料--:"+sendText); socketChannel.register(selector, SelectionKey.OP_READ); } } selectionKeys.clear(); } } }
小結:之前對Selector註冊事件和SocketChannel有點小困惑。SocketChannel就像一根水管,當監聽到寫事件時,就往管道寫資料;當監聽到讀事件時,就從管道讀出資料。