文章目錄
1、NIO介紹
NIO 全稱non-blocking IO,是指JDK 提供的關於IO流的新 API。從 JDK1.4 開始,Java 提供了 一系列改進的輸入/輸出的新特性,被統稱爲 NIO(即 New IO)。新增了許多用於處理輸入輸出的類,這些類都被放在 java.nio 包及子包下,並且對原 java.io 包中的很多類進行改寫,新增了滿足 NIO 的功能。
2、阻塞與非阻塞
阻塞: 傳統的IO流是阻塞的,當一個線程調用讀寫方法時,該線程就會被阻塞,即不能進行其它操作,直到讀寫結束。如果使用傳統IO進行網絡通信,由於線程會阻塞,而且只能處理服務端與一個客戶端的通信,會造成需要大量的線程,這樣會給服務器很大的壓力。
非阻塞: NIO是非阻塞的,當線程從某通道進行讀寫數據時,若沒有數據可以用。該線程便會轉向其他任務,這樣極大地節省了時間和提高效率。同時,一個線程可以處理多個IO連接通道,也能夠減緩服務端的壓力。
3、核心組件
3.1、Channel
Channel(通道)是NIO讀取數據的通道,是對BIO中輸入流和輸出流的強化。它提供了一個map()方法,可以將一塊數據映射到內存中,所以說NIO是面向塊的。此外,Channel的傳輸是雙向的,可以同時進行讀和寫操作。
3.2、Buffer
Buffer(緩衝區)實際上是一個特殊的數組,因爲它內置了三個屬性,所以可以跟蹤和記錄緩衝區的狀態變化,進而實現更加複雜的操作。其中,最重要的就是容量(capacity)、界限(limit)、位置(position)。
容量:緩衝區能夠容納的數據元素的最大數量,這一個容量在緩衝區被初始化時確定;
界限:緩衝區的第一個不能被讀或寫的元素;
位置:下一個要被讀或寫的元素的索引,位置會自動由相應的 get( )和 put( )函數更新;
標記:可直接將position定位到mark。
3.3、Selector
Selector(選擇器)能夠檢測多個註冊的通道上是否有事件發生。如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,纔會調用函數來進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程,並 且避免了多線程之間的上下文切換導致的開銷。
簡單來說,就是一個線程擁有一個Selector,而一個Selector管理多個讀寫IO,這樣即使一個IO沒有進行讀寫操作,也不會造成線程阻塞,導致性能降低。
4、操作
4.1、寫操作
package nio;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author RuiMing Lin
* @date 2020-03-14 14:52
*/
public class Demo1 {
public static void main(String[] args) throws Exception{
// 1.創建輸出流
FileOutputStream fos = new FileOutputStream("fos.txt");
// 2.獲取通道
FileChannel fileChannel = fos.getChannel();
// 3.提供一個緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.往緩衝區存入數據
String string = "hello,nio!";
buffer.put(string.getBytes());
// 5.翻轉
buffer.flip();
// 6.把緩衝區寫入通道
fileChannel.write(buffer);
// 7.關閉
fos.close();
}
}
4.2、讀操作
package nio;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author RuiMing Lin
* @date 2020-03-14 15:29
*/
public class Demo2 {
public static void main(String[] args) throws Exception{
//1.獲取文件,創建輸入流
File file = new File("fis.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 2.獲取通道
FileChannel fileChannel = fileInputStream.getChannel();
// 3.提供一個緩衝區
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
// 4.讀取通道的數據並保存在緩衝區中
fileChannel.read(buffer);
// 5.獲取緩衝區數據
System.out.println(new String(buffer.array()));
// 6.關流
fileInputStream.close();
}
}
4.3、複製操作
package nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author RuiMing Lin
* @date 2020-03-14 15:29
*/
public class Demo2 {
public static void main(String[] args) throws Exception{
// 1.獲取文件
FileInputStream fis=new FileInputStream("fis.txt");
FileOutputStream fos=new FileOutputStream("fos.txt");
// 2.獲取通道
FileChannel sourceCh = fis.getChannel();
FileChannel destCh = fos.getChannel();
// 3.複製
destCh.transferFrom(sourceCh, 0, sourceCh.size());
// 4.關流
sourceCh.close();
destCh.close();
}
}
5、使用NIO實現網絡傳輸
5.1、網絡通信
NIO最大用處就是用於網絡IO,因爲網絡IO會產生高併發高訪問的情況。上面演示代碼中進行文件IO時用到的 FileChannel並不支持非阻塞操作。NIO 中的網絡通道是非阻塞 IO 的實現,基於事件驅動,非常適用於服務器需要維持大量連接,但是數據交換量不大的情況,例如一些即時通信的服務等等。
在Java中編寫 Socket 服務器,通常有以下幾種模式:
- 一個客戶端連接用一個線程:
優點:程序編寫簡單;
缺點:當連接非常多時,分配的線程也會非常多,服務器可能會因爲資源耗盡而崩潰。 - 每一個客戶端連接交給一個擁有固定數量線程的連接池:
優點:程序編寫相對簡單,可以處理大量的連接;
缺點:線程的開銷非常大,連接如果非常多,排隊現象會比較嚴重。 - 使用 Java 的NIO
優點:用非阻塞的 IO 方式處理。這種模式可以用一個線程,通過selector處理大量的客戶端連接。
實現網絡非阻塞通信的小案例
定義一個客戶端類:
package nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author RuiMing Lin
* @date 2020-03-14 19:49
*/
public class NIOClient {
public static void main(String[] args) throws Exception{
// 1.得到網絡通道
SocketChannel channel = SocketChannel.open();
// 2.設置非阻塞
channel.configureBlocking(false);
// 3.提供服務器的IP+端口號
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
// 4.連接服務器
if (!channel.connect(address)) { //如果連接不上
while (!channel.finishConnect()) { //繼續連,此時並不阻塞線程
System.out.println("client:連接服務器端的同時,我還可以做別的事!");
}
}
// 5.得到一個緩衝區並存入數據
String string = "hello,服務端";
ByteBuffer buffer = ByteBuffer.wrap(string.getBytes());
// 6.發送數據
channel.write(buffer);
System.in.read();
}
}
定義一個服務端類:
package nio;
import java.net.InetSocketAddress;
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;
/**
* @author RuiMing Lin
* @date 2020-03-14 20:01
*/
public class NIOServer {
public static void main(String[] args) throws Exception{
// 1.得到ServerSocketChannel對象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.得到Selector對象
Selector selector = Selector.open();
// 3.綁定端口號
serverSocketChannel.bind(new InetSocketAddress(9999));
// 4.設置非阻塞
serverSocketChannel.configureBlocking(false);
// 5.註冊selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
//6.1 監控客戶端
if(selector.select(2000)==0){//nio 非阻塞式的優勢
System.out.println("Server:沒有客戶端搭理我,我就乾點別的事");
continue;
}
//6.2 得到 SelectionKey,判斷通道里的事件
Iterator<SelectionKey> keyIterator=selector.selectedKeys().iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
//客戶端連接請求事件
System.out.println("OP_ACCEPT");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {
//讀取客戶端數據事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("客戶端發來數據:" + new String(buffer.array()));
}
// 6.3 手動從集合中移除當前 key,防止重複處理
keyIterator.remove();
}
}
}
}
先啓動服務端類:此時沒有客戶端連接服務端,但是並沒有造成線程阻塞!
再啓動客戶端類:
當selector檢測客戶端有動作時,服務端接受數據。檢測到客戶端沒有動作時,服務端開啓的線程仍然可以進行其他操作!
有錯誤的地方敬請指出!覺得寫得可以的話麻煩給個贊!歡迎大家評論區或者私信交流!