① IO是面向流的,NIO是面向緩衝區的
② IO是阻塞的,NIO是非阻塞的
③ IO無Selector,NIO需要Selector
-
NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。在JavaAPI中提供了兩套NIO,一套是針對標準輸入輸出NIO,另一套就是網絡編程NIO。
-
1、面向流與面向緩衝
Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。 Java NIO的緩衝導向方法略有不同。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏尚未處理的數據。
-
2、阻塞與非阻塞IO
Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了。Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。
-
3、選擇器(Selectors)
Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
1、API調用
當然,使用NIO的API調用時看起來與使用IO時有所不同,但這並不意外,因爲並不是僅從一個InputStream逐字節讀取,而是數據必須先讀入緩衝區再處理
IO讀取數據代碼示例:
private static void readLine() {
String path = "/Volumes/code/java/test/springdemo/src/main/resources/banner.txt";
InputStream inputStream = null;
try {
inputStream = new BufferedInputStream(new FileInputStream(path));
InputStreamReader reader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ( (line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
請注意處理狀態由程序執行多久決定。換句話說,一旦reader.readLine()方法返回,你就知道肯定文本行就已讀完, readline()阻塞直到整行讀完,這就是原因。
而NIO的實現有所不同:
private static void nioRead() {
String path = "/Volumes/code/java/test/springdemo/src/main/resources/test.txt";
RandomAccessFile accessFile = null;
try {
accessFile = new RandomAccessFile(path, "rw");
// 獲取channel
FileChannel channel = accessFile.getChannel();
// 創建buffer並且分配空間大小爲1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 從管道中讀取數據寫入到Buffer中
int bytesRead = channel.read(byteBuffer);
while (bytesRead != -1) {
// 將position設爲0,limit 指向下一個不可以操作的元素爲止
byteBuffer.flip();
// 如果緩衝區還有內容
while (byteBuffer.hasRemaining()) {
System.out.println((char)byteBuffer.get());
}
// 將buffer 中魏都區的數據拷貝到Buffer的起始位置,供下次讀取
byteBuffer.compact();
bytesRead = channel.read(byteBuffer);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
if (accessFile != null) {
accessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意channel.read()這一行,從通道讀取字節到ByteBuffer。當這個方法調用返回時,你不知道你所需的所有數據是否在緩衝區內。你所知道的是,該緩衝區包含一些字節,這使得處理有點困難。假設第一次 read(buffer)調用後,讀入緩衝區的數據只有半行,你能處理數據嗎?顯然不能,需要等待,直到整行數據讀入緩存,在此之前,對數據的任何處理毫無意義。所以,你怎麼知道是否該緩衝區包含足夠的數據可以處理呢?好了,你不知道。發現的方法只能查看緩衝區中的數據。其結果是,在你知道所有數據都在緩衝區裏之前,你必須檢查幾次緩衝區的數據。這不僅效率低下,而且可以使程序設計方案雜亂不堪。
總結:
- NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
- 如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,實現NIO的服務器可能是一個優勢。同樣,如果你需要維持許多打開的連接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接,可能是一個優勢。一個線程多個連接的設計方案如下圖所示:
- SelectionKey.OP_ACCEPT —— 接收連接繼續事件,表示服務器監聽到了客戶連接,服務器可以接收這個連接了
- SelectionKey.OP_CONNECT —— 連接就緒事件,表示客戶與服務器的連接已經建立成功
- SelectionKey.OP_READ —— 讀就緒事件,表示通道中已經有了可讀的數據,可以執行讀操作了(通道目前有數據,可以進行讀操作了)
- SelectionKey.OP_WRITE —— 寫就緒事件,表示已經可以向通道寫數據了(通道目前可以用於寫操作)
/**
* fshows.com
* Copyright (C) 2013-2020 All Rights Reserved.
*/
package com.example.springdemo.test.io;
/**
* @author xuleyan
* @version NIOServer.java, v 0.1 2020-04-06 11:03 AM xuleyan
*/
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.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
/*標識數字*/
private int flag = 0;
/*緩衝區大小*/
private int BLOCK = 4096;
/*接受數據緩衝區*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*發送數據緩衝區*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
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:");
}
// 監聽
private void listen() throws IOException, InterruptedException {
while (true) {
// 選擇一組鍵,並且相應的通道已經打開
selector.select();
// 返回此選擇器的已選擇鍵集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
handleKey(selectionKey);
}
}
}
// 處理請求
private void handleKey(SelectionKey selectionKey) throws IOException, InterruptedException {
// 接受請求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count=0;
// 測試此鍵的通道是否已準備好接受新的套接字連接。
if (selectionKey.isAcceptable()) {
// 返回爲之創建此鍵的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的連接。
// 此方法返回的套接字通道(如果有)將處於阻塞模式。
client = server.accept();
// 配置爲非阻塞
client.configureBlocking(false);
// 註冊到selector,等待連接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 返回爲之創建此鍵的通道。
client = (SocketChannel) selectionKey.channel();
//將緩衝區清空以備下次讀取
receivebuffer.clear();
//讀取服務器發送來的數據到緩衝區中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服務器端接受客戶端數據--:"+receiveText);
Thread.sleep(2000);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//將緩衝區清空以備下次寫入
sendbuffer.clear();
// 返回爲之創建此鍵的通道。
client = (SocketChannel) selectionKey.channel();
sendText="message from server--" + flag++;
//向緩衝區中輸入數據
sendbuffer.put(sendText.getBytes());
//將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
sendbuffer.flip();
//輸出到通道
client.write(sendbuffer);
System.out.println("服務器端向客戶端發送數據--:"+sendText);
Thread.sleep(2000);
client.register(selector, SelectionKey.OP_READ);
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException, InterruptedException {
// TODO Auto-generated method stub
int port = 8888;
NIOServer server = new NIOServer(port);
server.listen();
}
}
/**
* fshows.com
* Copyright (C) 2013-2020 All Rights Reserved.
*/
package com.example.springdemo.test.io;
/**
* @author xuleyan
* @version ClientServer.java, v 0.1 2020-04-06 11:04 AM xuleyan
*/
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.Iterator;
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, InterruptedException {
// TODO Auto-generated method stub
// 打開socket通道
SocketChannel socketChannel = SocketChannel.open();
// 設置爲非阻塞方式
socketChannel.configureBlocking(false);
// 打開選擇器
Selector selector = Selector.open();
// 註冊連接服務端socket動作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 連接
socketChannel.connect(SERVER_ADDRESS);
// 分配緩衝區大小內存
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。
//此方法執行處於阻塞模式的選擇操作。
selector.select();
//返回此選擇器的已選擇鍵集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判斷此通道上是否正在進行連接操作。
// 完成套接字通道的連接過程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成連接!");
Thread.sleep(2000);
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//將緩衝區清空以備下次讀取
receivebuffer.clear();
//讀取服務器發送來的數據到緩衝區中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客戶端接受服務器端數據--:"+receiveText);
Thread.sleep(2000);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
//將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客戶端向服務器端發送數據--:"+sendText);
Thread.sleep(2000);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
參考:https://www.cnblogs.com/xiaoxi/p/6576588.html
參考:https://blog.csdn.net/qq_24365213/article/details/77159713