萬字總結 NIO 多路複用技術,深入解析 NIO 的實現原理!

什麼是 NIO

NIO 是 Java 提供的一種基於 Channel 和 Buffer 的 IO 操作方式,即:利用內存映射文件方式處理輸入和輸出。NIO 具有更加強大和靈活的 IO 操作能力,提供了非阻塞 IO、多路複用等特性,特別適合需要處理大量連接的網絡編程場景

NIO 官方叫法爲 New I/O,但是由於後續加入了 AIO,導致 New IO 已經不能表達已有的 IO 模型,因此 NIO 也被業界稱爲 Non-blocking I/O,即非阻塞 IO

本文只對 Non-blocking IO 進行探討,AIO 不做過多贅述,後續會詳細介紹

使用場景

NIO(new IO) 相關包路徑

其中包下的常用類後續會詳細說明

NIO 的實現基礎

NIO 是基於 Linux IO 模型的 IO 多路複用模型實現的,netty、tomcat5 及以後的版本的實現都是基於 NIO,想要理解 Linux 的 IO 模型參考:

這裏的多路是指 N 個連接,每一個連接對應一個 channel,或者說多路就是多個 channel,是指多個連接複用了一個線程或者少量線程 (在 tomcat 中是少量線程)

NIO 的核心組件

JavaNIO 主要包含三個核心組件:

**Selector:**多路複用器 (選擇器),是 NIO 的基礎,也可以稱爲輪詢代理器、事件訂閱器或 Channel 容器管理器。Selector 提供選擇已經就緒的任務的能力,允許一個線程同時監聽多個通道上的事件,即:單線程同時管理多個網絡連接,並在某個通道上有數據可讀或可寫時及時做出響應,是 Java NIO 實現非阻塞 IO 的關鍵組件

**Channel:**是所有數據的傳輸通道,通道可以是文件、網絡連接等。Channel 提供了一個 map()方法,通過該 map()方法可以直接將 “一塊數據” 映射到內存中

**Buffer:**是一個容器 (類似數組),發送到 Channel 中的所有對象都必須首先放到 Buffer 中,從 Channel 中讀取的數據也會先放到 Buffer 中

爲什麼要將傳統 IO 模型中stream的概念換成channel+buffer的概念?

byte[] input = new byte[1024];
/* 讀取數據 */
socket.getInputStream().read(input);

而在 JavaNIO 中,緩衝區是一個固定大小的,連續內存塊,用於暫時存儲數據,爲緩衝區也提供了一系列的操作 API,因此在 NIO 特意強調了 Buffer 的概念

NIO 註冊、輪詢等待、讀寫操作協作關係如下圖:

Buffer

Buffer 是 Channel 操作讀寫的組件,包含了寫入和讀取得數據。在 NIO 庫中,所有數據都是用緩衝區處理的,緩衝區實際上就是一個數組,並提供了對數據結構化以及維護讀寫位置等信息。 Buffer 是一個抽象類,我們在網絡傳輸中大多數都是使用 ByteBuffer,它可以在底層字節數組上進行 get/set 操作。除了 ByteBuffer 之外,對應於其他基本數據類型 (boolean 除外) 都有相應的 Buffer 類(CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer)。

這些 Buffer 類沒有提供構造器訪問,因此創建 Buffer 類就必須使用靜態方法allocate(int capacity),即:ByteBuffer buffer = ByteBuffer.allocate(10)表示創建容量爲 10 的 ByteBuffer 對象

ByteBuffer 類的子類:MappedByteBuffer用於表示 Channel 將磁盤文件的部分或全部內容映射到內存中後得到的結果。MappedByteBuffer 對象是由 Channel 的 map() 方法返回

  1. 容量 (capacity): 緩衝區的容量,表示該 Buffer 的最大數據容量,創建後不可改變,不能爲負值

  2. 界限 (limit): 第一個不應該被讀出或寫入的緩衝區位置索引,位於 limit 後的數據既不可被讀也不可被寫

  3. 位置 (position): 用於指明下一個可以被讀出或寫入的緩衝區位置索引,索引從 0 開始,即如果從 Channel 中讀了兩個數據後 (0,1),position 指向的索引應該是 2(第三個即將讀取數據的位置)

position 可以自己設置,即設置從索引爲 mark 處讀取數據

/**
 * XxxBuffer方法:
 *      put():向Buffer中放入一些數據--一般不使用,都是從Channel中獲取數據
 *      get():向Buffer中取出數據
 *      flip():當Buffer裝入數據結束後,調用該方法可以設置limit爲當前位置,避免後續數據的添加--爲輸出數據做準備
 *      clear():對界限、位置進行初始化,limit置爲capacity,position設置爲0,爲下一次取數做準備
 *      int capacity():返回Buffer的容量大小
 *      boolean hasRemaining():判斷當前位置和界限之間是否還有元素可供處理
 *      int limit():返回Buffer的界限(limit)的位置
 *      Buffer mark():設置Buffer的標記(mark)位置,只能在0-position之間做標記
 *      int position():返回Buffer中的position值
 *      Buffer position(int newPs):設置Buffer的position,並返回position被修改後的Buffer對象
 *      int remaining():返回當前位置和界限之間的元素個數
 *      Buffer reset():將位置轉到mark所在的位置
 *      Buffer rewind():將位置設置爲0,取消設置的mark
 * @Param: 
 * @return: void
 */ 
public void BufferTest(){
    //創建一個CharBuffer緩衝區,設置容量爲20
    CharBuffer buff= CharBuffer.allocate(20);
    //測試方法:
    //獲取當前容量、界限、位置
    System.out.println("初始值:"+buff.capacity()+"\n"+
            buff.limit()+"\n"+
            buff.position());//20、20、0
    buff.put('1');
    buff.put('2');
    buff.put('3');
    buff.position(1).mark();//標記位置索引處
    buff.rewind();//將position設置爲0,並將mark清除,此時再調用reset()將會報錯java.nio.InvalidMarkException
    buff.mark().reset();//將position轉移到標記處
    buff.put("abc");
    buff.put("java");
    //abcjava
    //設置界限值
    buff.limit(buff.position());
    System.out.println("添加數據後:"+buff.capacity()+"\n"+
            buff.limit()+"\n"+
            buff.position());//20、7、7
    //初始化容量、界限、位置
    int position = buff.position();
    buff.position(0);
    System.out.println("修改後:"+buff.capacity()+"\n"+
            buff.limit()+"\n"+
            buff.position());//20、7、0
    //遍歷Buffer數組的數據
    for (int i = 0; i < position; i++) {
        System.out.print(buff.get());
    }
    System.out.println();
    //hasRemaining判斷是否可繼續添加元素,position >= limit返回false,position < limit返回true
    System.out.println(buff.remaining());//0
    System.out.println(buff.hasRemaining());//false
    buff.limit(15);
    System.out.println(buff.hasRemaining());//true

    System.out.println(buff.position());//7
    System.out.println("remaining="+buff.remaining());//8 還可以添加8個元素
    buff.clear();
    System.out.println("clear後:"+buff.capacity()+"\n"+
            buff.limit()+"\n"+
            buff.position());//20、20、0
}

Buffer 的缺點:

  1. XxxBuffer 使用allocate()方法創建的 Buffer 對象是普通的 Buffer-- 創建在 Heap 上的一塊連續區域 -- 間接緩衝區

  2. ByteBuffer 還有一個allocateDirect()方法創建的 Buffer 是直接 Buffer-- 創建在物理內存上開闢空間 -- 直接緩衝區

  3. 間接緩衝區:易於管理,垃圾回收器可回收,但是空間有限,讀寫文件速度較慢 (從磁盤讀到內存)

  4. 直接緩衝區:空間較大,讀寫速度快 (從磁盤讀到磁盤的速度),但是不受垃圾回收器的管理,創建和銷燬都極耗性能

  5. 直接 Buffer 的創建成本高於間接 Buffer,所以直接 Buffer 只用於生存期長的 Buffer。

  6. 直接 Buffer 只有 ByteBuffer 才能創建,因此如果需要其他的類型,只能使用創建好的 ByteBuffer 轉型爲其他類型

重要注意事項:

flip() 方法可以將 Buffer 從寫模式切換到讀模式,flip() 方法會將 position 設回到 0,並將 limit 設置成之前 position 的值

Channel

Channel 表示打開到 IO 設備的連接,可以直接將指定的文件的部分或全部直接映射爲 Buffer-- 映射,程序不能直接訪問 Channel 中的數據 (讀取、寫入都不行),必須通過 Buffer 進行承載後從 Buffer 中操作這些數據

Channel 有兩種實現:SelectableChannel 用於網絡讀寫;FileChannel 用於文件操作

其中 SelectableChannel 有以下幾種實現:

Channel 相比於 IO 中的 Stream 流更加高效,但是必須和 Buffer 一起使用。

Channel 的使用

  1. Channel 不能使用構造器來創建,只能通過字節流 InputStream,OutputStream(節點流) 來調用 getChannel() 方法來創建

  2. 不同的節點流調用 getChannel() 方法創建的 Channel 對象不一樣。

如:FileInputStream/FileOutputStream->FileChannel PipedInputStream/PipedOutputStream->Pip.SinkChannel/Pip.SourceChannel

  1. Channel 常用的三個方法:
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
    //1.創建文件對象--指定讀取和寫入的文件
    File src=new File("E:\\Documents\\java.txt");
    File dest=new File("E:\\Documents\\java1.txt");
    //2.使用FileInputStream進行文件讀取、FileOutputStream進行文件寫入
    //不一樣的是採用管道的方式--這裏就需要getChannel()創建Channel對象
    inChannel = new FileInputStream(src).getChannel();//只能讀
    outChannel = new FileOutputStream(dest).getChannel();//只能寫
    //3.將管道數據通過map()方法傳遞給MappedByteBuffer對象進行緩衝承載
    MappedByteBuffer buffer=inChannel.map(FileChannel.MapMode.READ_ONLY, 0, src.length());
    //4.將獲取的內容buffer交給Channel寫回到指定文件java1.txt中
    outChannel.write(buffer);
    //將文件內容打印到控制檯
    //1.初始化position和limit
    buffer.clear();
    //2.設置輸出編碼格式
    Charset charset=Charset.forName("UTF-8");
    //3.將ByteBuffer轉換成字符集的Buffer
    CharsetDecoder decoder=charset.newDecoder();
    CharBuffer cb=decoder.decode(buffer);
    //4.輸出字符集buffer
    System.out.println(cb);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        inChannel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        outChannel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

使用 RandomAccessFile 創建 Channel 對象

  1. 管道的寫數據的方式是追加 -- 但是重新執行程序就是覆蓋 -- 這種情況需要修改 position 的位置

  2. 源文件的隨機訪問對象創建的管道必須可讀,目標文件的隨機訪問對象創建的管道必須可寫

  3. 源文件的隨機訪問對象創建的管道使用 map() 方法生成 buffer 後,目標文件的隨機訪問對象創建的管道使用 write() 方法寫出 buffer

  4. 最後一定要關閉流

FileChannel channel = null;
FileChannel channel1 = null;
try {
    File srcPath=new File("E:\\Documents\\java.txt");
    File destPath=new File("E:\\Documents\\java1.txt");
    channel = new RandomAccessFile(srcPath,"r").getChannel();
    channel1 = new RandomAccessFile(destPath,"rw").getChannel();
    ByteBuffer map = channel.map(FileChannel.MapMode.READ_ONLY, 0, srcPath.length());
    channel1.position(destPath.length());
    channel1.write(map);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        channel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
File src=new File("E:/Documents/java.txt");
FileChannel channel = new FileInputStream(src).getChannel();
ByteBuffer bf=ByteBuffer.allocate(256);
int len=0;
while ((len=channel.read(bf))!=-1) {
    //limit設置爲position,避免操作空白區域。如果覆蓋到指定位置後,後續還有內容也不可讀取,這樣避免覆蓋不完全出現錯誤數據
    bf.flip();
    System.out.println(bf);
    Charset charset=Charset.forName("UTF-8");
    CharsetDecoder decoder = charset.newDecoder();
    CharBuffer cb = decoder.decode(bf);
    System.out.println(cb);
    //重置buffer的參數,內容依舊是採用覆蓋的方式,clear不會修改Buffer中的內容
    bf.clear();
}

其他組件

**Charset 類:**用於將 Unicode 字符映射成爲字節序列以及逆映射操作

字符集和 Charset

由於計算機的文件、數據、圖片文件底層都是二進制存儲的 (全部都是字節碼)

編碼:將明文的字符序列轉換成計算機理解的二進制序列稱爲編碼

解碼:將二進制序列轉換成明文字符串稱爲解碼

java 默認使用 Unicode 字符集,當從操作系統中讀取數據到 java 程序容易出現亂碼

當 A 程序使用 A 字符集進行數據編碼 (二進制) 存儲到硬盤,B 程序採用 B 字符集進行數據解碼,解碼的二進制數據轉換後的字符與 A 字符集轉換後的字符不一致就出現了亂碼的情況。

JDK1.4 提供了 Charset 來處理字節序列和字符序列之間的轉換關係

常用方法

forName(String charsetName): 創建 Charset 對應字符集的對象實例

newDecoder(): 通過 Charset 對象獲取對應的解碼器

newEncoder(): 通過 Charset 對象獲取對應的編碼器

CharBuffer encode(ByteBuffer bb): 將 ByteBuffer 中的字節序列轉換爲字符序列

ByteBuffer decode(CharBuffer cb): 將 CharBuffer 中的字符序列轉換爲字節序列

ByteBuffer encode(String str): 將 String 中的字符序列轉換爲字節序列

//      SortedMap<String, Charset> stringCharsetSortedMap = Charset.availableCharsets();
//      stringCharsetSortedMap.forEach((key,value)-> System.out.println(key+"<->"+value));
        Charset charset = Charset.forName("UTF-8");
        ByteBuffer bb = charset.encode("中文字符");
        System.out.println(bb);//java.nio.HeapByteBuffer[pos=0 lim=12 cap=19]
        //編碼解碼方式一:
        CharBuffer decode1 = charset.decode(bb);
        System.out.println(decode1);//中文字符
        ByteBuffer encode1 = charset.encode(decode1);
        System.out.println(encode1);//java.nio.HeapByteBuffer[pos=0 lim=12 cap=19]
        //編碼解碼方式二:
        CharsetDecoder decoder = charset.newDecoder();
        CharsetEncoder encoder = charset.newEncoder();
        CharBuffer decode = decoder.decode(encode1);
        System.out.println(decode);//中文字符
        ByteBuffer encode = encoder.encode(decode);
        System.out.println(encode);//java.nio.HeapByteBuffer[pos=0 lim=12 cap=19]
    }

文件鎖

NIO 中 java 提供了 FileLock 來支持文件鎖定功能,在 FileChannel 中提供的 lock()/tryLock() 方法可以獲取文件鎖 FileLock 對象

直接使用 lock() 或 tryLock() 方法獲取的文件鎖是排他鎖,即 shared 默認值爲 false

FileLock fileLock = null;
try {
    FileOutputStream fileOutputStream = new FileOutputStream("E:/Documents/java.txt");
    FileChannel channel = fileOutputStream.getChannel();
    fileLock = channel.tryLock();//創建鎖以後,其他程序將無法對該文件進行修改
    Thread.sleep(1000);
} catch (IOException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    try {
        fileLock.release();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

雖然文件鎖可以用於控制併發訪問,但是還是推薦使用數據庫來保存程序信息,而不是文件

注意:

  1. 對於部分平臺,文件鎖即使可以被獲取,文件依舊是可以被其他線程操作的

  2. 對於部分平臺,不支持同步地鎖定一個文件並把它映射到內存中

  3. 文件鎖是由 java 虛擬機持有的,如果兩個 java 程序使用同一個 java 虛擬機,則不能對同一個文件進行加鎖操作

  4. 對於部分平臺,關閉 FileChannel 時,會釋放 java 虛擬機在該文件上的所有鎖,因此應該避免對同一個被鎖定的文件打開多個 FileChannel

NIO 工具類

NIO 問題:

  1. File 類功能有限

  2. File 類不能利用特定文件系統的特性

  3. File 類的方法性能不高

  4. File 類大多數方法出錯時不會提供異常信息

升級 NIO.2:

public class Nio2Test {
    @Test
    public void pathsTest(){
        Path path = Paths.get("E:/Documents/java.txt");
        //path包含的路徑數量
        System.out.println(path.getNameCount());//2=>(Document,java.txt)
        //獲取根目錄
        System.out.println(path.getRoot());//E:\
        //獲取絕對路徑
        System.out.println(path.toAbsolutePath());//E:\Documents\java.txt
        Path path1 = Paths.get("E:""Documents""java.txt");
        System.out.println(path1);//E:\Documents\java.txt
    }
    @Test
    public void File() throws IOException {
        //複製文件
        Files.copy(Paths.get("E:","Documents","java1.txt"), new FileOutputStream("E:/Documents/java2.txt"));
        //檢查文件是否爲隱藏文件
        System.out.println("Nio2Test.java:"+Files.isHidden(Paths.get("out.txt")));//false
        List<String> list = Files.readAllLines(Paths.get("E:/Documents/java2.txt"), Charset.forName("UTF-8"));
        System.out.println(list);
        //獲取文件大小
        long size = Files.size(Paths.get("E:/Documents/java2.txt"));
        System.out.println(size);
        //寫數據到文件中
        ArrayList<String> poem = new ArrayList<>();
        poem.add("今天搞完IO沒得問題吧");
        poem.add("明天搞完網絡編程第一章沒得問題吧");
        poem.add("後天搞完網絡編程第二章搞完IO沒得問題吧");
        poem.add("大後天搞完網絡編程第三章搞完IO沒得問題吧");
        Path write = Files.write(Paths.get("E:/Documents/java2.txt"), poem, Charset.forName("UTF-8"));//覆蓋
        System.out.println(write);
        //按行獲取文件內容,使用Stream接口中的forEache方法遍歷
        Files.lines(Paths.get("E:/Documents/java2.txt"),Charset.forName("UTF-8")).forEach(ele-> System.out.println(ele));
        //獲取目錄下文件,使用Stream接口中的forEache方法遍歷
        Files.list(Paths.get("E:/Documents")).forEach(ele-> System.out.println(ele));
        //獲取當前文件的根目錄別名
        FileStore fileStore = Files.getFileStore(Paths.get("E:/Documents/java2.txt"));
        System.out.println(fileStore);
        //E盤總空間
        long totalSpace = fileStore.getTotalSpace();
        System.out.println(totalSpace);
        //E盤可用空間
        long unallocatedSpace = fileStore.getUnallocatedSpace();
        System.out.println(unallocatedSpace);
    }
}

使用 Files 的 FileVisitor 遍歷文件和目錄

不使用 Files,通常想要遍歷指定目錄下的所有文件和子目錄都是使用遞歸的方式,這種方式不僅複雜,靈活性也不高

在 Files 類中提供了兩個方法來遍歷文件和子目錄

walkFileTree(Path start,FileVisitor<? super Path> visitor):遍歷 start 路徑下的所有文件和子目錄

walkFileTree(Path start,Setoptions,int maxDepth,FileVisitor<? super Path> visitor):遍歷 start 路徑下的所有文件和子目錄,但是可根據 maxDepth 控制遍歷深度

兩個方法都使用了 FileVisitor 作爲入參,FileVisitor 代表一個文件訪問器,walkFileTree() 方法會自動遍歷 start 路徑下的所有文件和子目錄,遍歷文件和子目錄都會觸發 FileVisitor 中相應的方法

FileVisitor 中定義的四個方法:

FileVisitResult postVisitDirectory(T dir,IOException exc):訪問子目錄之後觸發該方法

FileVisitResult preVisitDirectory(T dir,BasicFileAttributes attrs):訪問子目錄之前觸發該方法

FileVisitResult visitFile(T file,BasicFileAttributes attrs):訪問 file 文件時觸發該方法

FileVisitResult visitFileFailed(T file,IOException exec):訪問 file 文件失敗時觸發該方法

FileVisitResult 是一個枚舉類,代表訪問之後的後續行爲:

CONTINUE:代表繼續訪問

SKIP_SIBLINGS:代表繼續訪問,但不訪問該文件或目錄的兄弟文件或目錄

SKIP_SUBTREE:代表繼續訪問,但不訪問該文件或目錄的子目錄樹

TERMINATE:代表中止訪問

如果想要實現自己的文件訪問器,可以通過繼承 SimpleFileVisitor 來實現,SimpleFileVisitor 是 FileVisitor 的實現類,這樣就可以根據需要、選擇性的重寫指定方法了

public class FileVisitorTest{
  public static void main(String[] args) throws Exception{
    Files.walkFileTree(Paths.get("G:","publish","codes","15"),new SimpleFileVisitor<Path>(){
      @Override
      public FileVisitResult visitFile(Path file,BasicFileAttributes attrs) throws IOException{
        System.out.println("正在訪問"+file+"文件");
        //找到了FileVisitorTest.java文件
        if(file.endsWith("FileVisitorTest.java")){
          System.out.println("--已經找到目標文件--");
          return FileVisitResult.TERMINATE;
        }
        return FileVisitResult.CONTINUE;
      }
      @Override
      public FileVisitResult preVisitDirectory(Path dir,BasicFileAttributes attrs) throws IOException{
        System.out.println("正在訪問"+dir+"路徑");
        return FileVisitResult.CONTINUE;
      }
    })
  }
}

使用 WatchService 監控文件變化

不使用 WatchService 的情況下,想要監控文件的變化,則需要考慮啓動一條後臺線程,這條後臺線程每隔一段實踐去遍歷一次指定目錄的文件,如果發現此次遍歷結果與上次遍歷結果不同,則認爲文件發生了變化,這種方式不僅十分繁瑣,而且性能也不好

Path 類提供了一個方法用於監聽文件系統的變化

register(WatchService watcher,WatchEvent.Kind<?>... events):用 watcher 監聽該 path 代表的目錄下的文件變化。events 參數指定要監聽哪些類型的事件

這個方法最核心的就是 WatchService,它代表一個文件系統監聽服務,它負責監聽 path 代表的目錄下的文件變化,一旦使用 register() 方法完成註冊之後,接下來就可以調用 WatchService 的如下三個方法來獲取監聽目錄的文件變化事件

WatchKey poll():獲取下一個 WatchKey,如果沒有 WatchKey 發生就立即返回 null;

WatchKey poll(long timeout,TimeUnit unit):嘗試等待 timeout 實踐去獲取下一個 WatchKey;

WatchKey take():獲取下一個 WatchKey,如果沒有 WatchKey 發生就一直等待

public class WatchServiceTest{
  public static void main(String[) args) throws Exception{
    //獲取文件系統atchService對象
    WatchService watchService = FileSystems.getDefault() 
          .newWatchService(); 
    //爲C:盤根路徑註冊監昕
    Paths.get("C:/").register(watchService 
            , StandardWatchEventKinds.ENTRY_CREATE 
            , StandardWatchEventKinds.ENTRY_MODIFY 
            , StandardWatchEventKinds.ENTRY_DELETE) ; 
    while(true){
      //獲取下一個文件變化事件
      WatchKey key = watchService.take(); //①
      for (WatchEvent<?> event : key.pollEvents()){
        System.out.println(event.context()"文件發生了" + event.kind()"事件!" ) ; 
      }
      //重設 WatchKey
      boolean valid = key.reset(); 
      // 如果重設失敗 退出監聽
      if (!valid){
        break;
      }
    }
  }
}

代碼說明:

在①處試圖獲取下一個 WatchKey,如果沒有發生就等待,因此 C 盤路徑下的每次文件的變化都會被該程序監聽到

訪問文件屬性

在未使用 NIO 工具類的情況下,以前的 File 類可以訪問一些簡單的文件屬性,比如文件大小、修改時間、文件是否隱藏、是文件還是目錄等。如果程序需要獲取或修改更多的文件屬性,必須利用運行所在的平臺的特定代碼來實現。

NIO.2 在 java.nio.file.attribute 包下提供了大量的工具類,通過這些工具類,開發者可以非常簡單的讀取、修改文件屬性。這些工具類主要分爲兩類:

FileAttributeView 是其他 XxxAttributeView 的父接口,以下是一些常用的 FileAttributeView 的實現類

AclFileAttributeView:通過 AclFileAttributeView,可以爲特定文件設置 ACL(Access Control List) 及文件所有者屬性。其中 getAcl() 方法返回 List 對象,代表了該文件的權限集合;通過 setAcl(List) 方法可以修改該文件的 ACL

BasicFileAttributeView:它可以獲取或修改文件的基本屬性,包括文件的最後修改時間、最後訪問時間、創建時間、大小、是否爲目錄、是否爲符號鏈接等。其中 readAttributes() 方法返回一個 BasicFileAttributes 對象,對文件夾基本屬性的修改是通過 BasicFileAttributes 對象來完成的

DosFileAttributeView:它主要用於獲取或修改文件的 DOS 相關屬性,比如文件是否只讀、是否隱藏、是否爲系統文件、是否爲存檔文件等。其中 readAttributes() 方法返回一個 DosFileAttributes 對象,對這些屬性的修改是通過 DosFileAttributes 對象來完成的

FileOwnerAttributeView:它主要用於獲取或修改文件的所有者。其中 getOwner() 方法返回一個 UserPrincipal 對象來代表文件所有者,也可以調用 setOwner(UserPrincipal owner) 方法來改變文件的所有者

PosixFileAttributeView:它主要用於獲取或修改 POSIX(Portable Operating System Interface of INIX) 屬性,其中 readAttributes() 方法返回一個 PosixFileAttributes 對象,該對象可用於獲取或修改文件的所有者、組所有者、訪問權限信息 (可以看作是 UNIX 中 chmod 所作的事情)。注意:這個 View 只在 Unix、Linux 等系統上有用

UserDefinedFileAttributeAttributeView:它可以讓開發者爲文件設置一些自定義屬性

NIO 多路複用器實現客戶端服務端通信

package NIO;

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @author caihuaxin
 * @version 1.0.0
 * @doc 多路複用器nio服務
 * @date 2024/03/04
 */
public class MultiplexerNioServer implements Runnable{
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    private volatile boolean stop = false;

    /**
     * @param port 端口
     * @return
     * @doc 初始化多路複用器,綁定監聽端口
     */
    public MultiplexerNioServer(int port) {
        try {
            serverSocketChannel = ServerSocketChannel.open();//獲得一個serverChannel
            selector = Selector.open();//創建選擇器,獲取一個多路複用器
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);//設置爲非阻塞模式
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop(){
        this.stop = true; //優雅停止
    }

    @Override
    public void run() {
        while (!stop) {
            try{
                int client = selector.select(1000);
                System.out.println("1:"+client);
                if(client == 0){
                    continue;
                }
                System.out.println("2:" + client);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                SelectionKey key = null;
                while(iterator.hasNext()){
                    key = iterator.next();
                    iterator.remove();
                    try{
                        handle(key);
                    }catch (Exception e){
                        if(key != null){
                            key.cancel();
                            if(key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {

            }
        }
        if(selector != null){
            try {
                selector.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    public void handle(SelectionKey key) throws IOException{
        if(key.isValid()){
            if(key.isAcceptable()){
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                sc.register(selector, SelectionKey.OP_READ);
            }
            if(key.isReadable()){
                SocketChannel socketChannel = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);
                if (readBytes > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("input is:" + body);
                    res(socketChannel,body);
                }else if(readBytes < 0){
                    key.channel();
                    socketChannel.close();
                }
                //沒有讀到字節忽略
            }
        }
    }

    private void res(SocketChannel channel,String response) throws IOException {
        if(response != null && !response.isEmpty()){
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
            System.out.println("res end");
        }
    }
}
package NIO;

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.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

public class NioClientHandler implements Runnable{
    private String host;
    private int port;

    private Selector selector;

    private SocketChannel socketChannel;

    private volatile  boolean stop;

    public NioClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
    @Override
    public void run() {
        try{
            doConnect();
        }catch (Exception e){
            e.printStackTrace();
        }
        while (!stop){
            try {
                int select = selector.select(1000);
                if (select == 0){
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                SelectionKey key = null;
                while (iterator.hasNext()){
                    key = iterator.next();
                    iterator.remove();
                    try{
                        handKey(key);
                    }catch (Exception e){
                        if(key != null){
                            key.cancel();
                            if(key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
        if(selector != null){
            try{
                selector.close();
            }catch (IOException e){

            }
        }
    }

    private void handKey(SelectionKey key) throws IOException {
        if (key.isValid()){
            SocketChannel sc = (SocketChannel)key.channel();
            if(key.isConnectable()){
                if(sc.finishConnect()){
                    sc.register(selector,SelectionKey.OP_READ);
                    doWrite(sc);
                }else{
                    System.exit(1);
                }
            }
            if(key.isReadable()){
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if(readBytes > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    System.out.println(new String(bytes, StandardCharsets.UTF_8));
                    this.stop = true;
                } else if (readBytes<0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    private void doConnect() throws IOException {
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            socketChannel.register(selector,SelectionKey.OP_READ);
            doWrite(socketChannel);
        }else{
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }

    
    private void doWrite(SocketChannel socketChannel) throws IOException {
        byte[] bytes = "0123456789".getBytes();
        ByteBuffer allocate = ByteBuffer.allocate(bytes.length);
        allocate.put(bytes);
        allocate.flip();
        socketChannel.write(allocate);
        if (allocate.hasRemaining()){
            System.out.println("write success");
        }
    }
}
package NIO;

public class NioServer {
    public static void main(String[] args) {
        int port = 8080;
        MultiplexerNioServer nioServer = new MultiplexerNioServer(port);
        new Thread(nioServer).start();
    }
}
package NIO;

public class NioClient {
    public static void main(String[] args) {
        new Thread(new NioClientHandler("localhost",8080),"nioClient-001").start();
    }
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/suUaVzlgOuWw74Yw1piCRA