Java NIO真得就這麼簡單
大多數人肯定了解Java IO, 但是對於NIO一般是陌生的,但是Java NIO是一個高頻知識點,又不得不學,所以本文通過圖文+代碼的方式,保姆級別的講述Java NIO的各個知識點。覺得寫得好的,希望點個贊,給個收藏。
Java IO 與 Java NIO的區別
Java IO 與 Java NIO讀取文件的差別
普通Java IO
public static void ioReadFile(String fileName) throws Exception {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(fileName)));
int len = 0;
byte [] bytes = new byte[32];
while((len = bis.read(bytes)) != -1){
for(int i = 0; i < len; i++) {
System.out.print((char) bytes[i]);
}
}
bis.close();
}
Java NIO
public static void nioReadFile(String fileName) throws Exception{
RandomAccessFile read = new RandomAccessFile(fileName, "r");
FileChannel channel = read.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(32);
while(channel.read(byteBuffer) > 0) {
byteBuffer.flip();
while(byteBuffer.hasRemaining()){
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
read.close();
channel.close();
}
Java IO與Java NIO主要有三個區別
-
普通IO是面向流(
stream
)的處理,而NIO是面向緩衝區(buffer
)的處理-
面向流
Java IO面向流意味着每次從流中讀取一個或多個字節,直至讀取所有字節,字節數據沒有被緩存在任何地方,不能前後移動在流中讀取數據的位置。
-
面向緩衝區
Java NIO面向緩衝區,數據讀取到一個稍後處理的緩衝區,需要時可在緩衝區中前後移動。增加了處理過程中的靈活性。
-
-
普通IO是阻塞IO,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了。而NIO是非阻塞IO,Java 一個線程從某通道發送請求讀取數據,如果無可讀取數據,其不會被阻塞,在數據變得可讀取之前可以做其他事情
-
普通IO不支持selector,而NIO支持selector.(selector下文會詳細解釋).通過Selector對象,可以實現IO複用,下文會詳細講述。
NIO的三個核心部分
從上面NIO讀取文件的代碼,可以看出,NIO方式讀取文件,首先需要獲取一個傳輸數據的通道
channel
,之後構建一個承載數據的buffer
.打個形象點的比喻就是channel
相當於鐵路,而buffer
相當於貨運火車,通過鐵路,貨運火車可以源源不斷將貨物,從出發點運抵目的地。其實還有一個selector
,構成NIO的三個核心。
channel
channel的讀寫是雙向的,既可以從通道讀數據,又可以往通道寫數據,而流一般是單向的,比如輸入流,輸出流,同時channel也支持異步得讀寫
channel主要有一下四種實現
- FileChannel 從文件中讀取數據
- DatagramChannel 通過UDP讀取網絡中的數據
- SocketChannel 通過TCP讀取網絡中的數據
- ServerSockerChannel 監聽新進來的TCP連接,類似web服務
buffer
buffer其實本質是一塊可以寫入數據,也可以讀取數據的內存
其工作流程主要分爲一下幾個步驟
- 寫入數據到buffer
- 調用flip方法
- 從buffer中讀取數據
- 調用clear方法或者compact方法
buffer主要有三個屬性capacity
,position
,limit
,通過分析其三個屬性,可以詳細瞭解其工作原理
capacity
:buffer的大小,對於ByteBuffer,就是規定其能存儲的最大byte數
buffer分爲讀模式和寫模式,如下圖所示
position
:下一個要被讀或寫的元素的位置。
寫數據時,初始的position值爲0.當一個byte、long等數據寫到Buffer後,position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity-1.
讀數據時,position也是從0開始,向前讀取數據。寫模式向讀模式切換時,會將position重置爲0
limit
:緩衝區裏的數據總數。
在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。寫模式下,limit等於Buffer的capacity。當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據。
重要方法
flip方法:從寫模式切換到讀模式,position置爲0,limit設置爲之前寫模式的position值。
rewind方法:將position設置爲0,但limit不變
clear方法:position設置爲0,limit設置爲capacity的值
compact方法:將所有未讀的數據拷貝到Buffer起始處,然後將position設到最後一個未讀元素正後面。limit屬性依然像clear方法一樣設置成capacity.現在Buffer準備好了寫數據,但是不會覆蓋未讀數據。
mark、reset方法:通過調用Buffer.mark()方法,可以標記Buffer的一個特定position。之後可以通過調用Buffer.reset方法恢復到這個position
Buffer.mark();
//call Buffer.get() a couple of times, e.g. during parsing.
Buffer.reset(); //set position back to mark.
selector
Selector是channel的管理器,Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。這是在一個單線程中使用一個Selector處理3個Channel的圖示:
要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。
Java NIO IO複用
java NIO複用,主要是通過Selector對象實現的,通過Selector對象,註冊監聽多個通道的多個事件,事件主要有四種,如下所示
OP_ACCEPT
: 監聽連接事件,服務器監聽客戶端的連接請求
OP_CONNECT
:連接就緒事件,客戶端與服務器已經連接成功
OP_READ
:讀就緒事件,通道中已有可讀數據,可以指向讀操作
OP_WRITE
:寫就緒事件,可以向通道中寫數據
IO複用代碼實例
下面代碼主要實現客戶端向服務端傳送圖片數據的功能
client代碼
public class NoBlockClient {
public static void main(String[] args) throws IOException {
//創建一個SocketChannel對象,通過從TCP讀取數據,並綁定相應服務端ip和port
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
//設置爲非阻塞IO
socketChannel.configureBlocking(false);
//爲需要傳送的圖片文件建立channel
FileChannel fileChannel = FileChannel.open(Paths.get("C:\\Users\\LENOVO\\Desktop\\1.png"), StandardOpenOption.READ);
//創建運載數據所需的ByteBuffer對象
ByteBuffer buffer = ByteBuffer.allocate(1024);
//從FileChannel中讀取數據到ByteBuffer
//將ByteBuffer對象寫入SocketChannel
while (fileChannel.read(buffer) != -1) {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
//關閉相應通道
fileChannel.close();
socketChannel.close();
}
}
Server代碼
public class NoBlockServer {
public static void main(String[] args) throws IOException {
//創建一個ServerSocketChannel對象
ServerSocketChannel server = ServerSocketChannel.open();
//設置爲非阻塞IO
server.configureBlocking(false);
//綁定監聽端口
server.bind(new InetSocketAddress(6666));
//建立Selector對象,並註冊OP_ACCEPT事件
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
//selector.select()方法一直會阻塞,除非監聽的事件就緒
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//有新的連接
if (selectionKey.isAcceptable()) {
//爲連接創建一個SocketChannel
SocketChannel client = server.accept();
client.configureBlocking(false);
//監聽讀事件
client.register(selector, SelectionKey.OP_READ);
//可讀
} else if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
//創建一個FileChannel,接受網絡數據
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//從SocketChannel讀取數據,並通過ByteBuffer,向FileChannel對象傳輸
while (client.read(buffer) > 0) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
client.close();
}
//處理完了的就緒事件,一定要刪除,否則會反覆處理
iterator.remove();
}
}
}
}
參考文章
}
//處理完了的就緒事件,一定要刪除,否則會反覆處理
iterator.remove();
}
}
}
}
### 參考文章
[如何學習Java的NIO?](https://www.zhihu.com/question/29005375/answer/667616386)