Java NIO核心概念總結篇
最近學習Java NIO的相關知識,為了以後方便複習記錄下主要知識點。
參考來源:某視訊中的講解以及一些博文,見文章結尾。
一、Java NIO基本介紹
Java NIO(New IO,也有人叫:Non Blocking IO)是從Java1.4版本開始引入的一個新的IO API,其與原來的IO有同樣的作用和目的,但是使用方式有很大的差別。NIO是為提供I/O吞吐量而專門設計,其卓越的效能甚至可以與C媲美。NIO是通過Reactor模式的事件驅動機制來達到Non blocking的,那麼什麼是Reactor模式呢?Reactor翻譯成中文是“反應器”,就是我們將事件註冊到Reactor中,當有相應的事件發生時,Reactor便會告知我們有哪些事件發生了,我們再根據具體的事件去做相應的處理(引用自:
NIO支援面向緩衝區的、基於通道的IO操作,將以更加高效的方式進行檔案的讀寫操作。
NIO的三個核心模組:Buffer(緩衝區)、Channel(通道)、Selector(選擇器)。
二、Java NIO與IO的主要區別
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向緩衝區(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
無 | 選擇器(Selectors) |
三、通道和緩衝區
1.緩衝區:
1.1 基本概念:緩衝區(Buffer)就是在記憶體中預留指定位元組數的儲存空間用來對輸入/輸出(I/O)的資料作臨時儲存,這部分預留的記憶體空間就叫做緩衝區;
1.2 作用:用來臨時儲存資料,可以理解為是I/O操作中資料的中轉站。緩衝區直接為通道(Channel)服務,寫入資料到通道或從通道讀取資料,這樣的操利用緩衝區資料來傳遞就可以達到對資料高效處理的目的。
1.3 型別:Buffer就像一個數組,可以儲存多個相同型別的資料,根據資料型別的不同(Boolean型別除外),有以下七個Buffer常用的子類:ByteBuffer、CharBuffer 、ShortBuffer 、IntBuffer 、LongBuffer 、FloatBuffer 、DoubleBuffer 。
1.4 緩衝區的四個基本屬性
屬性 | 概念 |
---|---|
容量(capacity) | 表示Buffer最大的資料容量,緩衝區的容量不能為負數,而且一旦建立,不可修改 |
限制(limit) | 緩衝區中當前的資料量,即位於limit之後的資料不可讀寫 |
位置(position) | 下一個要讀取或寫入的資料的索引 |
標記(mark) | 呼叫mark()方法來記錄一個特定的位置:mark=position,然後再呼叫reset()可以讓position恢復到標記的位置即position=mark |
*** 容量、限制、位置、標記遵守以下不變式:0 <= mark <= position <= limit <= capacity
1.5 建立緩衝區:獲取一個指定容量的xxxBuffer物件,以ByteBuffer為例:
建立一個容量大小為1024的ByteBuffer陣列,需要注意的是:所有的緩衝區類都不能直接使用new關鍵字例項化,它們都是抽象類,但是它們都有一個用於建立相應例項的靜態工廠方法:static XxxBuffer allocate(int capacity);
1.6 緩衝區的兩個資料操作方法:put()和get()
get();獲取Buffer中的資料put(),放入資料到Buffer中。
1.7 flip()方法:將寫資料狀態切換為讀資料狀態
flip()的原始碼:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
具體操作程式碼如下:
// 1.建立一個容量為1024的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
// 2.往緩衝區裡寫資料
String str = "abcde";
buf.put(str.getBytes());
// 3.切換資料模式
buf.flip();
// 4.讀取資料
byte[] dst = new byte[buf.limit()];
buf.get(dst);
1.8 直接緩衝區與非直接緩衝區
2.通道:
2.1 基本概念:表示IO源與目標(例如:檔案、套接字)開啟的連線。但是通道(Channel)本身不能直接訪問資料,需要與緩衝區(Buffer)配合才能實現資料的讀取操作。
2.2 作用:如果把緩衝區理解為火車,那麼通道就是鐵路,即通道(Channel)負責傳輸,快取區(Buffer)負責儲存。
2.3 Channel的主要實現類:
實現類 | 概念 |
---|---|
FileChannel | 用於本地讀取、寫入、對映和操作檔案的通道 |
DatagramChannel | 通過UDP讀寫網路中的資料通道 |
SocketChannel | 通過TCP讀寫網路中的資料通道 |
ServerSocketChannel | 可以監聽新進來的TCP連線,對每一個新進來的連線都會建立一個SocketChannel |
2.4 支援通道的類:
用於本地IO操作:FileInputStream、FileOutputStream、RandomAccessFile(隨機檔案儲存流)
用於網路IO操作:DatagramSocket、Socket、ServerSocket
2.5 獲取通道的三種方法
方式一:getChannel()方法
FileInputStream fis = new FileInputStream("NIO.pdf");
FileOutputStream fos = new FileOutputStream("newNIO.pdf");
// 獲取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
方式二:通過Files類的靜態方法newByteChannel()獲取
FileChannel inChannel = null;
FileChannel outChannel = null;
// 獲取通道
inChannel = (FileChannel) Files.newByteChannel(Paths.get("NIO.pdf"), StandardOpenOption.READ);
outChannel = (FileChannel) Files.newByteChannel(Paths.get("newNIO.pdf"),
StandardOpenOption.READ, StandardOpenOption.WRITE,StandardOpenOption.CREATE);
方式三:通過通道的靜態方法:open()獲取
FileChannel inChannel = null;
FileChannel outChannel = null;
// 獲取通道
inChannel = FileChannel.open(Paths.get("NIO.pdf"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("newNIO.pdf"), StandardOpenOption.READ,
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
2.5 通道中的資料傳輸
(1) 將Buffer中的資料寫入Channel中:
int bytesWritten = inChannel.write(buf);
(2)從Channel讀取資料到Buffer中:
int bytesRead = inChannel.read(buf);
四、分散和聚集
分散讀取(Scatter)指從Channel中讀取的資料“分散”到多個Buffer中。按照緩衝區的順序,從Channel中讀取的資料依次將Buffer填滿。
聚集寫入(Gather)指將多個Buffer中的資料“聚集”到Channel中。按照緩衝區的順序,寫入position和limit之間的資料到Channel中去。
transferFrom和transferTo用法:
transferFrom用法:
RandomAccessFile fromFile = new RandomAccessFile("fromNIO.pdf", "rw");
// 獲取FileChannel
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toNIO.pdf", "rw");
FileChannel toChannel = toFile.getChannel();
// 定義傳輸位置
long position = oL;
// 最多傳輸的位元組數
long count = fromChannel.size();
// 將資料從源通道傳輸到另一個通道
toChannel.transferFrom(fromChannel, count, position);
transferTo()用法:和transferFrom相比,position和fromChannel這兩個引數的位置換了
RandomAccessFile fromFile = new RandomAccessFile("fromNIO.pdf", "rw");
// 獲取FileChannel
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toNIO.pdf", "rw");
FileChannel toChannel = toFile.getChannel();
// 定義傳輸位置
long position = oL;
// 最多傳輸的位元組數
long count = fromChannel.size();
// 將資料從源通道傳輸到另一個通道
toChannel.transferFrom(position, count, fromChannel);
五、阻塞與非阻塞
阻塞:
基本概念:傳統的IO流都是阻塞式的。也就是說,當一個執行緒呼叫read()或者write()方法時,該執行緒將被阻塞,直到有一些資料讀讀取或者被寫入,在此期間,該執行緒不能執行其他任何任務。在完成網路通訊進行IO操作時,由於執行緒會阻塞,所以伺服器端必須為每個客戶端都提供一個獨立的執行緒進行處理,當伺服器端需要處理大量的客戶端時,效能急劇下降。
非阻塞:
基本概念:Java NIO是非阻塞式的。當執行緒從某通道進行讀寫資料時,若沒有資料可用時,該執行緒會去執行其他任務。執行緒通常將非阻塞IO的空閒時間用於在其他通道上執行IO操作,所以單獨的執行緒可以管理多個輸入和輸出通道。因此NIO可以讓伺服器端使用一個或有限幾個執行緒來同時處理連線到伺服器端的所有客戶端。
下面給出一個例子:分別用阻塞和非阻塞的方式實現,內容中涉及到的選擇器(Selector)會在後面進行講解。
阻塞式:
// NIO的阻塞
public class TestNIOBlockDemo1 {
// 客戶端
@Test
public void client() throws IOException{
SocketChannel socketChannel = null;
FileChannel inChannel = null;
// 1 建立一個socket連線
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
// 2 獲取通道
inChannel = FileChannel.open(Paths.get("file/1.txt"), StandardOpenOption.READ);
// 3 分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
// 4 讀取本地檔案,併發送到服務端
while(inChannel.read(buf) != -1){ // 不等於-1,就說明讀到了東西
buf.flip(); // 將讀模式轉換為寫模式
socketChannel.write(buf);
buf.clear(); // 清空快取區
}
// 5 通知服務端,客戶端已經傳輸完畢
socketChannel.shutdownOutput();
// 6 接收服務端的反饋資訊
int len = 0;
while((len = socketChannel.read(buf)) != -1){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
//7 關閉通道
socketChannel.close();
inChannel.close();
}
// 服務端
@Test
public void server() throws IOException{
// 1 獲取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("file/2.txt"), StandardOpenOption.READ, StandardOpenOption.CREATE);
// 2 繫結連線
serverSocketChannel.bind(new InetSocketAddress(9898));
// 3 獲取客戶端的連線通道
SocketChannel socketChannel = serverSocketChannel.accept();
// 4 分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
// 5 接收客戶端的資料,並儲存到本地
while(socketChannel.read(buf) != -1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
// 6 接收完成,傳送反饋給客戶端
buf.put("服務端接收資料成功!".getBytes());
buf.flip();
socketChannel.write(buf);
// 7 關閉通道
socketChannel.close();
outChannel.close();
serverSocketChannel.close();
}
}
非阻塞式:
// 非阻塞NIO
public class TestNonNIOBlockDemo1 {
// 客戶端
@Test
public void client() throws IOException{
// 1.獲取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
// 2.切換非阻塞模式
socketChannel.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();
socketChannel.write(buf);
buf.clear();
}
// 5.關閉通道
socketChannel.close();
}
// 服務端
@Test
public void server() throws IOException{
// 1.獲取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.切換非阻塞式模式
serverSocketChannel.configureBlocking(false);
// 3.繫結連線
serverSocketChannel.bind(new InetSocketAddress(9898));
// 4.獲取選擇器
Selector selector = Selector.open();
// 5.將通道註冊到選擇器上,並指定“監聽接收事件”
serverSocketChannel.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 socketChannel = serverSocketChannel.accept();
// 11.切換非阻塞式模式
socketChannel.configureBlocking(false);
// 12.將該通道註冊到選擇器上
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if(sk.isReadable()){
// 13.獲取當前選擇器上“讀就緒”狀態的通道
SocketChannel socketChannel = (SocketChannel) sk.channel();
// 14.讀取資料
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = socketChannel.read(buf)) > 0){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
// 15.取消選擇鍵SelectionKey
it.remove();
}
}
}
}
ps:從上面的例子也可以看出來使用NIO完成網路通訊的三個核心為:
1、通道(Channel):負責連線
2、緩衝區(Buffer):負責資料的儲存
3、選擇器(Selector):監控狀態
六、選擇器(Selector)
參考白話之NIO,這篇文章詳細的介紹了選擇器(Selector),推薦閱讀!
1、基本概念
選擇器(Selector):是可選擇通道(SelectableChannel)物件的多路複用器,Selector可以同時監控多個SelectableChannel的IO狀況,提供了詢問通道是否已經準備好執行每個I/O操作的能力,即利用Selector可以使一個單獨的執行緒管理多個Channel,這樣會大量的減少執行緒之間上下文切換的開銷。
可選擇通道(SelectableChannel):SelectableChannel這個抽象類提供了實現通道的可選擇性所需要的公共方法。它是所有支援就緒檢查的通道類的父類。因為FileChannel類沒有繼承SelectableChannel因此是不是可選通道,而所有socket通道都是可選擇的,包括從管道(Pipe)物件的中獲得的通道。SelectableChannel可以被註冊到Selector物件上,同時可以指定對那個選擇器而言,那種操作是感興趣的。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言只能被註冊一次。
選擇器鍵(SelectionKey):表示SelectableChannel和Selector之間的註冊關係。每次像選擇器註冊通道時就會選擇一個事件(選擇鍵)。選擇鍵包含兩個數值的操作集,指示了該註冊關係所關心的通道操作,以及通道已經準備好的操作。
2、Selector的使用
1、建立選擇器(Selector)
Selector selector = Selector.open();
2、將Channel註冊到Selector
// Channel必須處於非阻塞模式下
channel.configureBlocking(false);
// 將Channel註冊到Selector中
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register引數說明:
第一個引數:selector代表註冊到哪個選擇器中;
第二個引數:是“interest集合”,表示選擇器所關心的通道操作,它實際上是一個表示選擇器在檢查通道就緒狀態時需要關心的操作的位元掩碼。比如一個選擇器對通道的read和write操作感興趣,那麼選擇器在檢查該通道時,只會檢查通道的read和write操作是否已經處在就緒狀態。它有以下四種可以監聽的事件型別:
事件型別 | 表示方法 |
---|---|
讀 | SelectionKey.OP_READ |
寫 | SelectionKey.OP_WRITE |
連線 | SelectionKey.OP_CONNECT |
接收 | SelectionKey.OP_ACCEPT |
若註冊時不止監聽一個事件,則可以使用“位或”操作符連線:
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE
當通道觸發了某個操作之後,表示該通道的某個操作已經就緒,可以被操作。因此:
某個SocketChannel成功連線到另一個伺服器稱為“連線就緒”(OP_CONNECT);
一個ServerSocketChannel準備好接收新進入的連線稱為“接收就緒”(OP_ACCEPT);
一個有資料可讀的通道可以說是“讀就緒”(OP_READ);
等待寫資料的通道可以說是“寫就緒”(OP_WRITE)。
3、取消選擇鍵SelectionKey remove();方法
七、管道(Pipe)
管道是2個執行緒之間的單向資料連線。Pipe有一個source通道和一個sink通道。資料會從source通道讀取,並且寫入到sink通道。
public class PipeTest {
@Test
public void test() throws IOException{
String str = "pcwl_java";
// 建立管道
Pipe pipe = Pipe.open();
// 向管道中寫入資料,需要訪問sink通道 ,SinkChannel是內部類
Pipe.SinkChannel sinkChannel = pipe.sink();
// 通過SinkChannel的write()方法寫資料
ByteBuffer buf1 = ByteBuffer.allocate(1024);
buf1.clear();
buf1.put(str.getBytes());
buf1.flip();
while(buf1.hasRemaining()){
sinkChannel.write(buf1);
}
// 從管道中讀取資料,需要訪問source通道,SourceChannel是內部類
Pipe.SourceChannel sourceChannel = pipe.source();
// 呼叫source通道的read()方法來讀取資料
ByteBuffer buf2 = ByteBuffer.allocate(1024);
sourceChannel.read(buf2);
}
}
參考及推薦:
1、CSDN上一個有關NIO的專欄:Java NIO學習筆記
2、開源中國上的一個有關NIO的專欄:
因為開源中國上沒有專門的專欄介面設定,所以將每篇文章連結列在下面,方便閱讀:
3、分享一個關於NIO的視訊講解:視訊下載連結