一、IO模型
IO在計算機中指Input/Output,也就是輸⼊和輸出。
(一)內核空間與用戶空間
在計算機中,將空間分為內核空間(Kernel-space)和⽤戶空間(User-space)。 在 Linux 系統中,內核模塊運⾏在內核空間,對應的進程處於內核態;⽽⽤戶程序運⾏在⽤戶空間,對應的進程處於⽤戶態。
內核空間:內核空間總是駐留在內存中,它是為操作系統的內核保留的。應⽤程序是不允許直接在該區域進⾏讀寫或直接調⽤內核代碼定義的函數的。
⽤戶空間:每個普通的⽤戶進程都有⼀個單獨的⽤戶空間,處於⽤戶態的進程不能訪問內核空間中的數據,也不能直接調⽤內核函數的 ,因此要進⾏系統調⽤的時候,就要將進程切換到內核態才⾏。
在系統調用進行讀寫時,系統(應用程序)不負責數據在內核緩沖區和磁盤之間的交換。底層的讀寫交換,是由操作系統kernel內核完成的。

(二)IO模型類型
I/O 模型簡單的理解:就是⽤什么樣的通道進⾏數據的發送和接收,很⼤程度上決定了程序通信的性能。
IO模型⼤致有如下⼏種:
同步IO(synchronous IO)
異步IO(asynchronous IO)
阻塞IO(bloking IO)
⾮阻塞IO(non-blocking IO)
多路復⽤IO(multiplexing IO)
信號驅動式IO(signal-driven IO) //在實際中並不常⽤
1、同步與異步IO
⾸先來解釋同步和異步的概念,這兩個概念與消息的通知機制有關。也就是同步與異步主要是從消息通知機制⻆度來說的。
同步(synchronous IO): 同步就是發起⼀個調⽤后,被調⽤者未處理完請求之前,調⽤不返回。
異步(asynchronous IO): 異步就是發起⼀個調⽤后,⽴刻得到被調⽤者的回應表示已接收到請求,但是被調⽤者並沒有返回結果,此時我們可以處理其他的請求,被調⽤者通常依靠事件,回調等機制來通知調⽤者其返回結果。
所謂同步就是⼀個任務的完成需要依賴另外⼀個任務時,只有等待被依賴的任務完成后,依賴的任務才能算完成,這是⼀種可靠的任務序列。要么成功都成功,失敗都失敗,兩個任務的狀態可以保持⼀致。
所謂異步是不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什么⼯作,依 賴的任務也⽴即執⾏,只要⾃⼰完成了整個任務就算完成了。⾄於被依賴的任務最終是否真正完成,依賴它的任務⽆法確定,所以它是不可靠的任務序列。
同步和異步的區別最⼤在於異步調⽤者不需要等待處理結果,被調⽤者會通過回調等機制來通知調⽤者其返回結果。
2、阻塞與非阻塞IO
阻塞(bloking IO): 阻塞就是發起⼀個請求,調⽤者⼀直等待請求結果返回,也就是當前線程會被掛起,⽆法從事其他任務,只有當條件就緒才能繼續。
⾮阻塞(non-blocking IO): ⾮阻塞就是發起⼀個請求,調⽤者不⽤⼀直等着結果返回,可以先去⼲其他事情。
⾮阻塞和阻塞的概念相對應,指在不能⽴刻得到結果之前,該函數不會阻塞當前線程,⽽會⽴刻返回。雖然表⾯上看⾮阻塞的⽅式可以明顯的提⾼CPU的利⽤率,但是也帶了另外⼀種后果就是系統的線程切換增加。增加的CPU執⾏時間能不能補償系統的切換成本需要好好評估。
這里需要特殊注意一下,有⼈也許會把阻塞調⽤和同步調⽤等同起來,實際上它們是不同的:
(1)如果這個線程在等待當前函數返回時,仍在執⾏其他消息處理,那這種情況就叫做同步⾮阻塞;
(2)如果這個線程在等待當前函數返回時,沒有執⾏其他消息處理,⽽是處於掛起等待狀態,那這種情況就叫做同步阻塞;
所以同步的實現⽅式會有兩種:同步阻塞、同步⾮阻塞;同理,異步也會有兩種實現:異步阻塞、異步⾮阻塞;同步/異步關注的是消息通知的機制,⽽阻塞/⾮阻塞關注的是程序(線程)等待消息通知時的狀態
3、IO多路復用
IO多路復⽤(multiplexing)是⼀種同步IO模型,實現⼀個線程可以監視多個⽂件句柄;⼀旦某個⽂件句柄就緒,就能夠通知應⽤程序進⾏相應的讀寫操作;沒有⽂件句柄就緒時會阻塞應⽤程序,交出cpu。多路是指⽹絡連接,復⽤指的是同⼀個線程服務器端采⽤單線程通過select/epoll等系統調⽤獲取fd列表,遍歷有事件的fd進⾏accept/recv/send,使其能⽀持更多的並發連接請求。
fds = [listen_fd] // 偽代碼描述 while(1) { // 通過內核獲取有讀寫事件發⽣的fd,只要有⼀個則返回,⽆則阻塞 // 整個過程只在調⽤select、poll、epoll這些調⽤的時候才會阻塞,accept/recv是不會阻塞 for (fd in select(fds)) { if (fd == listen_fd) { client_fd = accept(listen_fd) fds.append(client_fd) } elseif (len = recv(fd) && len != -1) { // logic } } }
不同的操作系統中IO多路復⽤的⽅案:
Linux: select、poll、epoll
MacOS/FreeBSD: kqueue
Windows/Solaris: IOCP
select/poll/epoll之間的區別
|
select
|
pol | epoll | |
|
數據結構
|
bitmap | 數組 | 紅黑樹 |
| 最大連接數 | 1024 | 無上限 | 無上限 |
|
fd拷⻉
|
每次調⽤select拷⻉
|
每次調⽤poll拷⻉
|
fd⾸次調⽤epoll_ctl拷⻉,每次調⽤epoll_wait不拷⻉
|
|
⼯作效率
|
輪詢:O(n) | 輪詢:O(n) | 回調:O(1) |
(三)Java的IO模型
java共⽀持3種⽹絡編程模型I/O模式:BIO、NIO、AIO。
Java BIO:同步並阻塞(傳統阻塞型),服務器實現模式為⼀個連接⼀個線程,即客戶端有連接請求時服務器端就需要啟動⼀個線程進⾏處理,如果這個連接不做任何事情會造成不必要的線程開銷。
Java NIO:同步⾮阻塞,服務器實現模式為⼀個線程處理多個請求(連接),即客戶端發送的連接請求都會注冊到多路復⽤器上,多路復⽤器輪詢到連接有 I/O 請求就進⾏處理。
Java AIO(NIO.2):異步⾮阻塞, AIO 引⼊異步通道的概念,采⽤了 Proactor 模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,⼀般適⽤於連接數較多且連接時間較⻓的應⽤。
BIO、NIO、AIO 使⽤場景分析:
1. BIO ⽅式適⽤於連接數⽬⽐較⼩且固定的架構,這種⽅式對服務器資源要求⽐較⾼,並發局限於應⽤中,JDK1.4 以前的唯⼀選擇,但程序簡單易理解。
2. NIO ⽅式適⽤於連接數⽬多且連接⽐較短(輕操作)的架構,⽐如聊天服務器,彈幕系統,服務器通訊等。編程⽐較復雜,JDK1.4 開始⽀持。
3. AIO ⽅式使⽤於連接數⽬多且連接⽐較⻓(重操作)的架構,⽐如相冊服務器,充分調⽤ OS 參與並發操作,編程⽐較復雜,JDK7 開始⽀持。
二、BIO編程
(一)BIO說明
Java BIO 就是傳統的 Java I/O 編程,其相關的類和接⼝在 java.io。其是同步阻塞的,服務器實現模式為⼀個連接⼀個線程,即客戶端有連接請求時服務器端就需要啟動⼀個線程進⾏處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善(實現多個客戶連接服務器)。BIO ⽅式適⽤於連接數⽬⽐較⼩且固定的架構,這種⽅式對服務器資源要求⽐較⾼,並發局限於應⽤中,JDK1.4 以前的唯⼀選擇,程序簡單易理解。

采⽤ BIO 通信模型的服務端,通常由⼀個獨⽴的 Acceptor 線程負責監聽客戶端的連接。我們⼀般通過在 while(true) 循環中服務端會調⽤ accept() ⽅法等待接收客戶端的連接的⽅式監聽請求,請求⼀旦接收到⼀個連接請求,就可以建⽴通信套接字在這個通信套接字上進⾏讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執⾏完成。
但是,BIO有個致命的問題,在 Java 虛擬機中,線程是寶貴的資源,線程的創建和銷毀成本很⾼,除此之外,線程的切換成本也是很⾼的。尤其在 Linux 這樣的操作系統中,線程本質上就是⼀個進程,創建和銷毀線程都是重量級的系統函數。如果並發訪問量增加會導致線程數急劇膨脹可能會導致線程堆棧溢出、創建新線程失敗等問題,最終導致進程宕機或者僵死,不能對外提供服務。
偽異步IO:為了解決上面提到的BIO的致命問題,后來就有了采⽤線程池和任務隊列可以實現⼀種叫做偽異步的 I/O 通信框架。當有新的客戶端接⼊時,將客戶端的 Socket 封裝成⼀個Task(該任務實現java.lang.Runnable接⼝)投遞到后端的線程池中進⾏處理,JDK 的線程池維護⼀個消息隊列和 N 個活躍線程,對消息隊列中的任務進⾏處理。由於線程池可以設置消息隊列的⼤⼩和最⼤線程數,因此,它的資源占⽤是可控的,⽆論多少個客戶端並發訪問,都不會導致資源的耗盡和宕機。
偽異步I/O通信框架采⽤了線程池實現,因此避免了為每個請求都創建⼀個獨⽴線程造成的線程資源耗盡問題。不過因為它的底層任然是同步阻塞的BIO模型,因此⽆法從根本上解決問題。
(二)BIO編碼
使⽤ BIO 模型編寫⼀個服務器端,監聽 6666 端⼝,當有客戶端連接時,就啟動⼀個線程與之通訊。同時要求使⽤線程池機制改善,可以連接多個客戶端。服務器端可以接收客戶端發送的數據( telnet ⽅式即可)。
public class BIOServer { public static void main(String[] args) throws Exception { //線程池機制 //思路 //1. 創建一個線程池 //2. 如果有客戶端連接,就創建一個線程,與之通訊(單獨寫一個方法) ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //創建ServerSocket ServerSocket serverSocket = new ServerSocket(6666); System.out.println("服務器啟動了"); while (true) { System.out.println("線程信息id = " + Thread.currentThread().getId() + ",名字 = " + Thread.currentThread().getName()); //監聽,等待客戶端連接 System.out.println("等待連接...."); final Socket socket = serverSocket.accept(); System.out.println("連接到一個客戶端"); //就創建一個線程,與之通訊(單獨寫一個方法) newCachedThreadPool.execute(new Runnable() { public void run() {//我們重寫 //可以和客戶端通訊 handler(socket); } }); } } //編寫一個handler方法,和客戶端通訊 public static void handler(Socket socket) { try { byte[] bytes = new byte[1024]; //通過socket獲取輸入流 InputStream inputStream = socket.getInputStream(); //循環的讀取客戶端發送的數據 while (true) { System.out.println("線程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName()); System.out.println("read...."); int read = inputStream.read(bytes); if (read != -1) { System.out.println("客戶端說:"+new String(bytes, 0, read));//輸出客戶端發送的數據 } else { break; } } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("關閉和client的連接"); try { socket.close(); } catch (Exception e) { e.printStackTrace(); } } } }
啟動程序,輸出結果:
服務器啟動了 線程信息id = 1,名字 = main 等待連接....
使用telnet訪問服務后輸出結果:
服務器啟動了 線程信息id = 1,名字 = main 等待連接.... 連接到一個客戶端 線程信息id = 1,名字 = main 等待連接.... 線程信息id = 20名字 = pool-1-thread-1 read....
(三)BIO存在的問題
BIO存在如下問題:
1、每個請求都需要創建獨⽴的線程,與對應的客戶端進⾏數據 Read ,業務處理,數據 Write 。
2、當並發數較⼤時,需要創建⼤量線程來處理連接,系統資源占⽤較⼤。
3、連接建⽴后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。
在活動連接數不是特別⾼(⼩於單機1000)的情況下,這種模型是⽐較不錯的,可以讓每⼀個連接專注於⾃⼰的 I/O 並且編程模型簡單,也不⽤過多考慮系統的過載、限流等問題。線程池本身就是⼀個天然的漏⽃,可以緩沖⼀些系統處理不了的連接或請求。但是,當⾯對⼗萬甚⾄百萬級連接的時候,傳統的 BIO 模型是⽆能為⼒的。因此,我們需要⼀種更⾼效的 I/O 處理模型來應對更⾼的並發量。
三、NIO編程
(一)NIO簡介及與BIO對比
1、NIO簡介
NIO是⼀種同步⾮阻塞的I/O模型,在Java 1.4 中引⼊了NIO框架,對應 java.nio 包,提供了Channel , Selector,Buffer等抽象。NIO中的N可以理解為Non-blocking,不單純是New。它⽀持⾯向緩沖的,基於通道的I/O操作⽅法。 NIO提供了與傳統BIO模型中的 Socket 和 ServerSocket 相對應的 SocketChannel 和ServerSocketChannel 兩種不同的套接字通道實現,兩種通道都⽀持阻塞和⾮阻塞兩種模式。阻塞模式使⽤就像傳統中的⽀持⼀樣,⽐較簡單,但是性能和可靠性都不好;⾮阻塞模式正好與之相反。對於低負載、低並發的應⽤程序,可以使⽤同步阻塞I/O來提升開發速率和更好的維護性;對於⾼負載、⾼並發的(⽹絡)應⽤,應使⽤ NIO 的⾮阻塞模式來開發。

NIO 有三⼤核⼼部分: Channel (通道)、 Buffer (緩沖區)、 Selector (選擇器)。
NIO 是⾯向緩沖區,或者⾯向塊編程的。數據讀取到⼀個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性,使⽤它可以提供⾮阻塞式的⾼伸縮性⽹絡。NIO 是可以做到⽤⼀個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以分配 50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,⾮得分配 10000 個。HTTP 2.0 使⽤了多路復⽤的技術,做到同⼀個連接並發處理多個請求,⽽且並發請求的數量⽐HTTP 1.1 ⼤了好⼏個數量級。
2、NIO與BIO對比
(1)BIO是面向流,而NIO是面向緩沖區的,或者說BIO 以流的⽅式處理數據,⽽ NIO 以塊的⽅式處理數據,塊 I/O 的效率⽐流 I/O ⾼很多。
(2)BIO 是阻塞的, NIO 則是⾮阻塞的。
(3)BIO 基於字節流和字符流進⾏操作,⽽ NIO 基於 Channel (通道)和 Buffer (緩沖區)進⾏操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫⼊到通道中。 Selector (選擇器)⽤於監聽多個通道的事件(⽐如:連接請求,數據到達等),因此使⽤單個線程就可以監聽多個客戶端通道。
(二)NIO核心原理

1、每個 Channel 都會對應⼀個 Buffer 。
2、Selector 對應⼀個線程,⼀個線程對應多個 Channel (連接)。
3、該圖反應了有三個 Channel 注冊到該 Selector //程序
4、程序切換到哪個 Channel 是由事件決定的, Event 就是⼀個重要的概念。
5、Selector 會根據不同的事件,在各個通道上切換。
6、Buffer 就是⼀個內存塊,底層是有⼀個數組。
7、數據的讀取寫⼊是通過 Buffer ,這個和 BIO , BIO 中要么是輸⼊流,或者是輸出流,不能雙向,但是 NIO 的 Buffer 是可以讀也可以寫,需要 flip ⽅法切換 Channel 是雙向的,可以返回底層操作系統的情況,⽐如 Linux ,底層的操作系統通道就是雙向的。
(三)緩沖區(Buffer)
在NIO厙中,所有數據都是⽤緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的; 在寫⼊數據時,寫⼊到緩沖區中。任何時候訪問NIO中的數據,都是通過緩沖區進⾏操作。最常⽤的緩沖區是 ByteBuffer,⼀個 ByteBuffer 提供了⼀組功能⽤於操作 byte 數組。除了ByteBuffer,還有其他的⼀些緩沖區,事實上,每⼀種Java基本類型(除了Boolean類型)都對應有⼀種緩沖區。
在 NIO 中, Buffer 是⼀個頂層⽗類,它是⼀個抽象類,類的層級關系圖:

Buffer 類定義了所有的緩沖區都具有的四個屬性來提供關於其所包含的數據元素的信息:capacity(容量)、position(游標位置)、limit (末尾限定符),其中,position 和 limit 的意義依賴於當前 Buffer 是處於讀模式還是寫模式。capacity 的含義⽆論讀寫模式都是相同的。
Capacity (容量):
作為⼀個內存塊,Buffer 有⼀個固定的⼤⼩,我們叫做 “capacity(容量)"。你最多只能向 Buffer 寫⼊ capacity ⼤⼩的字節,⻓整數,字符等。⼀旦 Buffer 滿了,你必須在繼續寫⼊數據之前清空它(讀出數據,或清除數據)。
Position (游標位置):
當你開始向 Buffer 寫⼊數據時,你必須知道數據將要寫⼊的位置。position 的初始值為 0。當⼀個字節或⻓整數等類似數據類型被寫⼊ Buffer 后,position 就會指向下⼀個將要寫⼊數據的位置(根據數據類型⼤⼩計算)。position 的最⼤值是 capacity - 1。
當你需要從 Buffer 讀出數據時,你也需要知道將要從什么位置開始讀數據。在你調⽤ flip ⽅法將Buffer 從寫模式轉換為讀模式時,position 被重新設置為 0。然后你從 position 指向的位置開始讀取數據,接下來 position 指向下⼀個你要讀取的位置。
Limit(末尾限定符):
在寫模式下對⼀個Buffer的限制即你能將多少數據寫⼊Buffer中。在寫模式下,限制等同於Buffer的容量(capacity)。
當切換Buffer為讀模式時,限制表示你最多能讀取到多少數據。因此,當切換Buffer為讀模式時,限制會被設置為寫模式下的position值。換句話說,你能讀到之前寫⼊的所有數據(限制被設置為已寫的字節數,在寫模式下就是position)。
mark(標記):你可以調⽤ Buffer.mark() ⽅法在 Buffer 中標記給定位置。之后,你可以調⽤Buffer.reset() ⽅法重置回標記的這個位置。
Buffer 類相關⽅法⼀覽:

最常⽤的⾃然是 ByteBuffer 類(⼆進制數據),該類的主要⽅法如下:

使用一個案例說明buffer
public class BasicBuffer { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(10); intBuffer.put(10); intBuffer.put(101); System.err.println("Write mode: "); System.err.println("\tCapacity: " + intBuffer.capacity()); System.err.println("\tPosition: " + intBuffer.position()); System.err.println("\tLimit: " + intBuffer.limit()); intBuffer.flip(); System.err.println("Read mode: "); System.err.println("\tCapacity: " + intBuffer.capacity()); System.err.println("\tPosition: " + intBuffer.position()); System.err.println("\tLimit: " + intBuffer.limit()); } }
上面代碼中 ,首先寫入兩個 int 值, 此時 capacity = 10, position = 2, limit = 10,然后調用 flip 轉換為讀模式, 此時 capacity = 10, position = 0, limit = 2;
輸出結果:
Write mode: Capacity: 10 Position: 2 Limit: 10 Read mode: Capacity: 10 Position: 0 Limit: 2
(四)通道(Channel)
1、簡介
通常來說NIO中的所有IO都是從 Channel(通道) 開始的,類似於BIO中的Stream,但是BIO 中的 Stream 是單向的,例如 FileInputStream 對象只能進⾏讀取數據的操作,⽽ NIO中的通道( Channel )是雙向的,可以讀操作,也可以寫操作。
Channel 在 NIO 中是⼀個接⼝ public interface Channel extends Closeable{};常⽤的 Channel 類有FileChannel ⽤於⽂件的數據讀寫、DatagramChannel ⽤於 UDP 的數據讀寫、ServerSocketChannel 和 SocketChannel ⽤於 TCP 的數據讀寫(ServerSocketChannel 類似
以FileChannel為例,FileChannel 主要⽤來對本地⽂件進⾏ IO 操作,常⻅的⽅法有:
public int read(ByteBuffer dst) ,從通道讀取數據並放到緩沖區中
public int write(ByteBuffer src) ,把緩沖區的數據寫到通道中
public long transferFrom(ReadableByteChannel src, long position, longcount) ,從⽬標通道中復制數據到當前通道
public long transferTo(long position, long count, WritableByteChanneltarget) ,把數據從當前通道復制給⽬標通道
2、代碼實例:
(1)本地文件寫數據
public class NIOFileChannel01 { public static void main(String[] args) throws Exception { String str = "hello,every one,every body"; //創建一個輸出流 -> channel FileOutputStream fileOutputStream = new FileOutputStream("/Users/conglongli/Desktop/file01.txt"); //通過 fileOutputStream 獲取對應的 FileChannel //這個 fileChannel 真實類型是 FileChannelImpl FileChannel fileChannel = fileOutputStream.getChannel(); //創建一個緩沖區 ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //將 str 放入 byteBuffer byteBuffer.put(str.getBytes()); //對 byteBuffer 進行 flip byteBuffer.flip(); //將 byteBuffer 數據寫入到 fileChannel fileChannel.write(byteBuffer); fileOutputStream.close(); } }
(2)本地文件讀取
public class NIOFileChannel02 { public static void main(String[] args) throws Exception { //創建文件的輸入流 File file = new File("/Users/conglongli/Desktop/file01.txt"); FileInputStream fileInputStream = new FileInputStream(file); //通過 fileInputStream 獲取對應的 FileChannel -> 實際類型 FileChannelImpl FileChannel fileChannel = fileInputStream.getChannel(); //創建緩沖區 ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length()); //將通道的數據讀入到 Buffer fileChannel.read(byteBuffer); //將 byteBuffer 的字節數據轉成 String System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } }
(3)使用一個Buffer完成讀取和寫入
public class NIOFileChannel03 { public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("/Users/conglongli/Desktop/file01.txt"); FileChannel fileChannel01 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("/Users/conglongli/Desktop/file05.txt"); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { //循環讀取 //這里有一個重要的操作,一定不要忘了 /* public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; } */ byteBuffer.clear(); //清空 buffer int read = fileChannel01.read(byteBuffer); System.out.println("read = " + read); if (read == -1) { //表示讀完 break; } //將 buffer 中的數據寫入到 fileChannel02--2.txt byteBuffer.flip(); fileChannel02.write(byteBuffer); } //關閉相關的流 fileInputStream.close(); fileOutputStream.close(); } }
(4)直接使用transferFrom ⽅法讀取和寫入
public class NIOFileChannel04 { public static void main(String[] args) throws Exception { //創建相關流 FileInputStream fileInputStream = new FileInputStream("/Users/conglongli/Desktop/123.png"); FileOutputStream fileOutputStream = new FileOutputStream("/Users/conglongli/Desktop/456.png"); //獲取各個流對應的 FileChannel FileChannel sourceCh = fileInputStream.getChannel(); FileChannel destCh = fileOutputStream.getChannel(); //使用 transferForm 完成拷貝 destCh.transferFrom(sourceCh, 0, sourceCh.size()); //關閉相關通道和流 sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); } }
3、關於 Buffer 和 Channel 的注意事項和細節
(1)ByteBuffer ⽀持類型化的 put 和 get , put 放⼊的是什么數據類型, get 就應該使⽤相應的數據類型來取出,否則可能有 BufferUnderflowException 異常。
public class NIOByteBufferPutGet { public static void main(String[] args) { //創建一個 Buffer ByteBuffer buffer = ByteBuffer.allocate(64); //類型化方式放入數據 buffer.putInt(100); buffer.putLong(9); buffer.putChar('尚'); buffer.putShort((short) 4); //取出 buffer.flip(); System.out.println(); System.out.println(buffer.getInt()); System.out.println(buffer.getLong()); System.out.println(buffer.getInt()); //這里應該使用buffer.getChar() System.out.println(buffer.getShort()); } }

(2)可以使用buffer.asReadOnlyBuffer()將⼀個普通 Buffer 轉成只讀 Buffer
public class ReadOnlyBuffer { public static void main(String[] args) { //創建一個 buffer ByteBuffer buffer = ByteBuffer.allocate(64); for (int i = 0; i < 64; i++) { buffer.put((byte) i); } //讀取 buffer.flip();//得到一個只讀的 Buffer ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println(readOnlyBuffer.getClass()); //讀取 while (readOnlyBuffer.hasRemaining()) { System.out.println(readOnlyBuffer.get()); } readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException } }

(3)NIO 還提供了 MappedByteBuffer ,可以讓⽂件直接在內存(堆外的內存)中進⾏修改,⽽如何同步到⽂件由 NIO 來完成。
public class MappedByteBufferTest { public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/conglongli/Desktop/file01.txt", "rw"); //獲取對應的通道 FileChannel channel = randomAccessFile.getChannel(); /** * 參數 1:FileChannel.MapMode.READ_WRITE 使用的讀寫模式 * 參數 2:0:可以直接修改的起始位置 * 參數 3:5: 是映射到內存的大小(不是索引位置),即將 1.txt 的多少個字節映射到內存 * 可以直接修改的范圍就是 0-5 * 實際類型 DirectByteBuffer */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0, (byte) 'C'); mappedByteBuffer.put(3, (byte) '8'); //mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException randomAccessFile.close(); System.out.println("修改成功~~"); } }
(4)前⾯的讀寫操作,都是通過⼀個 Buffer 完成的, NIO 還⽀持通過多個 Buffer (即 Buffer 數組)完成讀寫操作,即 Scattering 和 Gathering
public class ScatteringAndGatheringTest { public static void main(String[] args) throws Exception { //使用 ServerSocketChannel 和 SocketChannel 網絡 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); //綁定端口到 socket,並啟動 serverSocketChannel.socket().bind(inetSocketAddress); //創建 buffer 數組 ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(3); //等客戶端連接 (telnet) SocketChannel socketChannel = serverSocketChannel.accept(); int messageLength = 8; //假定從客戶端接收 8 個字節 //循環的讀取 while (true) { int byteRead = 0; while (byteRead < messageLength) { long l = socketChannel.read(byteBuffers); byteRead += 1; //累計讀取的字節數 System.out.println("byteRead = " + byteRead); //使用流打印,看看當前的這個 buffer 的 position 和 limit Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println); } //將所有的 buffer 進行 flip Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //將數據讀出顯示到客戶端 long byteWirte = 0; while (byteWirte < messageLength) { long l = socketChannel.write(byteBuffers);// byteWirte += l; } //將所有的buffer進行clear Arrays.asList(byteBuffers).forEach(buffer -> { buffer.clear(); }); System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength); } } }
(五)選擇器(Selector)
選擇器⽤於使⽤單個線程處理多個通道。因此,它需要較少的線程來處理這些通道。線程之間的切換對於操作系統來說是昂貴的。 因此,為了提⾼系統效率選擇器是有⽤的。NIO有選擇器,⽽IO沒有。
Java 的 NIO ,⽤⾮阻塞的 IO ⽅式。可以⽤⼀個線程,處理多個的客戶端連接,就會使⽤到Selector (選擇器)。Selector 能夠檢測多個注冊的通道上是否有事件發⽣(注意:多個 Channel 以事件的⽅式可以注冊到同⼀個 Selector ),如果有事件發⽣,便獲取事件然后針對每個事件進⾏相應的處理。
選擇器的好處是可以只⽤⼀個單線程去管理多個通道,也就是管理多個連接和請求。只有在連接/通道真正有讀寫事件發⽣時,才會進⾏讀寫,就⼤⼤地減少了系統開銷,並且不必為每個連接都創建⼀個線程,不⽤去維護多個線程。同時也避免了多線程之間的上下⽂切換導致的開銷。
選擇器需要介紹四個類:Selector、SelectionKey、ServerSocketChannel、SocketChannel
1、Selector類

Selector 相關⽅法說明:
selector.select(); //阻塞
selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
selector.wakeup(); //喚醒 selector
selector.selectNow(); //不阻塞,⽴⻢返還
2、SelectionKey
SelectionKey ,表示 Selector 和⽹絡通道的注冊關系,共四種:
int OP_ACCEPT :有新的⽹絡連接可以 accept ,值為 16
int OP_CONNECT :代表連接已經建⽴,值為 8
int OP_READ :代表讀操作,值為 1
int OP_WRITE :代表寫操作,值為 4
源碼中:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
SelectionKey 相關⽅法:

3、ServerSocketChannel
ServerSocketChannel 在服務器端監聽新的客戶端 Socket 連接,ServerSocketChannel類似ServerSocket。
相關⽅法如下:

4、SocketChannel
SocketChannel,⽹絡 IO 通道,具體負責進⾏讀寫操作。NIO 把緩沖區的數據寫⼊通道,或者把通道⾥的數據讀到緩沖區。SocketChannel 類似 Socket。

5、應用實例
服務端:
public class WebServer { public static void main(String[] args) { try { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000)); ssc.configureBlocking(false); Selector selector = Selector.open(); // 注冊 channel,並且指定感興趣的事件是 Accept ssc.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer readBuff = ByteBuffer.allocate(1024); ByteBuffer writeBuff = ByteBuffer.allocate(128); writeBuff.put("received".getBytes()); writeBuff.flip(); while (true) { int nReady = selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); if (key.isAcceptable()) { // 創建新的連接,並且把連接注冊到selector上,而且, // 聲明這個channel只對讀操作感興趣。 SocketChannel socketChannel = ssc.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); readBuff.clear(); socketChannel.read(readBuff); String msg = new String(readBuff.array(),0, readBuff.position(), "utf-8"); readBuff.flip(); System.out.println("received : " + msg); key.interestOps(SelectionKey.OP_WRITE); } else if (key.isWritable()) { writeBuff.rewind(); SocketChannel socketChannel = (SocketChannel) key.channel(); socketChannel.write(writeBuff); key.interestOps(SelectionKey.OP_READ); } } } } catch (IOException e) { e.printStackTrace(); } } }
客戶端:
public class WebClient { public static void main(String[] args) throws IOException { try { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000)); ByteBuffer writeBuffer = ByteBuffer.allocate(32); ByteBuffer readBuffer = ByteBuffer.allocate(32); writeBuffer.put("hello111".getBytes()); writeBuffer.flip(); while (true) { writeBuffer.rewind(); socketChannel.write(writeBuffer); readBuffer.clear(); socketChannel.read(readBuffer); } } catch (IOException e) { } } }
啟動兩個客戶端,一個客戶端put值為hello111,一個客戶端put值為hello111222,服務端輸出結果:

(六)NIO與零拷貝
可以參看一下:NIO與零拷貝
四、AIO
JDK7 引⼊了 AsynchronousI/O ,即 AIO 。在進⾏ I/O 編程中,常⽤到兩種模式: Reactor和 Proactor 。 Java 的 NIO 就是 Reactor ,當有事件觸發時,服務器端得到通知,進⾏相應的處理。
AIO 即 NIO2.0 ,叫做異步不阻塞的 IO 。 AIO 引⼊異步通道的概念,采⽤了 Proactor 模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,⼀般適⽤於連接數較多且連接時間較⻓的應⽤。
⽬前 AIO 還沒有⼴泛應⽤, Netty 也是基於 NIO ,⽽不是 AIO。
