Java中IO的NIO通道解析
概述
NIO 中的 N 可以理解為 Non-blocking,一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入,對應(yīng)的在java.nio包下。
NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向緩沖、基于通道的 I/O 操作方法。
NIO 提供了與傳統(tǒng) BIO 模型中的 Socket 和 ServerSocket 相對應(yīng)的 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現(xiàn)。
NIO 這兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。
對于低負載、低并發(fā)的應(yīng)用程序,可以使用同步阻塞 I/O 來提升開發(fā)效率和更好的維護性;對于高負載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 的非阻塞模式來開發(fā)。
正文
我們先看一下 NIO 涉及到的核心關(guān)聯(lián)類圖,如下:

上圖中有三個關(guān)鍵類:Channel 、Selector 和 Buffer,它們是 NIO 中的核心概念。
- Channel:可以理解為通道;
- Selector:可以理解為選擇器;
- Buffer:可以理解為數(shù)據(jù)緩沖流;
NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 傳輸過程中涉及到的信息具體化,讓程序員有機會去控制它們。
當我們進行傳統(tǒng)的網(wǎng)絡(luò) 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();
// 將通道設(shè)置為非阻塞
channel.configureBlocking(false);
// 客戶端連接服務(wù)器,其實方法執(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)用,方法返回,而對于客戶端來說,通道一直是被選中的
* 這里和服務(wù)端的方法不一樣,查看api注釋可以知道,當至少一個通道被選中時。
*/
selector.select();
// 獲取監(jiān)聽事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代處理
while (iterator.hasNext()) {
// 獲取事件
SelectionKey key = iterator.next();
// 移除事件,避免重復(fù)處理
iterator.remove();
// 檢查是否是一個就緒的已經(jīng)連接服務(wù)端成功事件
if (key.isConnectable()) {
handleConnect(key);
} else if (key.isReadable()) {// 檢查套接字是否已經(jīng)準備好讀數(shù)據(jù)
handleRead(key);
}
}
}
}
/**
* 處理客戶端連接服務(wù)端成功事件
*/
private static void handleConnect(SelectionKey key) throws IOException {
// 獲取與服務(wù)端建立連接的通道
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);
// 輸出服務(wù)端響應(yīng)發(fā)送過來的消息
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服務(wù)端發(fā)來的消息:" + msg);
}
}服務(wù)端示例:
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服務(wù)端
*/
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();
// 將通道設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
// 將ServerSocketChannel對應(yīng)的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();
// 移除事件,避免重復(fù)處理
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()));
// 給通道設(shè)置讀事件,客戶端監(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)絡(luò) TCP 套接字數(shù)據(jù)報讀寫;
DatagramChannel 是用于網(wǎng)絡(luò) 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):第一個不應(yīng)該讀取或?qū)懭氲臄?shù)據(jù)的索引,即位于 limit 后的數(shù)據(jù)不可讀寫。緩沖區(qū)的限制不能為負,并且不能大于容量。
- 位置(position):下一個要讀取或?qū)懭氲臄?shù)據(jù)的索引。緩沖區(qū)的位置不能為負,并且不能大于其限制。
- 標記(mark)和重置(reset):標記是一個索引,通過Buffer中的mark()方法指定Buffer中的一個特定的position,之后可以通過調(diào)用reset()方法恢復(fù)到這個position。
簡而言之:0 <= mark <= position <= limit <= capacity。
Selector(選擇器)
Selector 被稱為選擇器 ,當然你也可以翻譯為多路復(fù)用器 。它是Java NIO 核心組件中的一個,用于檢查一個或多個 Channel(通道)的狀態(tài)是否處于連接就緒、接受就緒、可讀就緒、可寫就緒。
如此可以實現(xiàn)單線程管理多個 channels,也就是可以管理多個網(wǎng)絡(luò)連接。

使用 Selector 的好處在于: 相比傳統(tǒng)方式使用多個線程來管理 IO,Selector 使用了更少的線程就可以處理通道了,并且實現(xiàn)網(wǎng)絡(luò)高效傳輸!
創(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 實例,該實例代表的就是選擇器與通道的一個關(guān)聯(lián)關(guān)系。你可以調(diào)用它的 selector 方法返回當前相關(guān)聯(lián)的選擇器實例,也可以調(diào)用它的 channel 方法返回當前關(guān)聯(lián)關(guān)系中的通道實例。
除此之外,SelectionKey 的 readyOps 方法將返回當前選擇感興趣當前通道中事件中準備就緒的事件集合,依然返回的一個整型數(shù)值,也就是一個二進制掩碼。
例如:
int readySet = selectionKey.readyOps();
假如 readySet 的值為 13,二進制 「0000 1101」,從后向前數(shù),第一位為 1,第三位為 1,第四位為 1,那么說明選擇器關(guān)聯(lián)的通道,讀就緒、寫就緒,連接就緒。
所以,當我們注冊一個通道到選擇器之后,就可以通過返回的 SelectionKey 實例監(jiān)聽該通道的各種事件。
當然,一旦某個選擇器中注冊了多個通道,我們不可能一個一個的記錄它們注冊時返回的 SelectionKey 實例來監(jiān)聽通道事件,選擇器應(yīng)當有方法返回所有注冊成功的通道相關(guān)的 SelectionKey 實例。
Set<SelectionKey> keys = selector.selectedKeys();
selectedKeys 方法會返回選擇器中注冊成功的所有通道的 SelectionKey 實例集合。我們通過這個集合的 SelectionKey 實例,可以得到所有通道的事件就緒情況并進行相應(yīng)的處理操作。
總結(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ù)包過來呢,就不用挨個遍歷尋找找答案了。
到此這篇關(guān)于Java中IO的NIO通道解析的文章就介紹到這了,更多相關(guān)NIO通道解析內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實現(xiàn)文件上傳的兩種方法(uploadify和Spring)
這篇文章主要為大家詳細介紹了Java實現(xiàn)文件上傳的兩種方法,uploadify和Spring實現(xiàn)文件上傳,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
springboot?@PostConstruct無效的解決
這篇文章主要介紹了springboot?@PostConstruct無效的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
很多人竟然不知道Java線程池的創(chuàng)建方式有7種
本文主要介紹了很多人竟然不知道Java線程池的創(chuàng)建方式有7種,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07
SpringBoot整合SpringSecurity實現(xiàn)權(quán)限控制之實現(xiàn)多標簽頁
這篇文章主要介紹了SpringBoot整合SpringSecurity實現(xiàn)權(quán)限控制之實現(xiàn)多標簽頁,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-11-11
詳解hibernate雙向多對多關(guān)聯(lián)映射XML與注解版
本篇文章主要介紹了詳解hibernate雙向多對多關(guān)聯(lián)映射XML與注解版,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
Springboot設(shè)置文件上傳大小限制的實現(xiàn)示例
Spring Boot工程嵌入的tomcat限制了請求的文件大小默認為1MB,單次請求的文件的總數(shù)不能大于10Mb,本文主要介紹了Springboot設(shè)置文件上傳大小限制的實現(xiàn)示例,感興趣的可以了解一下2023-11-11
SpringBoot集成Redis實現(xiàn)訂單超時管理的詳細步驟
文章描述了如何使用Redis來處理超時支付訂單,包括引入依賴、配置連接、定義實體類、實現(xiàn)服務(wù)和監(jiān)聽器等步驟,文章討論了優(yōu)化方案、注意事項以及Redis的性能和可靠性問題,感興趣的朋友跟隨小編一起看看吧2026-03-03
SpringBoot中使用Zookeeper實現(xiàn)分布式鎖的案例
本文主要介紹了SpringBoot中使用Zookeeper實現(xiàn)分布式鎖的案例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-01-01
SpringBoot集成Sharding-JDBC實現(xiàn)分庫分表方式
這篇文章主要介紹了SpringBoot集成Sharding-JDBC實現(xiàn)分庫分表方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07

