Java中IO的NIO通道解析
概述
NIO 中的 N 可以理解為 Non-blocking,一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入,對應的在java.nio包下。
NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向緩沖、基于通道的 I/O 操作方法。
NIO 提供了與傳統(tǒng) BIO 模型中的 Socket 和 ServerSocket 相對應的 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現(xiàn)。
NIO 這兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。
對于低負載、低并發(fā)的應用程序,可以使用同步阻塞 I/O 來提升開發(fā)效率和更好的維護性;對于高負載、高并發(fā)的(網(wǎng)絡)應用,應使用 NIO 的非阻塞模式來開發(fā)。
正文
我們先看一下 NIO 涉及到的核心關聯(lián)類圖,如下:
上圖中有三個關鍵類:Channel 、Selector 和 Buffer,它們是 NIO 中的核心概念。
- Channel:可以理解為通道;
- Selector:可以理解為選擇器;
- Buffer:可以理解為數(shù)據(jù)緩沖流;
NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 傳輸過程中涉及到的信息具體化,讓程序員有機會去控制它們。
當我們進行傳統(tǒng)的網(wǎng)絡 IO 操作時,比如調(diào)用 write() 往 Socket 中的 SendQ 隊列寫數(shù)據(jù)時,當一次寫的數(shù)據(jù)超過 SendQ 長度時,操作系統(tǒng)會按照 SendQ 的長度進行分割的,這個過程中需要將用戶空間數(shù)據(jù)和內(nèi)核地址空間進行切換,而這個切換不是程序員可以控制的,由底層操作系統(tǒng)來幫我們處理。
而在 Buffer 中,我們可以控制 Buffer 的 capacity(容量),并且是否擴容以及如何擴容都可以控制。
代碼示例
實例圖:
客戶端程序示例:
package org.example.nio.example; 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.util.Iterator; import java.util.Set; /** * NIO客戶端 */ public class NIOClient { // 通道管理器(Selector) private static Selector selector; public static void main(String[] args) throws IOException { // 創(chuàng)建通道管理器(Selector) selector = Selector.open(); // 創(chuàng)建通道SocketChannel SocketChannel channel = SocketChannel.open(); // 將通道設置為非阻塞 channel.configureBlocking(false); // 客戶端連接服務器,其實方法執(zhí)行并沒有實現(xiàn)連接,需要在handleConnect方法中調(diào)channel.finishConnect()才能完成連接 channel.connect(new InetSocketAddress("127.0.0.1", 9090)); /** * 將通道(Channel)注冊到通道管理器(Selector),并為該通道注冊selectionKey.OP_CONNECT * 注冊該事件后,當事件到達的時候,selector.select()會返回, * 如果事件沒有到達selector.select()會一直阻塞。 */ channel.register(selector, SelectionKey.OP_CONNECT); // 循環(huán)處理 while (true) { /* * 選擇一組可以進行I/O操作的事件,放在selector中,客戶端的該方法不會阻塞, * selector的wakeup方法被調(diào)用,方法返回,而對于客戶端來說,通道一直是被選中的 * 這里和服務端的方法不一樣,查看api注釋可以知道,當至少一個通道被選中時。 */ selector.select(); // 獲取監(jiān)聽事件 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); // 迭代處理 while (iterator.hasNext()) { // 獲取事件 SelectionKey key = iterator.next(); // 移除事件,避免重復處理 iterator.remove(); // 檢查是否是一個就緒的已經(jīng)連接服務端成功事件 if (key.isConnectable()) { handleConnect(key); } else if (key.isReadable()) {// 檢查套接字是否已經(jīng)準備好讀數(shù)據(jù) handleRead(key); } } } } /** * 處理客戶端連接服務端成功事件 */ private static void handleConnect(SelectionKey key) throws IOException { // 獲取與服務端建立連接的通道 SocketChannel channel = (SocketChannel) key.channel(); if (channel.isConnectionPending()) { // channel.finishConnect()才能完成連接 channel.finishConnect(); } channel.configureBlocking(false); // 數(shù)據(jù)寫入通道 String msg = "Hello Server!"; channel.write(ByteBuffer.wrap(msg.getBytes())); // 通道注冊到選擇器,并且這個通道只對讀事件感興趣 channel.register(selector, SelectionKey.OP_READ); } /** * 監(jiān)聽到讀事件,讀取客戶端發(fā)送過來的消息 */ private static void handleRead(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); // 從通道讀取數(shù)據(jù)到緩沖區(qū) ByteBuffer buffer = ByteBuffer.allocate(128); channel.read(buffer); // 輸出服務端響應發(fā)送過來的消息 byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服務端發(fā)來的消息:" + msg); } }
服務端示例:
package org.example.nio.example; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; 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; /** * NIO服務端 */ public class NIOServer { // 通道管理器(Selector) private static Selector selector; public static void main(String[] args) throws IOException { // 創(chuàng)建通道管理器(Selector) selector = Selector.open(); // 創(chuàng)建通道ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 將通道設置為非阻塞 serverSocketChannel.configureBlocking(false); // 將ServerSocketChannel對應的ServerSocket綁定到指定端口(port) ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(new InetSocketAddress(9090)); /** * 將通道(Channel)注冊到通道管理器(Selector),并為該通道注冊selectionKey.OP_ACCEPT事件 * 注冊該事件后,當事件到達的時候,selector.select()會返回, * 如果事件沒有到達selector.select()會一直阻塞。 */ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 循環(huán)處理 while (true) { // 當注冊事件到達時,方法返回,否則該方法會一直阻塞 selector.select(); // 獲取監(jiān)聽事件 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); // 迭代處理 while (iterator.hasNext()) { // 獲取事件 SelectionKey key = iterator.next(); // 移除事件,避免重復處理 iterator.remove(); // 檢查是否是一個就緒的可以被接受的客戶端請求連接 if (key.isAcceptable()) { handleAccept(key); } else if (key.isReadable()) {// 檢查套接字是否已經(jīng)準備好讀數(shù)據(jù) handleRead(key); } } } } /** * 處理客戶端連接成功事件 */ private static void handleAccept(SelectionKey key) throws IOException { // 獲取客戶端連接通道 ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = server.accept(); socketChannel.configureBlocking(false); // 信息通過通道發(fā)送給客戶端 String msg = "Hello Client!"; socketChannel.write(ByteBuffer.wrap(msg.getBytes())); // 給通道設置讀事件,客戶端監(jiān)聽到讀事件后,進行讀取操作 socketChannel.register(selector, SelectionKey.OP_READ); } /** * 監(jiān)聽到讀事件,讀取客戶端發(fā)送過來的消息 */ private static void handleRead(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); // 從通道讀取數(shù)據(jù)到緩沖區(qū) ByteBuffer buffer = ByteBuffer.allocate(128); channel.read(buffer); // 輸出客戶端發(fā)送過來的消息 byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("server received msg from client:" + msg); } }
通道channel
NIO 的核心就是通道和緩存區(qū),所以它們的工作模式是這樣的:
通道有點類似 IO 中的流,但不同的是,同一個通道既允許讀也允許寫,而任意一個流要么是讀流要么是寫流。 但是你要明白一點,通道和流一樣都是需要基于物理文件的,而每個流或者通道都通過文件指針操作文件,這里說的通道是雙向的也是有前提的,那就是通道基于隨機訪問文件RandomAccessFile的可讀可寫文件指針。
基本的通道類型有如下一些:
FileChannel 是基于文件的通道;
SocketChannel 和 ServerSocketChannel 用于網(wǎng)絡 TCP 套接字數(shù)據(jù)報讀寫;
DatagramChannel 是用于網(wǎng)絡 UDP 套接字數(shù)據(jù)報讀寫。
通道不能單獨存在,它永遠需要綁定一個緩存區(qū),所有的數(shù)據(jù)只會存在于緩存區(qū)中,無論你是寫或是讀,必然是緩存區(qū)通過通道到達磁盤文件,或是磁盤文件通過通道到達緩存區(qū)。即緩存區(qū)是數(shù)據(jù)的起點,也是終點。
緩存區(qū)Buffer
緩沖區(qū)(Buffer):一個用于特定基本數(shù)據(jù)類型的容器。由java.nio包定義的,所有緩沖區(qū)都是Buffer抽象類的子類。
Java NIO 中的 Buffer 主要用于和 NIO 通道進行交互,數(shù)據(jù)是從通道讀入到緩沖區(qū)的,然后從緩沖區(qū)中寫入到通道中的。
Buffer 就像一個數(shù)組,可以保存多個相同類型的數(shù)據(jù)。根據(jù)數(shù)據(jù)類型的不同(boolean)除外,有以下 Buffer 常用子類:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。上述 Buffer 類他們都是通過相似的方法進行管理數(shù)據(jù)的,只是各自管理的數(shù)據(jù)類型不同而已。都是通過如下的方法獲取一個 Buffer 對象:
public static XxxBuffer allocate(int capacity) {}
緩沖區(qū)的基本屬性
- 容量(capacity):表示 Buffer 最大數(shù)據(jù)容量,緩沖區(qū)容量不能為負,并且一旦創(chuàng)建不能更改。
- 限制(limit):第一個不應該讀取或?qū)懭氲臄?shù)據(jù)的索引,即位于 limit 后的數(shù)據(jù)不可讀寫。緩沖區(qū)的限制不能為負,并且不能大于容量。
- 位置(position):下一個要讀取或?qū)懭氲臄?shù)據(jù)的索引。緩沖區(qū)的位置不能為負,并且不能大于其限制。
- 標記(mark)和重置(reset):標記是一個索引,通過Buffer中的mark()方法指定Buffer中的一個特定的position,之后可以通過調(diào)用reset()方法恢復到這個position。
簡而言之:0 <= mark <= position <= limit <= capacity。
Selector(選擇器)
Selector 被稱為選擇器 ,當然你也可以翻譯為多路復用器 。它是Java NIO 核心組件中的一個,用于檢查一個或多個 Channel(通道)的狀態(tài)是否處于連接就緒、接受就緒、可讀就緒、可寫就緒。
如此可以實現(xiàn)單線程管理多個 channels,也就是可以管理多個網(wǎng)絡連接。
使用 Selector 的好處在于: 相比傳統(tǒng)方式使用多個線程來管理 IO,Selector 使用了更少的線程就可以處理通道了,并且實現(xiàn)網(wǎng)絡高效傳輸!
創(chuàng)建一個選擇器一般是通過 Selector 的工廠方法,Selector.open :
Selector selector = Selector.open();
而一個通道想要注冊到某個選擇器中,必須調(diào)整模式為非阻塞模式,例如:
//創(chuàng)建一個 TCP 套接字通道 SocketChannel channel = SocketChannel.open(); //調(diào)整通道為非阻塞模式 channel.configureBlocking(false); //向選擇器注冊一個通道 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
以上代碼是注冊一個通道到選擇器中的最簡單版本,支持注冊選擇器的通道都有一個 register 方法,該方法就是用于注冊當前實例通道到指定選擇器的。
該方法的第一個參數(shù)就是目標選擇器,第二個參數(shù)其實是一個二進制掩碼,它指明當前選擇器感興趣當前通道的哪些事件。以枚舉類型提供了以下幾種取值:
- int OP_READ = 1 << 0;
- int OP_WRITE = 1 << 2;
- int OP_CONNECT = 1 << 3;
- int OP_ACCEPT = 1 << 4;
這種用二進制掩碼來表示某些狀態(tài)的機制,我們在講述虛擬機類類文件結(jié)構(gòu)的時候也遇到過,它就是用一個二進制位來描述一種狀態(tài)。
register 方法會返回一個 SelectionKey 實例,該實例代表的就是選擇器與通道的一個關聯(lián)關系。你可以調(diào)用它的 selector 方法返回當前相關聯(lián)的選擇器實例,也可以調(diào)用它的 channel 方法返回當前關聯(lián)關系中的通道實例。
除此之外,SelectionKey 的 readyOps 方法將返回當前選擇感興趣當前通道中事件中準備就緒的事件集合,依然返回的一個整型數(shù)值,也就是一個二進制掩碼。
例如:
int readySet = selectionKey.readyOps();
假如 readySet 的值為 13,二進制 「0000 1101」,從后向前數(shù),第一位為 1,第三位為 1,第四位為 1,那么說明選擇器關聯(lián)的通道,讀就緒、寫就緒,連接就緒。
所以,當我們注冊一個通道到選擇器之后,就可以通過返回的 SelectionKey 實例監(jiān)聽該通道的各種事件。
當然,一旦某個選擇器中注冊了多個通道,我們不可能一個一個的記錄它們注冊時返回的 SelectionKey 實例來監(jiān)聽通道事件,選擇器應當有方法返回所有注冊成功的通道相關的 SelectionKey 實例。
Set<SelectionKey> keys = selector.selectedKeys();
selectedKeys 方法會返回選擇器中注冊成功的所有通道的 SelectionKey 實例集合。我們通過這個集合的 SelectionKey 實例,可以得到所有通道的事件就緒情況并進行相應的處理操作。
總結(jié)
優(yōu)點
1個線程就行就能處理所有連接,這個線程不停循環(huán)遍歷就行了。
缺點
單線程不停循環(huán)發(fā)起系統(tǒng)調(diào)用,一樣會耗盡 CPU 資源。
NIO 的瓶頸
在于需要不停的調(diào)起系統(tǒng)調(diào)用,每個鏈接我們都要調(diào)系統(tǒng)調(diào)用詢問是否有過來數(shù)據(jù),我們要是明確的知道哪個連接有數(shù)據(jù)包過來呢,就不用挨個遍歷尋找找答案了。
到此這篇關于Java中IO的NIO通道解析的文章就介紹到這了,更多相關NIO通道解析內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot的HandlerInterceptor中依賴注入為null問題
這篇文章主要介紹了SpringBoot的HandlerInterceptor中依賴注入為null問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09關于Nacos配置管理的統(tǒng)一配置管理、自動刷新詳解
這篇文章主要介紹了關于Nacos配置管理的統(tǒng)一配置管理、自動刷新詳解,Nacos是阿里的一個開源產(chǎn)品,是針對微服務架構(gòu)中的服務發(fā)現(xiàn)、配置管理、服務治理的綜合型解決方案,需要的朋友可以參考下2023-05-05SpringBoot定時調(diào)度之Timer與Quartz詳解
Java?中常用的定時調(diào)度框架有以下幾種:Timer、ScheduledExecutorService、Spring?Task和Quartz,本文主要來和大家講講他們的具體使用,需要的可以參考一下2023-06-06