第五章 NIO
5.1我們為什麼需要NIO
多線程環境下對共用狀態進行同步訪問,增加了系統調度和切換內容相關的開銷,程式員對這些開銷無法控制。
阻塞等待。
我們需要一種可以一次輪詢一組用戶端,以尋找那個用戶端需要服務。在NIO中,一個Channel代表一個可以輪詢的I/O目標,Channel能夠註冊一個Selector執行個體。Selector的select可以尋找“在當前一組通道中,哪一個需要服務”。
Buffer提供了比Stream抽象更高效和可預測的I/O。Stream抽象好的方面是隱藏了底層緩衝區的有限性,提供了一個能夠容納任意長度資料的容器的假象,要麼會產生大量的記憶體開銷,要麼會引入大量的環境切換。使用線程的過程中,這些開銷都隱藏在具體實現中,也失去了對其的可控性和可預測性。這種方法使得編寫程式變得容易,但調整他們的效能則變得困難。不幸的是,使用Java的Socket抽象,流是唯一的選擇。
Buffer抽象代表了一個有限容量的資料容器——其本質是一個數組,由指標指示了在哪存放資料和在哪讀取資料。使用Buffer有兩個好處,第一、與讀寫緩衝區資料相關聯的系統開銷暴露給了程式員,第二、一些對Java對象特殊的Buffer映射能夠直接操作底層的平台的資源,這樣操作節省了在不同的地址空間複製資料的開銷。
5.2 與Buffer一起使用Channel
Channel使用的不是流而是緩衝區來發送或者讀取資料。Buffer類或者其任何子類的執行個體都可以看做是一個定長的Java基礎資料型別 (Elementary Data Type)元素序列。與流不同,緩衝區由固定的、有限的容量,並由內部狀態記錄了由多少資料放入或者取出,就像是有限容量的隊列一樣。在Channel中使用的Buffer通常不是建構函式建立的,而是通過調用allocate()方法建立指定容量的Buffer執行個體:
ByteBuffer buffer = ByteBuffer.allocate(CAPACITY)
或者使用封裝一個已有資料來實現
ByteBuffer buffer = ByteBuffer.wrap(byteArray)
NIO的強大來自於channel的非阻塞特性。
下面是一個字元回顯的非阻塞用戶端
package com.suifeng.tcpip.chapter5;import java.io.IOException;import java.net.InetSocketAddress;import java.net.SocketException;import java.nio.ByteBuffer;import java.nio.channels.SocketChannel;/** * 非阻塞用戶端 * * @author Suifeng * */public class TCPEchoClientNonBlocking{public static void main(String[] args) throws IOException{if (args.length < 2 || args.length > 3){throw new IllegalArgumentException("Parameters:<Server> <Word> [<Port>]");}String server = args[0];byte[] msg = args[1].getBytes();int serverPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;SocketChannel channel = SocketChannel.open();// 將通道置為非阻塞方式channel.configureBlocking(false);if(!channel.connect(new InetSocketAddress(server,serverPort))){System.out.print("Trying connected server");// 輪詢串連的狀態,知道建立串連,這樣忙等比較浪費資源,while(!channel.finishConnect()){System.out.print(".");}}System.out.println("\nClient has connected to server successfully");// 寫緩衝區ByteBuffer writeBuffer = ByteBuffer.wrap(msg);// 讀緩衝區ByteBuffer readBuffer = ByteBuffer.allocate(msg.length);int totalBytesReceived = 0;int bytesReceived = -1;System.out.print("Waiting for server Response");while(totalBytesReceived < msg.length){// 向伺服器發送資料if(writeBuffer.hasRemaining()){channel.write(writeBuffer);}// 等待伺服器返回資料if((bytesReceived = channel.read(readBuffer)) == -1){throw new SocketException("Connection closed prematurely");}totalBytesReceived += bytesReceived;System.out.print(".");}System.out.println("");System.out.println("Received: "+new String(readBuffer.array(),0,totalBytesReceived));channel.close();}}
啟動伺服器端,監聽39393連接埠
啟動用戶端
再次查看伺服器端
5.3 Selector
一個Selector執行個體可以檢查一組通道的I/O狀態。
下面使用通道和選取器實現一個回顯伺服器,並且不適用多線程和忙等。
協議介面
package com.suifeng.tcpip.chapter5;import java.io.IOException;import java.nio.channels.SelectionKey;/** * 回顯伺服器協議介面 * @author Suifeng * */public interface TCPProtocol{/** * 接收請求 * @param key * @throws IOException */void handleAccept(SelectionKey key) throws IOException;/** * 讀取資料 * @param key * @throws IOException */void handleRead(SelectionKey key) throws IOException;/** * 接收資料 * @param key * @throws IOException */void handleWrite(SelectionKey key) throws IOException;}
回顯協議的實現
package com.suifeng.tcpip.chapter5;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;/** * 使用選取器和通道實現的回顯協議 * @author Administrator * */public class EchoSelectorProtocol implements TCPProtocol{private int bufferSize;public EchoSelectorProtocol(int bufferSize) {super();this.bufferSize = bufferSize;}@Overridepublic void handleAccept(SelectionKey key) throws IOException{System.out.println("Handle Accepting Now...");SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();// 設定為阻塞方式channel.configureBlocking(false);// 通道可讀channel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));}@Overridepublic void handleRead(SelectionKey key) throws IOException{System.out.println("Handle Reading Now...");SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buf = (ByteBuffer) key.attachment();long bytesRead = channel.read(buf);System.out.println("Receiving from client:" + channel.socket().getRemoteSocketAddress()+"\nReceived:"+new String(buf.array()));if (bytesRead == -1){channel.close();}else if(bytesRead > 0){// 通道可讀、可寫key.interestOps(SelectionKey.OP_WRITE | SelectionKey.OP_READ);}}@Overridepublic void handleWrite(SelectionKey key) throws IOException{System.out.println("Handling Writing Now....");ByteBuffer buf = (ByteBuffer) key.attachment();buf.flip();SocketChannel channel = (SocketChannel) key.channel();// 向用戶端寫入資料channel.write(buf);if (!buf.hasRemaining()){// 通道可讀key.interestOps(SelectionKey.OP_READ);}buf.compact();}}
伺服器端
package com.suifeng.tcpip.chapter5;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.util.Iterator;/** * 非阻塞處理伺服器端 * * @author Suifeng * */public class TCPServerSelector{private static final int BUFFER_SIZE = 32;private static final int TIMEOUT = 3000;public static void main(String[] args) throws IOException{if (args.length < 1){throw new IllegalArgumentException("Parameter(s):<Port> ...");}// 建立選取器執行個體Selector selector = Selector.open();// 可以同時監聽來自多個通道的資料,使用不同的連接埠for (String arg : args){// 建立通道ServerSocketChannel serverChannel = ServerSocketChannel.open();// 偵聽指定的連接埠serverChannel.socket().bind(new InetSocketAddress(Integer.parseInt(arg)));// 將通道設定為非阻塞方式serverChannel.configureBlocking(false);// 該通道可以進行accept操作serverChannel.register(selector, SelectionKey.OP_ACCEPT);}TCPProtocol protocol = new EchoSelectorProtocol(BUFFER_SIZE);System.out.println("Server is Running.");while(true){// 阻塞等待直到逾時if(selector.select(TIMEOUT) == 0){System.out.println("Waiting data from client.");continue;}// 擷取選取器下的鍵集Iterator<SelectionKey> keys = selector.selectedKeys().iterator();while(keys.hasNext()){SelectionKey key = keys.next();if(key.isAcceptable())// accept操作{protocol.handleAccept(key);}if(key.isReadable())// 可讀{protocol.handleRead(key);}if(key.isValid() && key.isWritable())// 可寫{protocol.handleWrite(key);} keys.remove();}}}}
啟動伺服器端,偵聽39393和39395連接埠
啟動用戶端,一次使用39393連接埠和39395連接埠發送資料
再次查看伺服器端