1. 程式人生 > >Java之NIO

Java之NIO

Java NIO是從Java 1.4版本開始引入的一個新的IO API,與傳統IO 的主要區別

IO NIO
面向流(Stream Oriented 面向緩衝區(Buffer Oriente
阻塞IO(Blocking IO 非阻塞IO(Non Blocking I
選擇器(Selector

Java NIO系統的核心在於:通道(Channel)和緩衝區(Buffer)。通道表示開啟到 IO 裝置(例如:檔案、套接字)的連線。若需要使用 NIO 系統,需要獲取用於連線 IO 裝置的通道以及用於容納資料的緩衝區。然後操作緩衝區,對資料進行處理。

也就是說:Channel 負責傳輸, Buffer 負責儲存

一、緩衝區(Buffer):

在 Java NIO 中負責資料的存取。緩衝區就是陣列。用於儲存不同資料型別的資料

 根據資料型別不同(boolean 除外),提供了相應型別的緩衝區:  ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer, DoubleBuffer

緩衝區存取資料的兩個核心方法:  put() : 存入資料到緩衝區中  get() : 獲取緩衝區中的資料

緩衝區中的四個核心屬性:  capacity : 容量,表示緩衝區中最大儲存資料的容量。一旦宣告不能改變。  limit : 界限,表示緩衝區中可以操作資料的大小。(limit 後資料不能進行讀寫)  position : 位置,表示緩衝區中正在操作資料的位置。  mark : 標記,表示記錄當前 position 的位置。可以通過 reset() 恢復到 mark 的位置

Buffer在讀寫的時候有兩個重要的方法

Buffer clear 清空緩衝區並返回對緩衝區的
Buffer flip 將緩衝區的界限設定為當前位置,並將當前位置充值為

比如說我們在利用通道進行復制檔案的時候,常常會這樣用到

			//②分配指定大小的緩衝區
			ByteBuffer buf = ByteBuffer.allocate(1024);
			//③將通道中的資料存入緩衝區中
			while(inChannel.read(buf) != -1){
				buf.flip(); //切換讀取資料的模式
				//④將緩衝區中的資料寫入通道中
				outChannel.write(buf);
				buf.clear(); //清空緩衝區
			}

先把通道中的資料讀到緩衝區,假設緩衝區讀到了一半的位置,用position方法就可得到當前正在操作資料的位置(512),使用flip方法切換到把buf的內容寫入到通道模式,如果我們再用position方法就會得到位置為0,limit方法得到位置512,也就是說,我們只能把buf裡面(0~512)個位元組的資料寫入到outChannel裡面。

二 、通道(Channel):用於源節點與目標節點的連線。在 Java NIO 中負責緩衝區中資料的傳輸。Channel 本身不儲存資料,因此需要配合緩衝區進行傳輸。

Java 為 Channel 介面提供的最主要實現類如下

FileChannel:用於讀取、寫入、對映和操作檔案的通

DatagramChannel:通過 UDP 讀寫網路中的資料通

SocketChannel:通過 TCP 讀寫網路中的數

ServerSocketChannel:可以監聽新進來的 TCP 連線,對每一個新進來的連線都會建立一個 SocketChanne

獲取通道的一種方式是對支援通道的物件呼叫getChannel() 方法。支援通道的類如下

本地 IO:

FileInputStream

FileOutputStream

RandomAccess

網路IO:

DatagramSock

Socket

ServerSock

獲取通道的其他方式是使用 Files 類的靜態方法 newByteChannel() 獲取位元組通道。或者通過通道的靜態方法 open() 開啟並返回

指定通道。

三 、NIO 的非阻塞式網路通訊

      傳統的 IO 流都是阻塞式的。也就是說,當一個執行緒呼叫 read() 或 write()時,該執行緒被阻塞,直到有一些資料被讀取或寫入,該執行緒在此期間不能執行其他任務。因此,在完成網路通訊進行 IO 操作時,由於執行緒會阻塞,所以伺服器端必須為每個客戶端都提供一個獨立的執行緒進行處理,當伺服器端需要處理大量客戶端時,效能急劇下降。

       Java NIO 是非阻塞模式的。當執行緒從某通道進行讀寫資料時,若沒有資料可用時,該執行緒可以進行其他任務。執行緒通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的執行緒可以管理多個輸入和輸出通道。因此,NIO 可以讓伺服器端使用一個或有限幾個執行緒來同時處理連線到伺服器端的所有客戶端。

使用 NIO 完成網路通訊的三個核心:

1. 通道(Channel):負責連線

2. 緩衝區(Buffer):負責資料的存取

3. 選擇器(Selector):是 SelectableChannel 的多路複用器。用於監控 SelectableChannel 的 IO 狀況。利用 Selector 可使一個單獨的執行緒管理多個 Channel。Selector 是非阻塞 IO 的核心。

所有連線的通道都要註冊到選擇器上,並指定監聽事件,讓選擇器來統一管理。

可以監聽的事件型別(可使用 SelectionKey 的四個常量表示): 讀 : SelectionKey.OP_READ (1) 寫 : SelectionKey.OP_WRITE (4) 連線 : SelectionKey.OP_CONNECT (8) 接收 : SelectionKey.OP_ACCEPT (16)

下面是一個網路通訊的案例:服務端建立一個ServerSocketChannel用於監聽客戶端,並註冊到選擇器上,指定事件為接收事件,如果客戶端的通道準備好了,請求連線,那麼選擇器就會進行處理,服務端會把該通道註冊到選擇器上,並指定時間為讀事件。然後再迭代選擇器裡面的通道依次處理這些通道。

 NIO 完成網路通訊的案例:

public class TestNonBlockingNIO {
	
	//客戶端
	@Test
	public void client() throws IOException{
		//1. 獲取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		//2. 切換非阻塞模式
		sChannel.configureBlocking(false);
		
		//3. 分配指定大小的緩衝區
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//4. 傳送資料給服務端
		Scanner scan = new Scanner(System.in);
		
		while(scan.hasNext()){
			String str = scan.next();
			buf.put((new Date().toString() + "\n" + str).getBytes());
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		//5. 關閉通道
		sChannel.close();
	}

	//服務端
	@Test
	public void server() throws IOException{
		//1. 獲取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		//2. 切換非阻塞模式
		ssChannel.configureBlocking(false);
		
		//3. 繫結連線
		ssChannel.bind(new InetSocketAddress(9898));
		
		//4. 獲取選擇器
		Selector selector = Selector.open();
		
		//5. 將通道註冊到選擇器上, 並且指定“監聽接收事件”
		ssChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//6. 輪詢式的獲取選擇器上已經“準備就緒”的事件
		while(selector.select() > 0){
			
			//7. 獲取當前選擇器中所有註冊的“選擇鍵(已就緒的監聽事件)”
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			
			while(it.hasNext()){
				//8. 獲取準備“就緒”的是事件
				SelectionKey sk = it.next();
				
				//9. 判斷具體是什麼事件準備就緒
				if(sk.isAcceptable()){
					//10. 若“接收就緒”,獲取客戶端連線
					SocketChannel sChannel = ssChannel.accept();
					
					//11. 切換非阻塞模式
					sChannel.configureBlocking(false);
					
					//12. 將該通道註冊到選擇器上
					sChannel.register(selector, SelectionKey.OP_READ);
				}else if(sk.isReadable()){
					//13. 獲取當前選擇器上“讀就緒”狀態的通道
					SocketChannel sChannel = (SocketChannel) sk.channel();
					
					//14. 讀取資料
					ByteBuffer buf = ByteBuffer.allocate(1024);
					
					int len = 0;
					while((len = sChannel.read(buf)) > 0 ){
						buf.flip();
						System.out.println(new String(buf.array(), 0, len));
						buf.clear();
					}
				}
				
				//15. 取消選擇鍵 SelectionKey
				it.remove();
			}
		}
	}
}