NIO
Java NIO 基本介紹
Java NIO
全稱Java non-blocking IO
,是指JDK
提供的新API
。從JDK1.4
開始,Java
提供了一系列改進的輸入/輸出的新特性,被統稱為NIO
(即NewIO
),是同步非阻塞的。NIO
相關類都被放在java.nio
包及子包下,并且對原java.io
包中的很多類進行改寫。NIO
有三大核心部分:Channel
(通道)、Buffer
(緩沖區)、Selector
(選擇器) 。NIO
是面向緩沖區,或者面向塊編程的。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網絡。- Java NIO 的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此,一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。
- 通俗理解:
NIO
是可以做到用一個線程來處理多個操作的。假設有10000
個請求過來,根據實際情況,可以分配50
或者100
個線程來處理。不像之前的阻塞IO
那樣,非得分配10000
個。 HTTP 2.0
使用了多路復用的技術,做到同一個連接并發處理多個請求,而且并發請求的數量比HTTP 1.1
大了好幾個數量級。
NIO 三大核心原理示意圖
一張圖描述 NIO
的 Selector
、Channel
和 Buffer
的關系。
- 每個
Channel
都會對應一個Buffer
。 Selector
對應一個線程,一個線程對應多個Channel
(連接)。- 該圖反應了有三個
Channel
注冊到該Selector
//程序 - 程序切換到哪個
Channel
是由事件決定的,Event
就是一個重要的概念。 Selector
會根據不同的事件,在各個通道上切換。Buffer
就是一個內存塊,底層是有一個數組。- 數據的讀取寫入是通過 Buffer,這個和 BIO是不同的,BIO 中要么是輸入流,或者是輸出流,不能雙向,但是 NIO 的 Buffer 是可以讀也可以寫,需要 flip 方法切換 Channel 是雙向的,可以返回底層操作系統的情況,比如 Linux,底層的操作系統通道就是雙向的。
NIO 和 BIO 的比較
-
BIO
以流的方式處理數據,而NIO
以塊的方式處理數據,塊I/O
的效率比流I/O
高很多。 -
BIO
是阻塞的,NIO
則是非阻塞的。 -
BIO 基于字節流和字符流進行操作,而 NIO 基于 Channel(通道)和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用于監聽多個通道的事件(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道。
-
Buffer和Channel之間的數據流向是雙向的
緩沖區(Buffer)
緩沖區(Buffer
):緩沖區本質上是一個可以讀寫數據的內存塊,可以理解成是一個**容器對象(含數組)**該對象提供了一組方法,可以更輕松地使用內存塊,,緩沖區對象內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況。Channel
提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由 Buffer
,
通道(Channel)
NIO
的通道類似于流,但有些區別如下:
- 通道可以同時進行讀寫,而流只能讀或者只能寫
- 通道可以實現異步讀寫數據
- 通道可以從緩沖讀數據,也可以寫數據到緩沖:
BIO
中的Stream
是單向的,例如FileInputStream
對象只能進行讀取數據的操作,而NIO
中的通道(Channel
)是雙向的,可以讀操作,也可以寫操作。Channel
在NIO
中是一個接口public interface Channel extends Closeable{}
- 常用的
Channel
類有:FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
。【ServerSocketChanne
類似ServerSocket
、SocketChannel
類似Socket
】 FileChannel
用于文件的數據讀寫,DatagramChannel
用于UDP
的數據讀寫,ServerSocketChannel
和SocketChannel
用于TCP
的數據讀寫。
NIO
還支持通過多個 Buffer
(即 Buffer
數組)完成讀寫操作,即 Scattering
和 Gathering
【舉例說明】
/*** @Author:jiangdw7* @date: 2023/8/9 10:04*/
public class ScatteringAndGatheringTest {public static void main(String[] args) throws Exception {//使用 ServerSocketChannel 和 SocketChannel 網絡ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);//綁定端口到 socket,并啟動serverSocketChannel.socket().bind(inetSocketAddress);//創建 buffer 數組ByteBuffer[] byteBuffers = new ByteBuffer[2];byteBuffers[0] = ByteBuffer.allocate(5);byteBuffers[1] = ByteBuffer.allocate(3);//等客戶端連接 (telnet)SocketChannel socketChannel = serverSocketChannel.accept();int messageLength = 8; //假定從客戶端接收 8 個字節//循環的讀取while (true) {int byteRead = 0;while (byteRead < messageLength) {long l = socketChannel.read(byteBuffers);byteRead += l; //累計讀取的字節數System.out.println("byteRead = " + byteRead);//使用流打印,看看當前的這個 buffer 的 position 和 limitArrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println);}//將所有的 buffer 進行 flipArrays.asList(byteBuffers).forEach(buffer -> buffer.flip());//將數據讀出顯示到客戶端long byteWirte = 0;while (byteWirte < messageLength) {long l = socketChannel.write(byteBuffers);byteWirte += l;}//將所有的buffer進行clearArrays.asList(byteBuffers).forEach(buffer -> {buffer.clear();});System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength);}}
}
Selector(選擇器)
Java
的NIO
,用非阻塞的IO
方式。可以用一個線程,處理多個的客戶端連接,就會使用到Selector
(選擇器)。Selector
能夠檢測多個注冊的通道上是否有事件發生(注意:多個Channel
以事件的方式可以注冊到同一個Selector
),如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。- 只有在連接/通道真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,并且不必為每個連接都創建一個線程,不用去維護多個線程。
- 避免了多線程之間的上下文切換導致的開銷。
注意事項
NIO
中的ServerSocketChannel
功能類似ServerSocket
、SocketChannel
功能類似Socket
。Selector
相關方法說明selector.select();
//阻塞selector.select(1000);
//阻塞 1000 毫秒,在 1000 毫秒后返回selector.wakeup();
//喚醒 selectorselector.selectNow();
//不阻塞,立馬返還
public class NIOClient {private static Selector selector;public static void main(String[] args) throws Exception {selector = Selector.open();SocketChannel sc = SocketChannel.open();sc.configureBlocking(false);sc.connect(new InetSocketAddress("127.0.0.1", 8081));sc.register(selector, SelectionKey.OP_READ);ByteBuffer bf = ByteBuffer.allocate(1024);bf.put("Hi,server,i'm client".getBytes());if (sc.finishConnect()) {bf.flip();while (bf.hasRemaining()) {sc.write(bf);}while (sc.isConnected()) {selector.select();Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey key = it.next();if (key.isReadable()) {ByteArrayOutputStream bos = new ByteArrayOutputStream();bf.clear();SocketChannel othersc = (SocketChannel) key.channel();while (othersc.read(bf) > 0) {bf.flip();while (bf.hasRemaining()) {bos.write(bf.get());}bf.clear();}System.out.println("服務端返回的數據:" + bos.toString());Thread.sleep(5000);sc.close();System.out.println("客戶端關閉...");}}selector.selectedKeys().clear();}}}
}
public class NIOServer {private static Selector selector;private static ServerSocketChannel serverSocketChannel;private static ByteBuffer bf = ByteBuffer.allocate(1024);public static void main(String[] args) throws Exception {init();while (true) {int select = selector.select(10000);if (select == 0) {System.out.println("等待連接10秒...");continue;}Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey key = it.next();if (key.isAcceptable()) {System.out.println("連接準備就緒");ServerSocketChannel server = (ServerSocketChannel) key.channel();System.out.println("等待客戶端連接中........................");SocketChannel channel = server.accept();channel.configureBlocking(false);channel.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {System.out.println("讀準備就緒,開始讀.......................");SocketChannel channel = (SocketChannel) key.channel();System.out.println("客戶端的數據如下:");int readLen = 0;bf.clear();StringBuffer sb = new StringBuffer();while ((readLen = channel.read(bf)) > 0) {bf.flip();byte[] temp = new byte[readLen];bf.get(temp, 0, readLen);sb.append(new String(temp));bf.clear();}if (-1 == readLen) {System.out.println(channel.hashCode()+"號客戶端關閉。");channel.close();}else {channel.write(ByteBuffer.wrap(("客戶端,你傳過來的數據是:" + sb.toString()).getBytes()));System.out.println(sb.toString()+"132123");}}it.remove();}}}private static void init() throws Exception {selector = Selector.open();serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(8081));serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);}
}
參考連接
https://www.cnblogs.com/xdouby/p/8942083.html
https://www.zhihu.com/question/22524908
https://blog.csdn.net/ArtAndLife/article/details/121001656
AIO
-
JDK7
引入了AsynchronousI/O
,即AIO
。在進行I/O
編程中,常用到兩種模式:Reactor
和Proactor
。Java
的NIO
就是Reactor
,當有事件觸發時,服務器端得到通知,進行相應的處理 -
AIO
即NIO2.0
,叫做異步不阻塞的IO
。AIO
引入異步通道的概念,采用了Proactor
模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,一般適用于連接數較多且連接時間較長的應用 -
目前
AIO
還沒有廣泛應用,Netty
也是基于NIO
,而不是AIO
,因此我們就不詳解AIO
了,有興趣的同學可以參考《Java新一代網絡編程模型AIO原理及Linux系統AIO介紹》