Java經(jīng)典面試題之NIO多路復(fù)用
JAVA NIO 的多路復(fù)用是面試中經(jīng)常被問的問題,今天我們徹底搞明白究竟是怎么回事,首先我們先通過代碼來實(shí)現(xiàn) JAVA NIO 多路復(fù)用,然后我們會深入多路復(fù)用原理,讓大家面對這類面試能夠回答的很自如。
Java NIO 代碼
我們還是通過一個(gè)有 Server 端和 Client 端的網(wǎng)絡(luò)通信的例子來說明問題,不過這次是用 Java NIO 來實(shí)現(xiàn)。
Server
首先,上 Server 端代碼:
public class NioServer { private static ByteBuffer readBuffer; private static Selector selector; public static void main(String[] args) throws Exception{ // 服務(wù)端的初始化 init(); listen(); } private static void init(){ // 讀取請求數(shù)據(jù)的 Buffer readBuffer = ByteBuffer.allocate(128); ServerSocketChannel serverSocketChannel; try{ // 1.打開服務(wù)端 serverSocketChannel = ServerSocketChannel.open(); // 配置為非阻塞IO serverSocketChannel.configureBlocking(false); // 設(shè)置端口, serverSocketChannel.socket().bind(new InetSocketAddress(9000),100); // 打開 多路復(fù)用選擇器 selector = Selector.open(); // 2.把 serverSocketChannel 注冊到 selector 上。 監(jiān)聽 serverSocketChannel 的連接請求事件,與各個(gè)客戶端建立連接請求 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); }catch (IOException e){ e.printStackTrace(); } } private static void listen(){ while (true){ try{ // 3. selector查看是否有注冊在 selector 上的 Channel 有網(wǎng)絡(luò)事件發(fā)生。這個(gè)方法是阻塞的。 selector.select(); Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator(); // 4. 輪詢 SelectionKey 集合里的 SelectionKey,一個(gè) SelectionKey 代表一個(gè)網(wǎng)絡(luò)事件。 while (keysIterator.hasNext()){ SelectionKey key = (SelectionKey) keysIterator.next(); keysIterator.remove(); handleKey(key); } }catch (Exception e){ e.printStackTrace(); } } } private static void handleKey(SelectionKey key){ SocketChannel channel = null; // 5. 判斷網(wǎng)絡(luò)事件的類型并做出相應(yīng)的處理 try { // 如果是客戶端要求連接的請求 if(key.isAcceptable()){ ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); // 通過 TCP 三次握手,建立和獲取獲取客戶端和服務(wù)器的連接SocketChannel channel = serverSocketChannel.accept(); channel.configureBlocking(false); // 注冊網(wǎng)絡(luò)讀事件 channel.register(selector,SelectionKey.OP_READ); //如果是可以網(wǎng)絡(luò)讀的事件 }else if(key.isReadable()){ channel = (SocketChannel) key.channel(); readBuffer.clear();// postion 變?yōu)?,limit = capacity,也就是復(fù)位操作,準(zhǔn)備把數(shù)據(jù)寫入Buffer了 int count = channel.read(readBuffer); // 通過 Socket 來讀取數(shù)據(jù)并把數(shù)據(jù)寫入 Buffer 中。 if(count > 0){ readBuffer.flip();// 開始從Buffer讀數(shù)據(jù)。 String request = StandardCharsets.UTF_8.decode(readBuffer).toString(); System.out.println("Server receive the request: "+request); String response = "Server accept request"; channel.write(ByteBuffer.wrap(response.getBytes())); } } }catch (Exception e){ e.printStackTrace(); } } }
給大家講解一下代碼步驟。
1.初始化。首先初始化一個(gè) ServerSocketChannel 對象,然后調(diào)用它的 open() 方法,設(shè)置服務(wù)端服務(wù)的 TCP 端口號為 9000,代表服務(wù)端可以接收外部客戶端的請求了。
2.創(chuàng)建 selector,并把 serverSocketChannel 注冊在 selector 內(nèi),并讓 selector 監(jiān)聽serverSocketChannel 的網(wǎng)絡(luò)連接事件
。ServerSocketChannel 只有服務(wù)端會使用,是服務(wù)端與客戶端建立連接用的,我再給大家畫張圖來講解。
這里說明一下 ServerSocketChannel 的工作流程。
1.首先,創(chuàng)建完一個(gè) serverSocketChannel 對象后,我們要把 serverSocketChannel 注冊到 selector上,并對這個(gè) serverSocketChannel 的 OP_ACCEPT 事件
進(jìn)行監(jiān)聽,OP_ACCEPT 其實(shí)就是外部的客戶端要連接服務(wù)端的請求連接的事件。
2.當(dāng)客戶端發(fā)起請求后,Seletor 會監(jiān)聽到 OP_ACCEPT 事件
,隨后通過三次握手
建立連接,這樣客戶端和服務(wù)端之間的連接就建立好了。 很明顯,ServerSocketChannel 是為連接服務(wù)的,所有客戶端要想與服務(wù)端建立連接都要通過 ServerSocketChannel 來建立連接。
3.開始輪詢 selector 上注冊的事件。調(diào)用 selector.select() 方法來查看是否有網(wǎng)絡(luò)事件,這個(gè)方法是阻塞方法,有網(wǎng)絡(luò)事件時(shí)會返回事件的數(shù)量,代碼在一個(gè) while(true) 循環(huán)中,會不斷地輪詢執(zhí)行 selector.select() 方法,所以只要有網(wǎng)絡(luò)事件,事件就會得到處理。
4.輪詢 SelectionKey 集合里的 SelectionKey,一個(gè) SelectionKey 代表一個(gè)網(wǎng)絡(luò)事件。
5.這里的網(wǎng)絡(luò)事件主要有可連接事件和可讀取事件:
- 如果是 key.isAcceptable() == true,意味著可以連接了,那么我們首先調(diào)用 serverSocketChannel.accept() 通過三次握手來實(shí)現(xiàn) TCP 連接。
- 如果是 key.isReadable() == true,意味著可以讀取了,我們通過創(chuàng)建一個(gè)
ByteBuffer 類的對象
把數(shù)據(jù)寫到 readBuffer,然后再從readBuffer 來讀取數(shù)據(jù)
。讀取客戶端數(shù)據(jù)結(jié)束后,服務(wù)端會給客戶端發(fā)送響應(yīng),調(diào)用 channel.write()來實(shí)現(xiàn),當(dāng)然寫數(shù)據(jù)也是通過與 ByteBuffer 配合來實(shí)現(xiàn)的。
好,我們用一副圖來更好地展示服務(wù)端的代碼流程:
Client
然后,我們再看看 Client 端代碼:
public class NioClient { public static void main(String[] args) throws Exception{ // 啟動十個(gè)線程,模擬十個(gè)客戶端。 for(int i=0;i<10;i++){ new Worker().start(); } } static class Worker extends Thread{ @Override public void run() { SocketChannel channel = null; Selector selector = null; try{ // 1.創(chuàng)建一個(gè) SocketChannel,用來與服務(wù)端連接,并實(shí)現(xiàn)網(wǎng)絡(luò)讀寫操作。 channel = SocketChannel.open(); channel.configureBlocking(false); // 指定網(wǎng)絡(luò)地址,通過三次握手實(shí)現(xiàn) TCP 連接 channel.connect(new InetSocketAddress("localhost",9000)); // 創(chuàng)建一個(gè) selector 對象, 并把 SocketChannel 注冊到 selector 上,并監(jiān)聽 請求連接事件 OP_CONNECT selector = Selector.open(); channel.register(selector, SelectionKey.OP_CONNECT); // 2. 遍歷網(wǎng)絡(luò)事件 while (true){ // 查找收到的網(wǎng)絡(luò)事件 selector.select(); Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()){ SelectionKey key = (SelectionKey) keyIterator.next(); keyIterator.remove(); // 3. 判斷是否可以連接 if(key.isConnectable()){ // 如果連接成功了 if(channel.finishConnect()){ // 監(jiān)聽網(wǎng)絡(luò)讀事件 key.interestOps(SelectionKey.OP_READ); // 向服務(wù)端發(fā)送數(shù)據(jù) channel.write(ByteBuffer.wrap("hello,I'm client".getBytes())); }else { key.cancel(); } // 4. 如果有數(shù)據(jù)需要讀取 }else if(key.isReadable()){ ByteBuffer byteBuffer = ByteBuffer.allocate(128); // 把數(shù)據(jù)寫入 buffer channel.read(byteBuffer); byteBuffer.flip(); // 把數(shù)據(jù)讀出來。 String response = StandardCharsets.UTF_8.decode(byteBuffer).toString(); System.out.println("["+Thread.currentThread().getName()+"] receive response:"+response); Thread.sleep(5000); // 向服務(wù)端發(fā)送數(shù)據(jù) channel.write(ByteBuffer.wrap("hello".getBytes())); } } } }catch (Exception e){ e.printStackTrace(); }finally { if(channel != null){ try{ channel.close(); }catch (Exception e){ e.printStackTrace(); } } } } } }
客戶端代碼通過多線程模擬了十個(gè)客戶端同時(shí)向服務(wù)端發(fā)送請求,每一個(gè)線程的執(zhí)行過程我給大家講解一下。
1.創(chuàng)建一個(gè) SocketChannel 用來與服務(wù)端連接,這個(gè) SocketChannel 是用來保持與客戶端連接的 Channel,用來實(shí)現(xiàn)網(wǎng)絡(luò)讀寫操作。其實(shí),本質(zhì)上還是通過三次握手來實(shí)現(xiàn) TCP 連接。然后,把 OP_CONNECT 事件
注冊在新創(chuàng)建的 selector 上,作用跟服務(wù)端的 selector 是一樣的,這里就不再解釋了。
2.無限循環(huán)輪詢 SelectionKey 集合里的 SelectionKey。服務(wù)端輪詢基本一致,這里也會有兩個(gè)事件需要處理。
- 判斷是否可以連接。如果 key.isConnectable() == true,那么就認(rèn)為是可以連接的,但是可以連接并不意味著已經(jīng)連接上了。如果 channel.finishConnect()==true,我們才認(rèn)為連接成功建立了,這時(shí)就要把
OP_READ 事件
也就是讀事件也注冊到 selector 上,這樣我們就可以接收到服務(wù)端的數(shù)據(jù)了。然后,通過與 ByteBuffer 配合向服務(wù)端發(fā)送數(shù)據(jù)。 - 判斷是否是可讀。如果 key.isReadable() == true,那么就認(rèn)為網(wǎng)絡(luò)讀事件來了,我們需要讀取服務(wù)端給我們發(fā)送的數(shù)據(jù),讀取后再次向服務(wù)端發(fā)送數(shù)據(jù)。
服務(wù)端和客戶端的代碼運(yùn)行起來后,會不斷相互收發(fā)數(shù)據(jù)。我建議大家可以在 IDE 上運(yùn)行一下服務(wù)端和客戶端的代碼,這樣能夠更好地理解 Java NIO 的機(jī)制。
NIO 核心原理
上面給大家講解了一個(gè) NIO 的例子,并把相關(guān)代碼給大家解釋清楚了,下面我們再來看看 Java NIO 的核心原理是什么。
其實(shí) Java NIO 的多路復(fù)用機(jī)制并不是 Java NIO 本身來實(shí)現(xiàn)的,而是通過操作系統(tǒng)實(shí)現(xiàn)的。Java NIO 在調(diào)用操作系統(tǒng)的 API 來實(shí)現(xiàn)多路復(fù)用。
服務(wù)端初始化過程
我們先看下服務(wù)端的初始化過程:
// 1.打開服務(wù)端 serverSocketChannel = ServerSocketChannel.open(); // 配置為非阻塞IO serverSocketChannel.configureBlocking(false); // 設(shè)置端口, serverSocketChannel.socket().bind(new InetSocketAddress(9000),100); // 打開 多路復(fù)用選擇器 selector = Selector.open(); // 2.把 serverSocketChannel 注冊到 selector 上。 監(jiān)聽 serverSocketChannel 的連接請求事件,與各個(gè)客戶端建立連接請求 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
首先,第一步會創(chuàng)建一個(gè) ServerSocketChannel 的實(shí)例,然后配置為非阻塞。下一步,調(diào)用 serverSocketChannel.socket(),大家可以看到這個(gè)調(diào)用的返回是與 ServerSocket,其實(shí)本質(zhì)上就是與 ServerSocketChannel 相關(guān)聯(lián)的 TCP 的 Socket,然后調(diào)用 bind() 方法,設(shè)置 Socket 的端口,這樣就有了 Socket 的端口。
所以說, ServerSocketChannel 的作用是 TCP 協(xié)議下用來監(jiān)聽對 TCP 某個(gè)端口的連接請求。
好,服務(wù)端初始化過程基本給大家講解完了,下面給大家講解另一個(gè)重要的內(nèi)容:Selector 是如何注冊 Channel的。
Selector 工作原理
Selector 也是基于底層操作系統(tǒng)來實(shí)現(xiàn)的,我們看一下 Selector.open() 這個(gè)方法就知道了。
public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); }
上面的代碼就是打開一個(gè) Selector,其實(shí)底層也是基于操作系統(tǒng)來實(shí)現(xiàn)的。操作系統(tǒng)實(shí)現(xiàn)了 select 機(jī)制,用 select 機(jī)制來監(jiān)聽注冊到自己上面的 channel 有沒有網(wǎng)絡(luò)事件。當(dāng)每次注冊某個(gè) Channel 的某個(gè)網(wǎng)絡(luò)事件到 Selector 上的時(shí)候,都會為網(wǎng)絡(luò)事件指定一個(gè) SelectionKey。
SelectionKey
是一個(gè)枚舉類型,有下面幾種類型。
OP_CONNECT
:客戶端的請求連接的事件??蛻舳说?SocketChannel 會向 Selector 注冊這個(gè)事件,需要 Selector 監(jiān)聽服務(wù)端是否連接準(zhǔn)備好了。OP_ACCEPT
:服務(wù)端的接收連接的事件。服務(wù)端的 ServerSocketChannel 會向 Selector 注冊這個(gè)事件,需要 Selector 監(jiān)聽接收到客戶端的請求連接。OP_READ
:服務(wù)端和客戶端的網(wǎng)絡(luò)讀事件。服務(wù)端和客戶端的 SocketChannel 都會向 Selector 注冊這個(gè)事件。需要 Selector 監(jiān)聽是否接收到對方發(fā)送的數(shù)據(jù)了。OP_WRITE
:服務(wù)端和客戶端的網(wǎng)絡(luò)寫事件。服務(wù)端和客戶端的 SocketChannel 都會向 Selector 注冊這個(gè)事件。需要 Selector 監(jiān)聽是否可以向?qū)Ψ桨l(fā)送數(shù)據(jù)了。
也就是說,我們可以根據(jù)需要指定需要監(jiān)控的網(wǎng)絡(luò)事件。隨著注冊在 Selector 上的 Channel 越來越多,隨之而來的注冊在 Selector 上的網(wǎng)絡(luò)事件越來越多,那么,Selector 就能夠監(jiān)聽很多請求連接及讀寫的網(wǎng)絡(luò)事件,這樣我們通過多路復(fù)用來響應(yīng)海量客戶端,從而實(shí)現(xiàn)了高性能網(wǎng)絡(luò)服務(wù)端的目的。
當(dāng)然,當(dāng)我們對某個(gè)網(wǎng)絡(luò)事件不感興趣了,我們也可以取消對網(wǎng)絡(luò)事件的監(jiān)聽,比如當(dāng)客戶端已經(jīng)連接到了服務(wù)端,就可以取消對 OP_CONNECT 事件的關(guān)注,好處是能夠通過減少關(guān)注的事件數(shù)量減少 Selector 的負(fù)載,從而提升多路復(fù)用的效率。
多路復(fù)用的原理(建立連接過程)
我們應(yīng)該都知道 Java NIO 采用了多路復(fù)用
的思想。那么,多路復(fù)用
的原理是什么呢?
我先給大家解釋一下多路復(fù)用的原理,這里還是用一張圖來引出多路復(fù)用的原理:
根據(jù)這幅圖,我們只體現(xiàn)了網(wǎng)絡(luò)連接事件
(OP_ACCEPT 和 OP_CONNECT),讀寫事件忽略,這樣能讓大家看的更簡潔一些。
大家可以看到:客戶端向服務(wù)器連接前,服務(wù)端會把接收連接的事件(OP_ACCEPT)注冊到服務(wù)端的 NIO Selector
,然后 NIO Selector 再把事件注冊到操作系統(tǒng)
上,也就是說操作系統(tǒng)才是能真正實(shí)現(xiàn)多路復(fù)用。然后,服務(wù)端通過不斷循環(huán)來監(jiān)聽是否有客戶端發(fā)送連接的請求。如果有,操作系統(tǒng)會通過修改某些 JAVA 的屬性來異步通知
給 JAVA 程序。 如果 NIO Selector 輪詢
到某些屬性變化了,比如 key.isAcceptable() == true,那么就可以執(zhí)行相應(yīng)的業(yè)務(wù)邏輯。
程序會使用組件 Selector 來創(chuàng)建和管理多個(gè)連接的網(wǎng)絡(luò)事件,最重要的是 Selector 是在一個(gè)線程
里工作,這樣,服務(wù)端用一個(gè)線程就能管理 N 個(gè)客戶端的連接
。也就是說,Selecot 組件是 JAVA NIO 多路復(fù)用的靈魂。
Selector 的 select()方法
如果說 Selector 是 JAVA NIO 多路復(fù)用的靈魂,那么 Selector 的 select 方法就是核心,這個(gè)方法負(fù)責(zé)收集監(jiān)聽到的網(wǎng)絡(luò)事件,我們先看看這個(gè)方法有幾種重載方式:
- select():
阻塞方法
,返回值int。只要監(jiān)聽到有注冊在 Selector 上的事件準(zhǔn)備好了,就會返回監(jiān)聽到的事件的數(shù)量,否則一直阻塞。 這個(gè)方法的優(yōu)點(diǎn)是,線程不會空轉(zhuǎn)
,只有監(jiān)聽到了事件才會執(zhí)行。缺點(diǎn)是線程阻塞,這個(gè)線程內(nèi)的其它功能不能及時(shí)執(zhí)行。 - select(long timeout):
限時(shí)阻塞方法
。在一個(gè)時(shí)間段內(nèi)阻塞,如果在這個(gè)時(shí)間段內(nèi)監(jiān)聽到準(zhǔn)備好的網(wǎng)絡(luò)事件就返回網(wǎng)絡(luò)事件的數(shù)量,否則一直等到這個(gè)時(shí)間結(jié)束在返回。 這個(gè)方法的優(yōu)點(diǎn)是,線程在一個(gè)時(shí)間段內(nèi)不會空轉(zhuǎn),只有監(jiān)聽到了事件才會執(zhí)行。缺點(diǎn)是還是會有線程阻塞,這個(gè)線程內(nèi)的其它功能不能及時(shí)執(zhí)行。 - selectNow():
不會阻塞的方法
,只要執(zhí)行到這里就返回,如果沒有監(jiān)聽到事件就返回0。 這個(gè)方法的優(yōu)點(diǎn)是:由于沒有阻塞,線程內(nèi)的邏輯都會及時(shí)執(zhí)行,但是如果業(yè)務(wù)邏輯比較簡單,而且網(wǎng)絡(luò)事件出現(xiàn)的頻率不高這個(gè)線程就會一直空轉(zhuǎn),浪費(fèi)了 CPU 的資源。
現(xiàn)在大家看起來可能有些復(fù)雜,建議大家先把 demo 代碼搞明白,然后再對照上圖
,相信大家會理解的更透徹
。
到此這篇關(guān)于Java經(jīng)典面試題之NIO多路復(fù)用的文章就介紹到這了,更多相關(guān)Java NIO多路復(fù)用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何搭建一個(gè)完整的Java開發(fā)環(huán)境
這篇文章主要教大家如何搭建一個(gè)完整的Java開發(fā)環(huán)境,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11Java調(diào)用WebService服務(wù)的三種方式總結(jié)
雖然WebService這個(gè)框架已經(jīng)過時(shí),但是有些公司還在使用,在調(diào)用他們的服務(wù)的時(shí)候就不得不面對各種問題,本篇文章總結(jié)了最近我調(diào)用?WebService的心路歷程,3種方式可以分別嘗試,需要的朋友可以參考下2023-08-08@DS注解的使用,動態(tài)數(shù)據(jù)源,事務(wù)詳解
在項(xiàng)目中使用多數(shù)據(jù)源時(shí),可以借助苞米豆的dynamic-datasource-spring-boot-starter進(jìn)行配置,首先需引入相應(yīng)的jar包,并在application.yml中設(shè)置主從數(shù)據(jù)源,其中一般選擇master作為默認(rèn)數(shù)據(jù)源,在實(shí)現(xiàn)類中通過@DS注解指定數(shù)據(jù)源2024-09-09SpringBoot接收參數(shù)使用的注解實(shí)例講解
這篇文章主要介紹了詳解SpringBoot接收參數(shù)使用的幾種常用注解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08