柚子快報(bào)激活碼778899分享:java NIO編程
柚子快報(bào)激活碼778899分享:java NIO編程
目錄
1、什么是NIO編程?
為什么說(shuō)Java NIO是非阻塞的?
2、Java NIO 通道(Channel)詳解
如何獲取Channel對(duì)象?
3、Java NIO 緩沖區(qū)(Buffer)詳解
(1)獲取緩沖區(qū)對(duì)象
(2)將數(shù)據(jù)寫入Buffer以及從Buffer讀取數(shù)據(jù)
(3)通道Channel和緩沖區(qū)Buffer的相互讀寫
(4)圖解緩沖區(qū)Buffer
4、Java NIO 選擇器/Selector詳解
(1)將channel注冊(cè)到Selector上
(2)為什么說(shuō) select() 方法是阻塞的?
(3)Selector 的 selectedKeys() 方法
5、為什么說(shuō)Java NIO是事件驅(qū)動(dòng)的?
6、NIO編程完整的代碼示例
1、什么是NIO編程?
????????NIO(New I/O)是Java中一種提供了非阻塞式I/O操作的編程模型。它引入了一組新的Java類,用于取代傳統(tǒng)的Java I/O類(如InputStream和OutputStream),以提供更高效、更靈活的I/O操作。//?部分NIO API實(shí)際上是阻塞的,例如File API
????????Java NIO允許執(zhí)行非阻塞的IO。例如,線程可以請(qǐng)求通道將數(shù)據(jù)讀入緩沖區(qū)。當(dāng)通道將數(shù)據(jù)讀入緩沖區(qū)時(shí),線程可以做其他事情。一旦數(shù)據(jù)被讀入緩沖區(qū),線程就可以繼續(xù)處理它。將數(shù)據(jù)寫入通道也是如此。// 非阻塞的核心在于選擇器
????????NIO的核心組件包括以下幾個(gè)方面:
通道(Channel):通道是數(shù)據(jù)傳輸?shù)妮d體,類似于傳統(tǒng)的流(Stream)。不同的是,通道可以同時(shí)支持讀和寫操作,而流一般只能單向傳輸數(shù)據(jù)。通道可以連接到文件、套接字等不同的I/O源。//數(shù)據(jù)總是從通道讀入緩沖區(qū),或從緩沖區(qū)寫入通道緩沖區(qū)(Buffer):緩沖區(qū)是NIO中用于存儲(chǔ)數(shù)據(jù)的對(duì)象。數(shù)據(jù)從通道讀入緩沖區(qū),或者從緩沖區(qū)寫入通道。緩沖區(qū)可以提供更高效的數(shù)據(jù)處理,因?yàn)樗梢耘孔x寫數(shù)據(jù),而不需要逐個(gè)字節(jié)地操作。選擇器(Selector):選擇器是NIO的一個(gè)關(guān)鍵概念,用于實(shí)現(xiàn)多路復(fù)用。它允許單個(gè)線程監(jiān)視多個(gè)通道的I/O事件,從而提高系統(tǒng)資源的利用率。選擇器會(huì)不斷輪詢注冊(cè)在其上的通道,一旦某個(gè)通道就緒(有數(shù)據(jù)可讀或可寫),就會(huì)通知程序進(jìn)行相應(yīng)的處理。
????????下面是一個(gè)Thread使用Selector處理3個(gè)Channel的示例:
????????要使用Selector,首先需要注冊(cè)Channel,然后調(diào)用它的select()方法。select()方法將阻塞,直到為其中一個(gè)已注冊(cè)通道準(zhǔn)備好事件(Event)為止。select()方法返回后,線程就可以處理這些事件。
為什么說(shuō)Java NIO是非阻塞的?
????????Java NIO提供了一種非阻塞的I/O操作模型,相比傳統(tǒng)的阻塞式I/O模型,Java NIO允許應(yīng)用程序在進(jìn)行I/O操作時(shí)不需要等待數(shù)據(jù)的到達(dá)或發(fā)送完成,而可以繼續(xù)執(zhí)行其他任務(wù)。
????????在傳統(tǒng)的阻塞式I/O模型中,當(dāng)一個(gè)線程執(zhí)行一個(gè)I/O操作(如讀取文件或網(wǎng)絡(luò)數(shù)據(jù))時(shí),它將被阻塞,直到操作完成或發(fā)生錯(cuò)誤。這意味著在阻塞模型下,一個(gè)線程只能處理一個(gè)I/O操作,如果有多個(gè)I/O操作需要處理,就需要使用多個(gè)線程,增加了線程的開(kāi)銷和管理復(fù)雜性。
????????而Java NIO使用了非阻塞的I/O操作模型。它引入了Channel(通道)和Selector(選擇器)的概念,使得應(yīng)用程序可以注冊(cè)多個(gè)Channel到一個(gè)Selector上,并通過(guò)Selector來(lái)監(jiān)控這些Channel的狀態(tài)。Selector可以輪詢已注冊(cè)的Channel,當(dāng)某個(gè)Channel滿足I/O事件時(shí)(如可讀、可寫等),就會(huì)通知應(yīng)用程序進(jìn)行相應(yīng)的處理。
????????在非阻塞模型下,一個(gè)線程可以同時(shí)處理多個(gè)Channel的I/O操作,而不需要為每個(gè)Channel創(chuàng)建一個(gè)單獨(dú)的線程。這種方式可以大大提高系統(tǒng)的并發(fā)性能和資源利用率。
????????此外,Java NIO還提供了基于事件驅(qū)動(dòng)的異步I/O操作模型(AIO,Asynchronous I/O),通過(guò)使用Future和回調(diào)機(jī)制,允許應(yīng)用程序在進(jìn)行I/O操作時(shí)不需要主動(dòng)等待操作完成,而是在操作完成后由操作系統(tǒng)通知應(yīng)用程序。// NIO2
2、Java NIO 通道(Channel)詳解
????????Java NIO通道類似于流,但有一些不同:
可以對(duì)通道進(jìn)行讀和寫操作。流通常是單向的(讀或?qū)?。// 通道可以讀和寫通道可以異步讀寫。通道總是向緩沖區(qū)讀或從緩沖區(qū)寫。
????????如上所述,數(shù)據(jù)總是從通道讀入緩沖區(qū),或者從緩沖區(qū)寫入通道:
????????下面是Java NIO中最重要的Channel實(shí)現(xiàn):
FileChannel:從文件中讀取和寫入數(shù)據(jù)。DatagramChannel:可以通過(guò)UDP在網(wǎng)絡(luò)上讀寫數(shù)據(jù)。SocketChannel:可以通過(guò)TCP在網(wǎng)絡(luò)上讀寫數(shù)據(jù)。ServerSocketChannel:允許偵聽(tīng)傳入的TCP連接,如web服務(wù)器一樣。該Channel會(huì)為每個(gè)傳入連接創(chuàng)建一個(gè)SocketChannel。
如何獲取Channel對(duì)象?
????????在Java NIO中,可以通過(guò)以下幾種方式來(lái)獲取Channel對(duì)象:
????????(1)文件通道(FileChannel):通過(guò)文件輸入流(FileInputStream)或文件輸出流(FileOutputStream)獲取文件通道。示例代碼如下:
FileInputStream fis = new FileInputStream("path/to/file");
FileChannel fileChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("path/to/file");
FileChannel fileChannel = fos.getChannel();
????????(2)網(wǎng)絡(luò)通道(SocketChannel、ServerSocketChannel、DatagramChannel):通過(guò)Java NIO提供的網(wǎng)絡(luò)編程類獲取網(wǎng)絡(luò)通道。示例代碼如下:
SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
DatagramChannel datagramChannel = DatagramChannel.open();
????????(3)管道(Pipe):通過(guò)管道獲取通道。管道用于在兩個(gè)線程之間進(jìn)行通信,可以使用Pipe.open()方法打開(kāi)一個(gè)管道,并通過(guò)source()和sink()方法獲取通道。示例代碼如下:
Pipe pipe = Pipe.open();
Pipe.SinkChannel sinkChannel = pipe.sink();
Pipe.SourceChannel sourceChannel = pipe.source();
????????這些方法返回的Channel對(duì)象可以用于讀取或?qū)懭霐?shù)據(jù),執(zhí)行I/O操作。
????????另外,需要注意的是,以上方法獲取的Channel對(duì)象都是阻塞式的。如果想要使用非阻塞模式,可以通過(guò)調(diào)用configureBlocking(false)方法將通道設(shè)置為非阻塞模式,然后可以使用Selector來(lái)進(jìn)行非阻塞I/O操作。//?FileChannel無(wú)此方法,所以它總是阻塞的
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
????????這樣設(shè)置后,可以通過(guò)Selector來(lái)監(jiān)控這些非阻塞通道的狀態(tài),并進(jìn)行相應(yīng)的I/O操作。
????????下面是一個(gè)使用FileChannel復(fù)制文件的基本示例:// 通道+緩沖區(qū)+阻塞式
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
public class FileCopyExample {
public static void main(String[] args) {
try {
FileInputStream sourceFile = new FileInputStream("F:\\source.txt");
FileOutputStream destinationFile = new FileOutputStream("F:\\destination.txt");
FileChannel sourceChannel = sourceFile.getChannel();
FileChannel destinationChannel = destinationFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (sourceChannel.read(buffer) != -1) {
// 翻轉(zhuǎn)緩沖區(qū),為通道寫入序列做準(zhǔn)備:limit設(shè)置為當(dāng)前position的值,position設(shè)置為0,mark設(shè)置為-1(廢棄)
buffer.flip();
destinationChannel.write(buffer);
// 在放置數(shù)據(jù)之前清空緩存,為通道讀寫入序列做準(zhǔn)備:limit設(shè)置為capacity的值,position設(shè)置為0,mark設(shè)置為-1(廢棄):
buffer.clear();
}
sourceChannel.close();
destinationChannel.close();
sourceFile.close();
destinationFile.close();
System.out.println("File copied successfully.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、Java NIO 緩沖區(qū)(Buffer)詳解
????????緩沖區(qū)本質(zhì)上是一塊內(nèi)存,你可以向其中寫入數(shù)據(jù),然后可以再次讀取數(shù)據(jù)。該內(nèi)存塊封裝在NIO Buffer對(duì)象中,該對(duì)象提供了一組方法,讓使用內(nèi)存塊變得更容易。
????????Java NIO附帶了以下緩沖區(qū)類型:
ByteBuffer(字節(jié)緩沖區(qū)): ByteBuffer是最常用的緩沖區(qū)類型之一,它可以存儲(chǔ)字節(jié)數(shù)據(jù)。ByteBuffer可以分為直接緩沖區(qū)和非直接緩沖區(qū)。直接緩沖區(qū)使用操作系統(tǒng)的本地內(nèi)存,可以提高I/O操作的性能。CharBuffer(字符緩沖區(qū)): CharBuffer用于存儲(chǔ)字符數(shù)據(jù),它是基于16位Unicode字符的緩沖區(qū)。CharBuffer提供了一些方便的方法來(lái)處理字符數(shù)據(jù)。ShortBuffer、IntBuffer、LongBuffer(短整型、整型和長(zhǎng)整型緩沖區(qū)): 這些緩沖區(qū)類型分別用于存儲(chǔ)短整型、整型和長(zhǎng)整型數(shù)據(jù)。它們提供了特定于數(shù)據(jù)類型的方法和屬性。FloatBuffer、DoubleBuffer(浮點(diǎn)型和雙精度浮點(diǎn)型緩沖區(qū)): 這些緩沖區(qū)類型用于存儲(chǔ)浮點(diǎn)型和雙精度浮點(diǎn)型數(shù)據(jù)。它們也提供了特定于數(shù)據(jù)類型的方法和屬性。
(1)獲取緩沖區(qū)對(duì)象
????????在Java NIO中,可以通過(guò)以下方式來(lái)獲取Buffer對(duì)象:
????????1)使用分配方法(Allocation Methods):每個(gè)緩沖區(qū)類(如ByteBuffer、CharBuffer、IntBuffer等)都提供了allocate()方法來(lái)分配一個(gè)新的緩沖區(qū)對(duì)象。示例代碼如下:
ByteBuffer buffer = ByteBuffer.allocate(1024);
CharBuffer buffer = CharBuffer.allocate(1024);
????????這種方式會(huì)分配一個(gè)指定容量的緩沖區(qū)對(duì)象,可以根據(jù)需要進(jìn)行讀寫操作。
????????2)使用包裝方法(Wrapping Methods):除了分配方法,緩沖區(qū)類還提供了wrap()方法,用于包裝現(xiàn)有的數(shù)組來(lái)創(chuàng)建緩沖區(qū)對(duì)象。示例代碼如下:
byte[] byteArray = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(byteArray);
char[] charArray = new char[1024];
CharBuffer buffer = CharBuffer.wrap(charArray);
????????這種方式會(huì)使用現(xiàn)有的數(shù)組作為緩沖區(qū)的數(shù)據(jù)存儲(chǔ)。
????????3)使用視圖方法(View Buffer):緩沖區(qū)類還提供了一些視圖方法,可以創(chuàng)建基于現(xiàn)有緩沖區(qū)的新緩沖區(qū)對(duì)象。這些視圖緩沖區(qū)共享底層緩沖區(qū)的數(shù)據(jù),并根據(jù)視圖的不同提供不同類型的訪問(wèn)方式。常見(jiàn)的視圖緩沖區(qū)有ByteBuffer的子類:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer等。示例代碼如下:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
IntBuffer intBuffer = byteBuffer.asIntBuffer();
????????這種方式可以根據(jù)需要?jiǎng)?chuàng)建適合特定數(shù)據(jù)類型的緩沖區(qū)。
(2)將數(shù)據(jù)寫入Buffer以及從Buffer讀取數(shù)據(jù)
????????1)將數(shù)據(jù)寫入Buffer
????????在Java NIO中,有多種方式可以將數(shù)據(jù)寫入Buffer對(duì)象。以下是幾種常用的方式:
????????使用put()方法:Buffer類提供了多個(gè)put()方法的重載形式,可以根據(jù)不同數(shù)據(jù)類型來(lái)寫入數(shù)據(jù)。例如:
put(byte b):將單個(gè)字節(jié)寫入Buffer。put(byte[] array):將字節(jié)數(shù)組的內(nèi)容寫入Buffer。put(ByteBuffer src):將另一個(gè)ByteBuffer的內(nèi)容寫入當(dāng)前Buffer。putXxx()方法(如putChar(char value)、putInt(int value)等):根據(jù)數(shù)據(jù)類型將特定類型的數(shù)據(jù)寫入Buffer。
? ? ? ? 使用通道(Channel):可以使用通道的read()方法將數(shù)據(jù)直接寫入Buffer。例如:
int bytesRead = inChannel.read(buf); //read into buffer.
????????2)從Buffer讀取數(shù)據(jù)
????????從Buffer讀取數(shù)據(jù)的兩種方式:使用get()方法和通道(Channel)
????????使用get()方法從緩沖區(qū)讀取數(shù)據(jù):
byte aByte = buf.get();
????????還有許多其他版本的get()方法,允許以許多不同的方式從Buffer讀取數(shù)據(jù)。例如,在特定位置讀取,或者從緩沖區(qū)讀取一個(gè)字節(jié)數(shù)組的數(shù)據(jù)等。
????????將數(shù)據(jù)從緩沖區(qū)讀入通道(Channel):可以使用通道的write()方法將數(shù)據(jù)從Buffer讀入通道。例如:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
(3)通道Channel和緩沖區(qū)Buffer的相互讀寫
????????1)把數(shù)據(jù)從Channel寫入Buffer:channel.read(buffer)
// 創(chuàng)建一個(gè)FileChannel
FileChannel channel = new FileInputStream("path/to/file").getChannel();
// 創(chuàng)建一個(gè)ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 將數(shù)據(jù)從Channel讀取到Buffer中
int bytesRead = channel.read(buffer);
????????2)把數(shù)據(jù)從Buffer寫入Channel:channel.write(buffer)
// 創(chuàng)建一個(gè)FileChannel
FileChannel channel = new FileOutputStream("path/to/file").getChannel();
// 創(chuàng)建一個(gè)ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 將數(shù)據(jù)寫入Buffer
buffer.put("Hello, World!".getBytes());
buffer.flip(); // 切換到讀模式
// 將Buffer中的數(shù)據(jù)寫入Channel
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// 關(guān)閉Channel
channel.close();
(4)圖解緩沖區(qū)Buffer
????????1)緩沖區(qū)的創(chuàng)建
創(chuàng)建一個(gè)緩沖區(qū):ByteBuffer allocate = ByteBuffer.allocate(10);然后往緩沖區(qū)中添加數(shù)據(jù):allocate.put("ABC".getBytes());
容量(Capacity):Buffer的容量表示它所能夠存儲(chǔ)的數(shù)據(jù)元素的最大數(shù)量。一旦Buffer被分配,它的容量就是固定的,不能更改。//內(nèi)存塊位置(Position):Buffer的位置表示下一個(gè)要讀取或?qū)懭氲脑氐乃饕?。初始時(shí),位置為0,并且在讀取或?qū)懭氩僮骱髸?huì)自動(dòng)更新。限制(Limit):Buffer的限制表示在讀取或?qū)懭氩僮髦?,最多可以讀取或?qū)懭氲脑財(cái)?shù)量。初始時(shí),限制與容量相等(limit = capacity),并且在讀取或?qū)懭氩僮骱罂梢允謩?dòng)設(shè)置。
????????示例代碼:
public static void main(String[] args) throws IOException {
//1、創(chuàng)建一個(gè)緩沖區(qū)
ByteBuffer allocate = ByteBuffer.allocate(10);
System.out.println(allocate.position());//0 獲取當(dāng)前索引所在位置
System.out.println(allocate.limit());//10 最多能操作到哪個(gè)索引位置
System.out.println(allocate.capacity());//10 返回緩沖區(qū)總長(zhǎng)度
System.out.println(allocate.remaining());//10 還有多少個(gè)可以操作的個(gè)數(shù):limit - position
System.out.println("———————————————————");
//2、添加3個(gè)字節(jié)
allocate.put("ABC".getBytes());
System.out.println(allocate.position());//3 獲取當(dāng)前索引所在位置
System.out.println(allocate.limit());//10 最多能操作到哪個(gè)索引位置
System.out.println(allocate.capacity());//10 返回緩沖區(qū)總長(zhǎng)度
System.out.println(allocate.remaining());//7 還有多少個(gè)可以操作的個(gè)數(shù)
}
????????2)緩沖區(qū)的讀寫模式轉(zhuǎn)換
????????寫模式切換到讀模式:flip()方法
????????flip()方法:用于將Buffer從寫模式切換到讀模式。flip()方法的作用是設(shè)置limit為當(dāng)前位置,然后將position重置為0,以便在讀取數(shù)據(jù)之前將Buffer準(zhǔn)備好。
????????當(dāng)將數(shù)據(jù)寫入Buffer時(shí),Buffer會(huì)跟蹤寫入了多少數(shù)據(jù)。一旦需要從Buffer讀取數(shù)據(jù),就需要調(diào)用flip()方法將Buffer從寫入模式切換到讀取模式。在讀取模式下,緩沖區(qū)允許讀取寫入緩沖區(qū)的所有數(shù)據(jù)。// 作用:將緩沖區(qū)從寫入模式切換到讀取模式,讀取緩沖區(qū)中的數(shù)據(jù)
????????讀模式切換到寫模式:clear()和compact()方法
????????clear()和compact():一旦從Buffer中讀取了所有數(shù)據(jù),就需要清除緩沖區(qū),以便為再次寫入做好準(zhǔn)備。調(diào)用clear()或compact()方法可以達(dá)到清除緩沖區(qū)的效果。clear()方法會(huì)清除整個(gè)緩沖區(qū),而compact()方法只會(huì)清除已經(jīng)讀取的數(shù)據(jù),未讀的數(shù)據(jù)都會(huì)被移到緩沖區(qū)的開(kāi)頭,新進(jìn)的數(shù)據(jù)將在未讀數(shù)據(jù)之后進(jìn)行寫入。//?clear()和compact() 并不會(huì)真正清除數(shù)據(jù),只是修改了相關(guān)位置數(shù)據(jù)的指針
????????切換到寫模式:clear()方法將Buffer從讀模式切換到寫模式,重置位置position為0,限制limit設(shè)置為容量capacity,以便重新寫入數(shù)據(jù)。
????????清空數(shù)據(jù):clear()方法不會(huì)清除緩沖區(qū)的數(shù)據(jù),而是將緩沖區(qū)標(biāo)記為可重寫狀態(tài),之前寫入的數(shù)據(jù)仍然存在,但是在寫模式下可以覆蓋它們。
4、Java NIO 選擇器/Selector詳解
????????在Java NIO中,Selector是一個(gè)可用于多路復(fù)用(Multiplexing)I/O操作的關(guān)鍵組件。它允許單個(gè)線程同時(shí)管理多個(gè)通道(Channels),監(jiān)控這些通道的I/O事件(如讀就緒、寫就緒等),并且在有就緒事件發(fā)生時(shí)進(jìn)行響應(yīng)。
????????以下是關(guān)于Selector的一些重要概念和使用方式:
????????1)創(chuàng)建和獲取Selector對(duì)象:
創(chuàng)建:可以通過(guò)Selector.open()方法創(chuàng)建一個(gè)新的Selector對(duì)象。獲?。嚎梢酝ㄟ^(guò)SelectableChannel對(duì)象的selector()方法獲取與之關(guān)聯(lián)的Selector對(duì)象。
? ? ? ? 2)注冊(cè)通道到Selector:
通道必須是可選擇的(SelectableChannel),如SocketChannel、ServerSocketChannel、DatagramChannel等。通過(guò)調(diào)用通道的register(Selector selector, int interestOps)方法將通道注冊(cè)到Selector上。interestOps參數(shù)表示感興趣的事件類型,可使用SelectionKey類的常量來(lái)指定,如SelectionKey.OP_READ、SelectionKey.OP_WRITE等。
????????3)選擇操作:
調(diào)用Selector的select()方法進(jìn)行選擇操作,它會(huì)阻塞直到至少有一個(gè)注冊(cè)的通道就緒或被中斷。返回值表示有多少個(gè)通道已經(jīng)就緒,可以通過(guò)Selector的selectedKeys()方法獲取就緒的SelectionKey集合。
????????4)處理就緒事件:
通過(guò)遍歷selectedKeys()方法返回的就緒SelectionKey集合,可以獲取每個(gè)就緒通道的相關(guān)信息??梢允褂肧electionKey的channel()方法獲取就緒通道,readyOps()方法獲取就緒的操作類型??梢愿鶕?jù)就緒的操作類型執(zhí)行相應(yīng)的處理邏輯。
????????5)取消注冊(cè)和關(guān)閉:
調(diào)用SelectionKey的cancel()方法可以取消通道的注冊(cè)。調(diào)用Selector的close()方法可以關(guān)閉Selector對(duì)象。
????????使用Selector可以在單個(gè)線程中同時(shí)處理多個(gè)通道的I/O操作,避免了為每個(gè)通道分配一個(gè)線程的開(kāi)銷。這種方式常用于需要同時(shí)管理多個(gè)通道的高并發(fā)應(yīng)用場(chǎng)景,如網(wǎng)絡(luò)服務(wù)器。
????????需要注意的是,Selector是基于事件驅(qū)動(dòng)的,所以在處理就緒事件時(shí)需要注意及時(shí)響應(yīng)并處理,否則可能會(huì)錯(cuò)過(guò)事件。
(1)將channel注冊(cè)到Selector上
????????使用Selector,必須首先向選擇器注冊(cè)通道。向選擇器注冊(cè)通道可以使用Channel.register()方法進(jìn)行注冊(cè),如下所示:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
????????注意:Channel必須處于非阻塞模式才能與Selector一起使用。所以不能在選擇器中使用FileChannel,因?yàn)镕ileChannel不能切換到非阻塞模式。// 所以一般文件的讀寫還是使用的BIO
????????向Selector注冊(cè)channel,也是由操作系統(tǒng)完成的,也調(diào)用了底層操作系統(tǒng)的相關(guān)實(shí)現(xiàn),這是因?yàn)镾elector的實(shí)現(xiàn)機(jī)制基于操作系統(tǒng)提供的底層I/O多路復(fù)用機(jī)制實(shí)現(xiàn)的。
? ? ? ? 通過(guò)register()方法注冊(cè)channel時(shí),需要指定偵聽(tīng)的 I/O 事件類型(讀、寫等)。選擇器可以偵聽(tīng)四種不同的事件:Connect、Accept、Read、Write,具體如下所示:
????????SelectionKey中定義的4種事件: // Java NIO基于事件驅(qū)動(dòng)
SelectionKey.OP_ACCEPT:接收連接繼續(xù)事件,表示服務(wù)器監(jiān)聽(tīng)到了客戶連接,服務(wù)器可以接收這個(gè)連接了SelectionKey.OP_CONNECT:連接就緒事件,表示客戶端與服務(wù)器的連接已經(jīng)建立成功SelectionKey.OP_READ:讀就緒事件,表示通道中已經(jīng)有了可讀的數(shù)據(jù),可以執(zhí)行讀操作了(通道目前有數(shù)據(jù),可以進(jìn)行讀操作了)SelectionKey.OP_WRITE:寫就緒事件,表示已經(jīng)可以向通道寫數(shù)據(jù)了(通道目前可以用于寫操作)
(2)為什么說(shuō) select() 方法是阻塞的?
????????select()方法是阻塞的主要原因是它的實(shí)現(xiàn)機(jī)制基于操作系統(tǒng)提供的底層I/O多路復(fù)用機(jī)制,例如Linux上的select()系統(tǒng)調(diào)用或Windows上的select()函數(shù)。這些底層機(jī)制都是阻塞的,因此select()方法在調(diào)用底層機(jī)制時(shí)也會(huì)阻塞。//?select()方法是由操作系統(tǒng)實(shí)現(xiàn)的
? ? ? ? 看下源碼,點(diǎn)擊Selector.select()方法,進(jìn)入Selector的實(shí)現(xiàn)類SelectorImpl,它會(huì)調(diào)用一個(gè)lockAndDoSelect()方法:// 基于 java 8
public int select(long var1) throws IOException {
if (var1 < 0L) {
throw new IllegalArgumentException("Negative timeout");
} else {
return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
}
}
????????進(jìn)入lockAndDoSelect()方法后,在該方法中會(huì)調(diào)用SelectorImpl.doSelect()方法。
protected abstract int doSelect(long var1) throws IOException;
private int lockAndDoSelect(long var1) throws IOException {
synchronized(this) {
if (!this.isOpen()) {
throw new ClosedSelectorException();
} else {
int var10000;
synchronized(this.publicKeys) {
synchronized(this.publicSelectedKeys) {
var10000 = this.doSelect(var1);
}
}
return var10000;
}
}
}
????????SelectorImpl.doSelect()方法是一個(gè)抽象方法,既然是抽象方法就會(huì)有對(duì)應(yīng)的實(shí)現(xiàn)。繼續(xù)跟進(jìn)到該方法的實(shí)現(xiàn)類WindowsSelectorImpl.doSelect()方法中,其中有一行代碼:this.subSelector.poll(),這行代碼就是去調(diào)用操作系統(tǒng)的底層實(shí)現(xiàn)機(jī)制了。
protected int doSelect(long var1) throws IOException {
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else {
// 省略...
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else {
// 省略...
try {
this.begin();
try {
// 在這里調(diào)用系統(tǒng)的select()方法
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
// 省略...
}
}
}
????????WindowsSelectorImpl是Java NIO庫(kù)中用于Windows平臺(tái)上的Selector的具體實(shí)現(xiàn)類。它實(shí)現(xiàn)了Selector接口,并提供了在Windows操作系統(tǒng)上使用I/O多路復(fù)用機(jī)制的功能。??
? ? ? 繼續(xù)跟進(jìn)this.subSelector.poll()方法,可以看到它調(diào)用了一個(gè)本地方法:// 該方法阻塞
private int poll() throws IOException {
return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout);
}
// 本地方法
private native int poll0(long var1, int var3, int[] var4, int[] var5, int[] var6, long var7);
????????this.subSelector對(duì)象
private final WindowsSelectorImpl.SubSelector subSelector = new WindowsSelectorImpl.SubSelector();
????????在WindowsSelectorImpl類中,SubSelector是一個(gè)內(nèi)部類,用于管理特定事件類型的I/O事件。SubSelector繼承自AbstractPollArrayWrapper類,用于封裝I/O事件的輪詢和處理。
????????SubSelector類中的poll()方法是用于執(zhí)行對(duì)已注冊(cè)通道的輪詢操作,并返回就緒的通道數(shù)量。
(3)Selector 的 selectedKeys() 方法
????????一旦調(diào)用了select()方法,并且它的返回值表明一個(gè)或多個(gè)通道已經(jīng)準(zhǔn)備就緒,接下來(lái)就可以調(diào)用選擇器的selectedKeys()方法,獲取已經(jīng)準(zhǔn)備就緒的通道集。:// 這個(gè)步驟還挺難理解的,先執(zhí)行select()方法,然后再執(zhí)行selectedKeys()方法
????????在Java NIO中,Selector的selectedKeys()方法用于獲取當(dāng)前已選擇鍵集合(selected key set)。
public Set
if (!this.isOpen() && !Util.atBugLevel("1.4")) {
throw new ClosedSelectorException();
} else {
return this.publicSelectedKeys;
}
}
????????該方法返回一個(gè)Set
????????需要注意的是,selectedKeys()方法返回的是一個(gè)可變的集合,即可以直接在返回的集合上進(jìn)行增刪操作。這意味著在處理完就緒事件后,需要手動(dòng)從集合中移除相應(yīng)的SelectionKey,以便下次的選擇操作能正確更新就緒狀態(tài)。// 先select(),后執(zhí)行selectedKeys()
????????在使用selectedKeys()方法時(shí),一般會(huì)結(jié)合Iterator來(lái)遍歷集合,并逐個(gè)處理就緒的SelectionKey。示例如下:
Set
Iterator
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
????????在上述該循環(huán)中,會(huì)循環(huán)判斷每一個(gè)SelectionKey引用的通道是否已經(jīng)準(zhǔn)備就緒。
????????在每次迭代結(jié)束時(shí)都會(huì)調(diào)用keyIterator.remove(),這是因?yàn)檫x擇器自己不會(huì)從鍵集中刪除SelectionKey的實(shí)例。所以,當(dāng)你完成對(duì)該通道的處理時(shí),就需要手動(dòng)去移除它。當(dāng)通道再次準(zhǔn)備就緒時(shí),選擇器會(huì)再次將這個(gè)鍵添加到所選的鍵集中。// 手動(dòng)移除鍵
????????另外,在處理具體的通道時(shí),SelectionKey.channel()方法返回的通道應(yīng)該被強(qiáng)制轉(zhuǎn)換為需要使用的通道類型,例如ServerSocketChannel或SocketChannel等。
SocketChannel sc = (SocketChannel) key.channel();
5、為什么說(shuō)Java NIO是事件驅(qū)動(dòng)的?
????????Java NIO(New I/O)是事件驅(qū)動(dòng)的,主要基于以下兩個(gè)核心組件:選擇器(Selector)和選擇鍵(SelectionKey)。
選擇器(Selector):選擇器是Java NIO中的核心組件之一。它允許單線程管理多個(gè)通道(如SocketChannel、ServerSocketChannel等),并根據(jù)通道的就緒狀態(tài)進(jìn)行事件驅(qū)動(dòng)的操作。選擇器可以在通道上注冊(cè)想要偵聽(tīng)的I/O事件(如讀、寫等),并在事件就緒時(shí)通知應(yīng)用程序。選擇鍵(SelectionKey):選擇鍵是通道在選擇器上的注冊(cè)信息。當(dāng)通道注冊(cè)到選擇器上時(shí),將創(chuàng)建一個(gè)選擇鍵來(lái)表示該注冊(cè)關(guān)系。選擇鍵包含了通道、選擇器以及想要偵聽(tīng)的I/O事件類型。通過(guò)選擇鍵,可以獲取和修改想要偵聽(tīng)的事件類型,并獲取就緒狀態(tài)的通道。
????????Java NIO的事件驅(qū)動(dòng)模型基于以下原理:
注冊(cè):應(yīng)用程序?qū)⑼ǖ雷?cè)到選擇器上,并指定想要偵聽(tīng)的I/O事件類型(如讀、寫等)。選擇:選擇器通過(guò)調(diào)用select()方法進(jìn)行阻塞,等待就緒的通道。當(dāng)有通道就緒時(shí),select()方法會(huì)返回就緒通道的數(shù)量,并可以通過(guò)selectedKeys()方法獲取到就緒通道的選擇鍵集合。就緒通知:選擇器在某個(gè)通道就緒時(shí),會(huì)將相應(yīng)的選擇鍵添加到選擇鍵集合中。應(yīng)用程序可以通過(guò)遍歷選擇鍵集合來(lái)獲取就緒的通道和感興趣的事件類型。事件處理:應(yīng)用程序可以根據(jù)就緒的通道和感興趣的事件類型,執(zhí)行相應(yīng)的操作,如讀取數(shù)據(jù)、寫入數(shù)據(jù)、接受連接等。處理完事件后,可以取消或修改通道的注冊(cè)信息。
????????Java NIO的事件驅(qū)動(dòng)模型允許應(yīng)用程序在單線程中同時(shí)處理多個(gè)通道的I/O操作,提高了系統(tǒng)的并發(fā)性和可擴(kuò)展性。相比于傳統(tǒng)的阻塞I/O模型,事件驅(qū)動(dòng)模型減少了線程的創(chuàng)建和管理開(kāi)銷,提高了系統(tǒng)的效率和響應(yīng)能力。
// 在Java NIO中,Selector并不會(huì)不斷輪詢注冊(cè)在其上的所有通道。實(shí)際上,Selector使用的是操作系統(tǒng)提供的事件通知機(jī)制,如epoll、kqueue等,來(lái)監(jiān)聽(tīng)和等待就緒的通道。
// 驗(yàn)證操作系統(tǒng)提供的事件通知機(jī)制,待續(xù)...
6、NIO編程完整的代碼示例
????????客戶端代碼:// 以下代碼可直接運(yùn)行
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;
import java.util.UUID;
public class NioClient {
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 8001;
Selector selector = null;
SocketChannel socketChannel = null;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 非阻塞
// 如果直接連接成功,則注冊(cè)到多路復(fù)用器上,發(fā)送請(qǐng)求消息,讀應(yīng)答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (true) {
try {
selector.select(1000);
Set
Iterator
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
//處理每一個(gè)channel
handleInput(selector, key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 多路復(fù)用器關(guān)閉后,所有注冊(cè)在上面的Channel資源都會(huì)被自動(dòng)去注冊(cè)并關(guān)閉
// if (selector != null)
// try {
// selector.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
//
// }
}
public static void doWrite(SocketChannel sc) throws IOException {
byte[] str = UUID.randomUUID().toString().getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(str.length);
writeBuffer.put(str);
writeBuffer.flip();
sc.write(writeBuffer);
}
public static void handleInput(Selector selector, SelectionKey key) throws Exception {
if (key.isValid()) {
// 判斷是否連接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Server said : " + body);
} else if (readBytes < 0) {
// 對(duì)端鏈路關(guān)閉
key.cancel();
sc.close();
} else {
; // 讀到0字節(jié),忽略
}
}
Thread.sleep(3000);
doWrite(sc);
}
}
}
????????服務(wù)端代碼:
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
int port = 8001;
Selector selector = null;
ServerSocketChannel servChannel = null;
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服務(wù)器在8001端口守候");
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (true) {
try {
selector.select(1000);
Set
Iterator
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(selector, key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
try {
Thread.sleep(500);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void handleInput(Selector selector, SelectionKey key) throws IOException {
if (key.isValid()) {
// 處理新接入的請(qǐng)求消息
if (key.isAcceptable()) {
// Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// Read the data
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String request = new String(bytes, "UTF-8"); //接收到的輸入
System.out.println("client said: " + request);
String response = request + " 666";
doWrite(sc, response);
} else if (readBytes < 0) {
// 對(duì)端鏈路關(guān)閉
key.cancel();
sc.close();
} else {
; // 讀到0字節(jié),忽略
}
}
}
}
public static void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
????????至此,全文結(jié)束。
????????附NIO編程的一些學(xué)習(xí)資料《Java NIO 教程》
柚子快報(bào)激活碼778899分享:java NIO編程
相關(guān)鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。