萬字總結 NIO 多路複用技術,深入解析 NIO 的實現原理!
什麼是 NIO
NIO 是 Java 提供的一種基於 Channel 和 Buffer 的 IO 操作方式,即:利用內存映射文件方式處理輸入和輸出。NIO 具有更加強大和靈活的 IO 操作能力,提供了非阻塞 IO、多路複用等特性,特別適合需要處理大量連接的網絡編程場景
-
在 JDK1.4 時提出了 NIO(New I/O),在 BIO 模型 (Blocking IO) 的基礎上,增加了 NIO 模型(Non-Blocking IO),即同步非阻塞方式
-
JDK7 時在 NIO 包中增加了 AIO,NIO 也隨之被稱爲 NIO2.0(即 NIO+AIO),NIO 是同步非阻塞的,AIO 是異步非阻塞的。
NIO 官方叫法爲 New I/O,但是由於後續加入了 AIO,導致 New IO 已經不能表達已有的 IO 模型,因此 NIO 也被業界稱爲 Non-blocking I/O,即非阻塞 IO
本文只對 Non-blocking IO 進行探討,AIO 不做過多贅述,後續會詳細介紹
使用場景
-
對於低負載、低併發的應用程序,可以使用同步阻塞 IO 來提升開發效率和維護性
-
但是對於高負載、高併發的網絡應用,應該使用 NIO 的非阻塞模式來開發
NIO(new IO) 相關包路徑
其中包下的常用類後續會詳細說明
-
java.nio:主要包含各種與 Buffer 相關的類
-
java.nio.channels:主要包含與 Channel 和 Selector 相關的類
套接字的特別說明 (因爲也在這個包下)
在該包路徑下,NIO 提供了與傳統 IO 模型中
Socket
和ServerSocket
相對應的SocketChannel
和ServerSocketChannel
兩種不同的套接字通道實現新增的這兩種通道都支持阻塞和非阻塞方式
-
java.nio.charset:主要包含與字符集相關的類
-
java.nio.file:主要包含文件處理的工具類
-
java.nio.channels.spi:主要包含與 Channel 相關的服務提供者編程接口
-
java.nio.charset.spi:主要包含與字符集相關的服務提供者編程接口
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
的概念?
-
Stream 與 Channel 對比
從這個角度看,Channel 是全雙工通信,Stream 是單工通信,那麼 Channel 必然就會比 Stream 更加高效
-
傳統的阻塞 IO 模型中,
stream
是用於在程序和數據源之間進行數據傳輸的抽象概念,流的特點就是順序的、逐個訪問。 -
Java NIO 中提出的 Channel 也是進行數據傳輸的抽象概念,區別在於 stream 是單向數據傳輸,而 channel 是雙向數據傳輸,
-
爲什麼傳統 IO 沒有提出 Buffer 的概念?
緩衝區以及緩衝區是如何工作的,這是所有 IO 實現的基礎,即輸入和輸出就是把數據移進 or 移出緩衝區
進程執行 IO 操作,就是向操作系統發出請求,將數據從緩衝區取出 (寫操作),或者將數據寫入緩衝區 (讀操作)
既然 Buffer 是所有 IO 實現的基礎,傳統 IO 模型並沒有 Buffer,是不是說錯了或者傳統 IO 並不是 IO?
其實並不是,只是傳統 IO 中 Buffer 是開發者自己創建的,也就是 byte[] 數組,這個 byte 數組設置多大都是開發者自己決定,因此沒有提出 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() 方法返回
-
容量 (capacity): 緩衝區的容量,表示該 Buffer 的最大數據容量,創建後不可改變,不能爲負值
-
界限 (limit): 第一個不應該被讀出或寫入的緩衝區位置索引,位於 limit 後的數據既不可被讀也不可被寫
-
位置 (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 的缺點:
-
XxxBuffer 使用
allocate()
方法創建的 Buffer 對象是普通的 Buffer-- 創建在 Heap 上的一塊連續區域 -- 間接緩衝區 -
ByteBuffer 還有一個
allocateDirect()
方法創建的 Buffer 是直接 Buffer-- 創建在物理內存上開闢空間 -- 直接緩衝區 -
間接緩衝區:易於管理,垃圾回收器可回收,但是空間有限,讀寫文件速度較慢 (從磁盤讀到內存)
-
直接緩衝區:空間較大,讀寫速度快 (從磁盤讀到磁盤的速度),但是不受垃圾回收器的管理,創建和銷燬都極耗性能
-
直接 Buffer 的創建成本高於間接 Buffer,所以直接 Buffer 只用於生存期長的 Buffer。
-
直接 Buffer 只有 ByteBuffer 才能創建,因此如果需要其他的類型,只能使用創建好的 ByteBuffer 轉型爲其他類型
重要注意事項:
flip() 方法可以將 Buffer 從寫模式切換到讀模式,flip() 方法會將 position 設回到 0,並將 limit 設置成之前 position 的值
Channel
Channel 表示打開到 IO 設備的連接,可以直接將指定的文件的部分或全部直接映射爲 Buffer-- 映射,程序不能直接訪問 Channel 中的數據 (讀取、寫入都不行),必須通過 Buffer 進行承載後從 Buffer 中操作這些數據
Channel 有兩種實現:SelectableChannel 用於網絡讀寫;FileChannel 用於文件操作
其中 SelectableChannel 有以下幾種實現:
-
ServerSocketChannel:應用服務器程序的監聽通道。只有通過了這個通道,應用程序才能向操作系統註冊支持多路複用 IO 的端口監聽。同時支持 UDP 協議和 TCP 協議
-
SocketChannel:TCP Socket 套接字的監聽通道,一個 Socket 套接字對應了一個客戶端 IP: 端口 → 服務端 IP: 端口的通信連接
-
DatagramChannel:UDP 數據報文的監聽通道
Channel 相比於 IO 中的 Stream 流更加高效,但是必須和 Buffer 一起使用。
Channel 的使用
-
Channel 不能使用構造器來創建,只能通過字節流 InputStream,OutputStream(節點流) 來調用 getChannel() 方法來創建
-
不同的節點流調用 getChannel() 方法創建的 Channel 對象不一樣。
如:FileInputStream/FileOutputStream->FileChannel PipedInputStream/PipedOutputStream->Pip.SinkChannel/Pip.SourceChannel
- Channel 常用的三個方法:
-
MappedByteBuffer map(FileChannel.MapMode mode,long position,long size)
“
將 Channel 對應的部分或全部的數據映射成 ByteBuffer
參數說明:
mode:映射模式 - 三種:READ_ONLY(只讀)、PRIVATE(私有(寫時複製))、READ_WRITE(讀寫)
position:Buffer 的初始化位置
size:Buffer 的容量
-
read(): 用於讀取 Buffer 中的數據
-
write(): 用於將數據寫入 Buffer
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 對象
-
管道的寫數據的方式是追加 -- 但是重新執行程序就是覆蓋 -- 這種情況需要修改 position 的位置
-
源文件的隨機訪問對象創建的管道必須可讀,目標文件的隨機訪問對象創建的管道必須可寫
-
源文件的隨機訪問對象創建的管道使用 map() 方法生成 buffer 後,目標文件的隨機訪問對象創建的管道使用 write() 方法寫出 buffer
-
最後一定要關閉流
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 來處理字節序列和字符序列之間的轉換關係
-
該類包含了用於創建編碼器和解碼器的方法
-
獲取 Charset 所支持的字符集的方法 availableCharsets()
常用方法
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(long position,long size,boolean shared):如果未獲取文件鎖,則會導致線程阻塞
-
tryLock(long position,long size,boolean shared):如果未獲取文件鎖直接返回 null, 獲取返回該文件鎖
-
position:從文件的 position 位置開始
-
size:給長度爲 size 的內容加鎖
-
shared:true 表示爲共享鎖:允許多個進程來讀取該文件,但是其他進程獲得該文件的排他鎖;false 表示該鎖爲排他鎖,自己讀取時其他線程不能獲取鎖
-
上述兩個方法參數解析:
直接使用 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();
}
}
雖然文件鎖可以用於控制併發訪問,但是還是推薦使用數據庫來保存程序信息,而不是文件
注意:
-
對於部分平臺,文件鎖即使可以被獲取,文件依舊是可以被其他線程操作的
-
對於部分平臺,不支持同步地鎖定一個文件並把它映射到內存中
-
文件鎖是由 java 虛擬機持有的,如果兩個 java 程序使用同一個 java 虛擬機,則不能對同一個文件進行加鎖操作
-
對於部分平臺,關閉 FileChannel 時,會釋放 java 虛擬機在該文件上的所有鎖,因此應該避免對同一個被鎖定的文件打開多個 FileChannel
NIO 工具類
NIO 問題:
-
File 類功能有限
-
File 類不能利用特定文件系統的特性
-
File 類的方法性能不高
-
File 類大多數方法出錯時不會提供異常信息
升級 NIO.2:
-
提供了 Path 接口和 Paths 實現工具類
-
提供了 Files 工具類
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 包下提供了大量的工具類,通過這些工具類,開發者可以非常簡單的讀取、修改文件屬性。這些工具類主要分爲兩類:
-
XxxAttributeView:代表某種文件屬性的視圖
-
XxxAttributes:代表某種文件屬性的集合,一般通過 XxxAttributeView 獲取 XxxAttributes
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