Java的NIO

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 服務器,通常有以下幾種模式:

  1. 一個客戶端連接用一個線程:
    優點:程序編寫簡單;
    缺點:當連接非常多時,分配的線程也會非常多,服務器可能會因爲資源耗盡而崩潰。
  2. 每一個客戶端連接交給一個擁有固定數量線程的連接池:
    優點:程序編寫相對簡單,可以處理大量的連接;
    缺點:線程的開銷非常大,連接如果非常多,排隊現象會比較嚴重。
  3. 使用 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檢測客戶端有動作時,服務端接受數據。檢測到客戶端沒有動作時,服務端開啓的線程仍然可以進行其他操作!

有錯誤的地方敬請指出!覺得寫得可以的話麻煩給個贊!歡迎大家評論區或者私信交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章