getChannel 方法會調用 FileChannelImpl 的工廠方法構建一個 FileChannelImpl 實例,FileChannelImpl 是抽象類 FileChannel 的一個子類實現。
構成 FileChannelImpl 實例所需的必要參數有,該文件的文件指針,該文件的完整路徑,讀寫權限等。
第二部分:
Buffer 的基本結構我們上述已經簡單介紹了,這裡不再贅述了,所謂的緩存區,本質上就是字節數組。
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity);}
ByteBuffer 實例的構建是通過工廠模式產生的,必須指定參數 capacity 作為內部字節數組的容量。HeapByteBuffer 是虛擬機的堆上內存,所有數據都將存儲在堆空間,我們不久將會介紹它的一個兄弟,DirectByteBuffer,它被分配在堆外內存中,具體的一會說。
這個 HeapByteBuffer 的構造情況我們不妨跟進去看看:
HeapByteBuffer(int cap, int lim) { super(-1, 0, lim, cap, new byte[cap], 0);}
調用父類的構造方法,初始化我們在 ByteBuffer 中提過的一些屬性值,如 position,capacity,mark,limit,offset 以及字節數組 hb。
接著,我們看看這個 read 方法的調用鏈。
這個 read 方法是子類 FileChannelImpl 對父類 FileChannel read 方法的重寫。這個方法不是讀操作的核心,我們簡單概括一下,該方法首先會拿到當前通道實例的鎖,如果沒有被其他線程佔有,那麼佔有該鎖,並調用 IOUtil 的 read 方法。
IOUtil 的 read 方法內部也調用了很多方法,有的甚至是本地方法,這裡只簡單介紹一下整個 read 方法的大體邏輯,具體細節留待大家自行學習。
首先判斷我們的 ByteBuffer 實例是不是一個 DirectBuffer,也就是判斷當前的 ByteBuffer 實例是不是被分配在直接內存中,如果是,那麼將調用 readIntoNativeBuffer 方法從磁盤讀取數據直接放入 ByteBuffer 實例所在的直接內存中。
否則,虛擬機將在直接內存區域分配一塊內存,該內存區域的首地址存儲在 var5 實例的 address 屬性中。
接著從磁盤讀取數據放入 var5 所代表的直接內存區域中。
最後,put 方法會將 var5 所代表的直接內存區域中的數據寫入到 var1 所代表的堆內緩存區並釋放臨時創建的直接內存空間。
這樣,我們傳入的緩存區中就成功的被讀入了數據。寫操作是相反的,大家可以自行類比,反正堆內數據想要到達磁盤就必定要經過堆外內存的複製過程。
第三第四部分比較簡單,這裡不再贅述了。提醒一下,想要更好的使用這個通道和緩存區進行文件讀寫操作,你就一定得對緩存區的幾個變量的值時刻把握住,position 和 limit 當前的值是什麼,大致什麼位置,一定得清晰,否則這個讀寫共存的緩存區可能會讓你暈頭轉向。
選擇器 Selector
Selector 是 Java NIO 的一個組件,它用於監聽多個 Channel 的各種狀態,用於管理多個 Channel。但本質上由於 FileChannel 不支持註冊選擇器,所以 Selector 一般被認為是服務於網絡套接字通道的。
而大家口中的「NIO 是非阻塞的」,準確來說,指的是網絡編程中客戶端與服務端連接交換數據的過程是非阻塞的。普通的文件讀寫依然是阻塞的,和 IO 是一樣的,這一點可能很多初學者會懵,包括我當時也總想不通為什麼說 NIO 的文件讀寫是非阻塞的,明明就是阻塞的。
創建一個選擇器一般是通過 Selector 的工廠方法,Selector.open :
Selector selector = Selector.open();
而一個通道想要註冊到某個選擇器中,必須調整模式為非阻塞模式,例如:
//創建一個 TCP 套接字通道SocketChannel channel = SocketChannel.open();//調整通道為非阻塞模式channel.configureBlocking(false);//向選擇器註冊一個通道SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
以上代碼是註冊一個通道到選擇器中的最簡單版本,支持註冊選擇器的通道都有一個 register 方法,該方法就是用於註冊當前實例通道到指定選擇器的。
該方法的第一個參數就是目標選擇器,第二個參數其實是一個二進制掩碼,它指明當前選擇器感興趣當前通道的哪些事件。以枚舉類型提供了以下幾種取值:
- int OP_READ = 1 << 0;
- int OP_WRITE = 1 << 2;
- int OP_CONNECT = 1 << 3;
- int OP_ACCEPT = 1 << 4;
這種用二進制掩碼來表示某些狀態的機制,我們在講述虛擬機類類文件結構的時候也遇到過,它就是用一個二進制位來描述一種狀態。
register 方法會返回一個 SelectionKey 實例,該實例代表的就是選擇器與通道的一個關聯關係。你可以調用它的 selector 方法返回當前相關聯的選擇器實例,也可以調用它的 channel 方法返回當前關聯關係中的通道實例。
除此之外,SelectionKey 的 readyOps 方法將返回當前選擇感興趣當前通道中事件中準備就緒的事件集合,依然返回的一個整型數值,也就是一個二進制掩碼。
例如:
int readySet = selectionKey.readyOps();
假如 readySet 的值為 13,二進制 「0000 1101」,從後向前數,第一位為 1,第三位為 1,第四位為 1,那麼說明選擇器關聯的通道,讀就緒、寫就緒,連接就緒。
所以,當我們註冊一個通道到選擇器之後,就可以通過返回的 SelectionKey 實例監聽該通道的各種事件。
當然,一旦某個選擇器中註冊了多個通道,我們不可能一個一個的記錄它們註冊時返回的 SelectionKey 實例來監聽通道事件,選擇器應當有方法返回所有註冊成功的通道相關的 SelectionKey 實例。
Setkeys = selector.selectedKeys();
selectedKeys 方法會返回選擇器中註冊成功的所有通道的 SelectionKey 實例集合。我們通過這個集合的 SelectionKey 實例,可以得到所有通道的事件就緒情況並進行相應的處理操作。
下面我們以一個簡單的客戶端服務端連接通訊的實例應用一下上述理論知識:
服務端代碼:
這段小程序的運行的實際效果是這樣的,客戶端建立請求到服務端,待請求完全建立,客戶端會去檢查服務端是否有數據寫回,而服務端的任務就很簡單了,接受任意客戶端的請求連接併為它寫回一段數據。
別看整個過程很簡單,但只要你有一點模糊的地方,你這個功能就不可能實現,不信你試試,尤其是加了選擇器的客戶端代碼,更值得大家一行一行分析。提醒一點的是,大家應更多的關注於哪些方法是阻塞的,哪些是非阻塞的,這會有助於分析代碼。
這其實也算一個最最簡單的服務器客戶端請求模型了,理解了這一點相信會有助於理解瀏覽器與 Web 服務器的工作原理的,這裡我就不再帶大家分析了,有任何不同看法的也歡迎給我留言,咱們一起學習探討。
想必你也能發現,加了選擇器的代碼會複雜很多,也並不一定高效於原來的代碼,這其實是因為你的功能比較簡單,並不涉及大量通道處理,邏輯一旦複雜起來,選擇器給你帶來的好處會非常明顯。
其實,NIO 中還有一塊 AIO ,也就是異步 IO 並沒有介紹,因為異步 IO 涉及到很多其他方面知識,這裡暫時不做介紹,後續文章將單獨介紹異步任務等相關內容。
最後:
給大家分享資深架構師錄製的視頻:(有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構)等這些成為架構師必備的內容,還有阿里大牛直播講解技術!
後臺私信回覆“架構” 就可以免費獲得這些視頻資料!
閱讀更多 JAVA技術程序員 的文章