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

上圖中有三個(gè)關(guān)鍵類:Channel 、Selector 和 Buffer,它們是 NIO 中的核心概念。
- Channel:可以理解為通道;
- Selector:可以理解為選擇器;
- Buffer:可以理解為數(shù)據(jù)緩沖流;
NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 傳輸過程中涉及到的信息具體化,讓程序員有機(jī)會(huì)去控制它們。
當(dāng)我們進(jìn)行傳統(tǒng)的網(wǎng)絡(luò) IO 操作時(shí),比如調(diào)用 write() 往 Socket 中的 SendQ 隊(duì)列寫數(shù)據(jù)時(shí),當(dāng)一次寫的數(shù)據(jù)超過 SendQ 長度時(shí),操作系統(tǒng)會(huì)按照 SendQ 的長度進(jìn)行分割的,這個(gè)過程中需要將用戶空間數(shù)據(jù)和內(nèi)核地址空間進(jìn)行切換,而這個(gè)切換不是程序員可以控制的,由底層操作系統(tǒng)來幫我們處理。
而在 Buffer 中,我們可以控制 Buffer 的 capacity(容量),并且是否擴(kuò)容以及如何擴(kuò)容都可以控制。
代碼示例
實(shí)例圖:

客戶端程序示例:
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ù)器,其實(shí)方法執(zhí)行并沒有實(shí)現(xiàn)連接,需要在handleConnect方法中調(diào)channel.finishConnect()才能完成連接
channel.connect(new InetSocketAddress("127.0.0.1", 9090));
/**
* 將通道(Channel)注冊(cè)到通道管理器(Selector),并為該通道注冊(cè)selectionKey.OP_CONNECT
* 注冊(cè)該事件后,當(dāng)事件到達(dá)的時(shí)候,selector.select()會(huì)返回,
* 如果事件沒有到達(dá)selector.select()會(huì)一直阻塞。
*/
channel.register(selector, SelectionKey.OP_CONNECT);
// 循環(huán)處理
while (true) {
/*
* 選擇一組可以進(jìn)行I/O操作的事件,放在selector中,客戶端的該方法不會(huì)阻塞,
* selector的wakeup方法被調(diào)用,方法返回,而對(duì)于客戶端來說,通道一直是被選中的
* 這里和服務(wù)端的方法不一樣,查看api注釋可以知道,當(dāng)至少一個(gè)通道被選中時(shí)。
*/
selector.select();
// 獲取監(jiān)聽事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代處理
while (iterator.hasNext()) {
// 獲取事件
SelectionKey key = iterator.next();
// 移除事件,避免重復(fù)處理
iterator.remove();
// 檢查是否是一個(gè)就緒的已經(jīng)連接服務(wù)端成功事件
if (key.isConnectable()) {
handleConnect(key);
} else if (key.isReadable()) {// 檢查套接字是否已經(jīng)準(zhǔn)備好讀數(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()));
// 通道注冊(cè)到選擇器,并且這個(gè)通道只對(duì)讀事件感興趣
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對(duì)應(yīng)的ServerSocket綁定到指定端口(port)
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(9090));
/**
* 將通道(Channel)注冊(cè)到通道管理器(Selector),并為該通道注冊(cè)selectionKey.OP_ACCEPT事件
* 注冊(cè)該事件后,當(dāng)事件到達(dá)的時(shí)候,selector.select()會(huì)返回,
* 如果事件沒有到達(dá)selector.select()會(huì)一直阻塞。
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循環(huán)處理
while (true) {
// 當(dāng)注冊(cè)事件到達(dá)時(shí),方法返回,否則該方法會(huì)一直阻塞
selector.select();
// 獲取監(jiān)聽事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代處理
while (iterator.hasNext()) {
// 獲取事件
SelectionKey key = iterator.next();
// 移除事件,避免重復(fù)處理
iterator.remove();
// 檢查是否是一個(gè)就緒的可以被接受的客戶端請(qǐng)求連接
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {// 檢查套接字是否已經(jīng)準(zhǔn)備好讀數(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)聽到讀事件后,進(jì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ū),所以它們的工作模式是這樣的:

通道有點(diǎn)類似 IO 中的流,但不同的是,同一個(gè)通道既允許讀也允許寫,而任意一個(gè)流要么是讀流要么是寫流。 但是你要明白一點(diǎn),通道和流一樣都是需要基于物理文件的,而每個(gè)流或者通道都通過文件指針操作文件,這里說的通道是雙向的也是有前提的,那就是通道基于隨機(jī)訪問文件RandomAccessFile的可讀可寫文件指針。
基本的通道類型有如下一些:
FileChannel 是基于文件的通道;
SocketChannel 和 ServerSocketChannel 用于網(wǎng)絡(luò) TCP 套接字?jǐn)?shù)據(jù)報(bào)讀寫;
DatagramChannel 是用于網(wǎng)絡(luò) UDP 套接字?jǐn)?shù)據(jù)報(bào)讀寫。
通道不能單獨(dú)存在,它永遠(yuǎn)需要綁定一個(gè)緩存區(qū),所有的數(shù)據(jù)只會(huì)存在于緩存區(qū)中,無論你是寫或是讀,必然是緩存區(qū)通過通道到達(dá)磁盤文件,或是磁盤文件通過通道到達(dá)緩存區(qū)。即緩存區(qū)是數(shù)據(jù)的起點(diǎn),也是終點(diǎn)。
緩存區(qū)Buffer
緩沖區(qū)(Buffer):一個(gè)用于特定基本數(shù)據(jù)類型的容器。由java.nio包定義的,所有緩沖區(qū)都是Buffer抽象類的子類。
Java NIO 中的 Buffer 主要用于和 NIO 通道進(jìn)行交互,數(shù)據(jù)是從通道讀入到緩沖區(qū)的,然后從緩沖區(qū)中寫入到通道中的。
Buffer 就像一個(gè)數(shù)組,可以保存多個(gè)相同類型的數(shù)據(jù)。根據(jù)數(shù)據(jù)類型的不同(boolean)除外,有以下 Buffer 常用子類:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。上述 Buffer 類他們都是通過相似的方法進(jìn)行管理數(shù)據(jù)的,只是各自管理的數(shù)據(jù)類型不同而已。都是通過如下的方法獲取一個(gè) Buffer 對(duì)象:
public static XxxBuffer allocate(int capacity) {}緩沖區(qū)的基本屬性
- 容量(capacity):表示 Buffer 最大數(shù)據(jù)容量,緩沖區(qū)容量不能為負(fù),并且一旦創(chuàng)建不能更改。
- 限制(limit):第一個(gè)不應(yīng)該讀取或?qū)懭氲臄?shù)據(jù)的索引,即位于 limit 后的數(shù)據(jù)不可讀寫。緩沖區(qū)的限制不能為負(fù),并且不能大于容量。
- 位置(position):下一個(gè)要讀取或?qū)懭氲臄?shù)據(jù)的索引。緩沖區(qū)的位置不能為負(fù),并且不能大于其限制。
- 標(biāo)記(mark)和重置(reset):標(biāo)記是一個(gè)索引,通過Buffer中的mark()方法指定Buffer中的一個(gè)特定的position,之后可以通過調(diào)用reset()方法恢復(fù)到這個(gè)position。
簡而言之:0 <= mark <= position <= limit <= capacity。
Selector(選擇器)
Selector 被稱為選擇器 ,當(dāng)然你也可以翻譯為多路復(fù)用器 。它是Java NIO 核心組件中的一個(gè),用于檢查一個(gè)或多個(gè) Channel(通道)的狀態(tài)是否處于連接就緒、接受就緒、可讀就緒、可寫就緒。
如此可以實(shí)現(xiàn)單線程管理多個(gè) channels,也就是可以管理多個(gè)網(wǎng)絡(luò)連接。

使用 Selector 的好處在于: 相比傳統(tǒng)方式使用多個(gè)線程來管理 IO,Selector 使用了更少的線程就可以處理通道了,并且實(shí)現(xiàn)網(wǎng)絡(luò)高效傳輸!
創(chuàng)建一個(gè)選擇器一般是通過 Selector 的工廠方法,Selector.open :
Selector selector = Selector.open();
而一個(gè)通道想要注冊(cè)到某個(gè)選擇器中,必須調(diào)整模式為非阻塞模式,例如:
//創(chuàng)建一個(gè) TCP 套接字通道 SocketChannel channel = SocketChannel.open(); //調(diào)整通道為非阻塞模式 channel.configureBlocking(false); //向選擇器注冊(cè)一個(gè)通道 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
以上代碼是注冊(cè)一個(gè)通道到選擇器中的最簡單版本,支持注冊(cè)選擇器的通道都有一個(gè) register 方法,該方法就是用于注冊(cè)當(dāng)前實(shí)例通道到指定選擇器的。
該方法的第一個(gè)參數(shù)就是目標(biāo)選擇器,第二個(gè)參數(shù)其實(shí)是一個(gè)二進(jìn)制掩碼,它指明當(dāng)前選擇器感興趣當(dāng)前通道的哪些事件。以枚舉類型提供了以下幾種取值:
- int OP_READ = 1 << 0;
- int OP_WRITE = 1 << 2;
- int OP_CONNECT = 1 << 3;
- int OP_ACCEPT = 1 << 4;
這種用二進(jìn)制掩碼來表示某些狀態(tài)的機(jī)制,我們?cè)谥v述虛擬機(jī)類類文件結(jié)構(gòu)的時(shí)候也遇到過,它就是用一個(gè)二進(jìn)制位來描述一種狀態(tài)。
register 方法會(huì)返回一個(gè) SelectionKey 實(shí)例,該實(shí)例代表的就是選擇器與通道的一個(gè)關(guān)聯(lián)關(guān)系。你可以調(diào)用它的 selector 方法返回當(dāng)前相關(guān)聯(lián)的選擇器實(shí)例,也可以調(diào)用它的 channel 方法返回當(dāng)前關(guān)聯(lián)關(guān)系中的通道實(shí)例。
除此之外,SelectionKey 的 readyOps 方法將返回當(dāng)前選擇感興趣當(dāng)前通道中事件中準(zhǔn)備就緒的事件集合,依然返回的一個(gè)整型數(shù)值,也就是一個(gè)二進(jìn)制掩碼。
例如:
int readySet = selectionKey.readyOps();
假如 readySet 的值為 13,二進(jìn)制 「0000 1101」,從后向前數(shù),第一位為 1,第三位為 1,第四位為 1,那么說明選擇器關(guān)聯(lián)的通道,讀就緒、寫就緒,連接就緒。
所以,當(dāng)我們注冊(cè)一個(gè)通道到選擇器之后,就可以通過返回的 SelectionKey 實(shí)例監(jiān)聽該通道的各種事件。
當(dāng)然,一旦某個(gè)選擇器中注冊(cè)了多個(gè)通道,我們不可能一個(gè)一個(gè)的記錄它們注冊(cè)時(shí)返回的 SelectionKey 實(shí)例來監(jiān)聽通道事件,選擇器應(yīng)當(dāng)有方法返回所有注冊(cè)成功的通道相關(guān)的 SelectionKey 實(shí)例。
Set<SelectionKey> keys = selector.selectedKeys();
selectedKeys 方法會(huì)返回選擇器中注冊(cè)成功的所有通道的 SelectionKey 實(shí)例集合。我們通過這個(gè)集合的 SelectionKey 實(shí)例,可以得到所有通道的事件就緒情況并進(jìn)行相應(yīng)的處理操作。
總結(jié)
優(yōu)點(diǎn)
1個(gè)線程就行就能處理所有連接,這個(gè)線程不停循環(huán)遍歷就行了。
缺點(diǎn)
單線程不停循環(huán)發(fā)起系統(tǒng)調(diào)用,一樣會(huì)耗盡 CPU 資源。
NIO 的瓶頸
在于需要不停的調(diào)起系統(tǒng)調(diào)用,每個(gè)鏈接我們都要調(diào)系統(tǒng)調(diào)用詢問是否有過來數(shù)據(jù),我們要是明確的知道哪個(gè)連接有數(shù)據(jù)包過來呢,就不用挨個(gè)遍歷尋找找答案了。
到此這篇關(guān)于Java中IO的NIO通道解析的文章就介紹到這了,更多相關(guān)NIO通道解析內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)文件上傳的兩種方法(uploadify和Spring)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)文件上傳的兩種方法,uploadify和Spring實(shí)現(xiàn)文件上傳,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
springboot?@PostConstruct無效的解決
這篇文章主要介紹了springboot?@PostConstruct無效的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
很多人竟然不知道Java線程池的創(chuàng)建方式有7種
本文主要介紹了很多人竟然不知道Java線程池的創(chuàng)建方式有7種,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07
SpringBoot整合SpringSecurity實(shí)現(xiàn)權(quán)限控制之實(shí)現(xiàn)多標(biāo)簽頁
這篇文章主要介紹了SpringBoot整合SpringSecurity實(shí)現(xiàn)權(quán)限控制之實(shí)現(xiàn)多標(biāo)簽頁,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11
詳解hibernate雙向多對(duì)多關(guān)聯(lián)映射XML與注解版
本篇文章主要介紹了詳解hibernate雙向多對(duì)多關(guān)聯(lián)映射XML與注解版,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
Springboot設(shè)置文件上傳大小限制的實(shí)現(xiàn)示例
Spring Boot工程嵌入的tomcat限制了請(qǐng)求的文件大小默認(rèn)為1MB,單次請(qǐng)求的文件的總數(shù)不能大于10Mb,本文主要介紹了Springboot設(shè)置文件上傳大小限制的實(shí)現(xiàn)示例,感興趣的可以了解一下2023-11-11
Spring Cloud Gateway重試機(jī)制原理解析
這篇文章主要介紹了Spring Cloud Gateway重試機(jī)制原理解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
SpringBoot集成Redis實(shí)現(xiàn)訂單超時(shí)管理的詳細(xì)步驟
文章描述了如何使用Redis來處理超時(shí)支付訂單,包括引入依賴、配置連接、定義實(shí)體類、實(shí)現(xiàn)服務(wù)和監(jiān)聽器等步驟,文章討論了優(yōu)化方案、注意事項(xiàng)以及Redis的性能和可靠性問題,感興趣的朋友跟隨小編一起看看吧2026-03-03
SpringBoot中使用Zookeeper實(shí)現(xiàn)分布式鎖的案例
本文主要介紹了SpringBoot中使用Zookeeper實(shí)現(xiàn)分布式鎖的案例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01
SpringBoot集成Sharding-JDBC實(shí)現(xiàn)分庫分表方式
這篇文章主要介紹了SpringBoot集成Sharding-JDBC實(shí)現(xiàn)分庫分表方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07

