最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java經(jīng)典面試題之NIO多路復用

 更新時間:2023年06月01日 10:47:44   作者:肖恩Sean  
JAVA?NIO?的多路復用是面試中經(jīng)常被問的問題,今天我們徹底搞明白究竟是怎么回事,文中的示例代碼講解詳細,希望對大家學習Java有所幫助

JAVA NIO 的多路復用是面試中經(jīng)常被問的問題,今天我們徹底搞明白究竟是怎么回事,首先我們先通過代碼來實現(xiàn) JAVA NIO 多路復用,然后我們會深入多路復用原理,讓大家面對這類面試能夠回答的很自如。

Java NIO 代碼

我們還是通過一個有 Server 端和 Client 端的網(wǎng)絡通信的例子來說明問題,不過這次是用 Java NIO 來實現(xiàn)。

Server

首先,上 Server 端代碼:

public class NioServer {
    private static ByteBuffer readBuffer;
    private static Selector selector;
    public static void main(String[] args)  throws Exception{
          // 服務端的初始化
          init();
          listen();
    }
    private static void init(){
        // 讀取請求數(shù)據(jù)的 Buffer
        readBuffer = ByteBuffer.allocate(128);
        ServerSocketChannel serverSocketChannel;
        try{
            // 1.打開服務端
            serverSocketChannel = ServerSocketChannel.open();
            // 配置為非阻塞IO
            serverSocketChannel.configureBlocking(false);
            // 設置端口,
            serverSocketChannel.socket().bind(new InetSocketAddress(9000),100);
            // 打開 多路復用選擇器
            selector = Selector.open();
            // 2.把 serverSocketChannel 注冊到 selector 上。 監(jiān)聽 serverSocketChannel 的連接請求事件,與各個客戶端建立連接請求
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    private static void listen(){
        while (true){
            try{
                // 3. selector查看是否有注冊在 selector 上的 Channel 有網(wǎng)絡事件發(fā)生。這個方法是阻塞的。
                selector.select();
                Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
                // 4. 輪詢 SelectionKey 集合里的 SelectionKey,一個 SelectionKey 代表一個網(wǎng)絡事件。
                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)絡事件的類型并做出相應的處理
        try {
            // 如果是客戶端要求連接的請求
            if(key.isAcceptable()){
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                // 通過 TCP 三次握手,建立和獲取獲取客戶端和服務器的連接SocketChannel
                channel = serverSocketChannel.accept();
                channel.configureBlocking(false);
                // 注冊網(wǎng)絡讀事件
                channel.register(selector,SelectionKey.OP_READ);
                //如果是可以網(wǎng)絡讀的事件
            }else if(key.isReadable()){
                channel = (SocketChannel) key.channel();
                readBuffer.clear();// postion 變?yōu)?,limit = capacity,也就是復位操作,準備把數(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.初始化。首先初始化一個 ServerSocketChannel 對象,然后調(diào)用它的 open() 方法,設置服務端服務的 TCP 端口號為 9000,代表服務端可以接收外部客戶端的請求了。

2.創(chuàng)建 selector,并把 serverSocketChannel 注冊在 selector 內(nèi),并讓 selector 監(jiān)聽serverSocketChannel 的網(wǎng)絡連接事件。ServerSocketChannel 只有服務端會使用,是服務端與客戶端建立連接用的,我再給大家畫張圖來講解。

這里說明一下 ServerSocketChannel 的工作流程。

1.首先,創(chuàng)建完一個 serverSocketChannel 對象后,我們要把 serverSocketChannel 注冊到 selector上,并對這個 serverSocketChannel 的 OP_ACCEPT 事件進行監(jiān)聽,OP_ACCEPT 其實就是外部的客戶端要連接服務端的請求連接的事件。

2.當客戶端發(fā)起請求后,Seletor 會監(jiān)聽到 OP_ACCEPT 事件,隨后通過三次握手建立連接,這樣客戶端和服務端之間的連接就建立好了。 很明顯,ServerSocketChannel 是為連接服務的,所有客戶端要想與服務端建立連接都要通過 ServerSocketChannel 來建立連接。

3.開始輪詢 selector 上注冊的事件。調(diào)用 selector.select() 方法來查看是否有網(wǎng)絡事件,這個方法是阻塞方法,有網(wǎng)絡事件時會返回事件的數(shù)量,代碼在一個 while(true) 循環(huán)中,會不斷地輪詢執(zhí)行 selector.select() 方法,所以只要有網(wǎng)絡事件,事件就會得到處理。

4.輪詢 SelectionKey 集合里的 SelectionKey,一個 SelectionKey 代表一個網(wǎng)絡事件。

5.這里的網(wǎng)絡事件主要有可連接事件和可讀取事件:

  • 如果是 key.isAcceptable() == true,意味著可以連接了,那么我們首先調(diào)用 serverSocketChannel.accept() 通過三次握手來實現(xiàn) TCP 連接。
  • 如果是 key.isReadable() == true,意味著可以讀取了,我們通過創(chuàng)建一個 ByteBuffer 類的對象把數(shù)據(jù)寫到 readBuffer,然后再從 readBuffer 來讀取數(shù)據(jù)。讀取客戶端數(shù)據(jù)結(jié)束后,服務端會給客戶端發(fā)送響應,調(diào)用 channel.write()來實現(xiàn),當然寫數(shù)據(jù)也是通過與 ByteBuffer 配合來實現(xiàn)的。

好,我們用一副圖來更好地展示服務端的代碼流程:

Client

然后,我們再看看 Client 端代碼:

public class NioClient {
    public static void main(String[] args) throws Exception{
        // 啟動十個線程,模擬十個客戶端。
        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)建一個 SocketChannel,用來與服務端連接,并實現(xiàn)網(wǎng)絡讀寫操作。
                channel = SocketChannel.open();
                channel.configureBlocking(false);
                // 指定網(wǎng)絡地址,通過三次握手實現(xiàn) TCP 連接
                channel.connect(new InetSocketAddress("localhost",9000));
                // 創(chuàng)建一個 selector 對象, 并把 SocketChannel 注冊到 selector 上,并監(jiān)聽 請求連接事件 OP_CONNECT
                selector = Selector.open();
                channel.register(selector, SelectionKey.OP_CONNECT);
                // 2. 遍歷網(wǎng)絡事件
                while (true){
                    // 查找收到的網(wǎng)絡事件
                    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)絡讀事件
                                key.interestOps(SelectionKey.OP_READ);
                                // 向服務端發(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);
                            // 向服務端發(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();
                    }
                }
            }
        }
    }
}

客戶端代碼通過多線程模擬了十個客戶端同時向服務端發(fā)送請求,每一個線程的執(zhí)行過程我給大家講解一下。

1.創(chuàng)建一個 SocketChannel 用來與服務端連接,這個 SocketChannel 是用來保持與客戶端連接的 Channel,用來實現(xiàn)網(wǎng)絡讀寫操作。其實,本質(zhì)上還是通過三次握手來實現(xiàn) TCP 連接。然后,把 OP_CONNECT 事件注冊在新創(chuàng)建的 selector 上,作用跟服務端的 selector 是一樣的,這里就不再解釋了。

2.無限循環(huán)輪詢 SelectionKey 集合里的 SelectionKey。服務端輪詢基本一致,這里也會有兩個事件需要處理。

  • 判斷是否可以連接。如果 key.isConnectable() == true,那么就認為是可以連接的,但是可以連接并不意味著已經(jīng)連接上了。如果 channel.finishConnect()==true,我們才認為連接成功建立了,這時就要把 OP_READ 事件也就是讀事件也注冊到 selector 上,這樣我們就可以接收到服務端的數(shù)據(jù)了。然后,通過與 ByteBuffer 配合向服務端發(fā)送數(shù)據(jù)。
  • 判斷是否是可讀。如果 key.isReadable() == true,那么就認為網(wǎng)絡讀事件來了,我們需要讀取服務端給我們發(fā)送的數(shù)據(jù),讀取后再次向服務端發(fā)送數(shù)據(jù)。

服務端和客戶端的代碼運行起來后,會不斷相互收發(fā)數(shù)據(jù)。我建議大家可以在 IDE 上運行一下服務端和客戶端的代碼,這樣能夠更好地理解 Java NIO 的機制。

NIO 核心原理

上面給大家講解了一個 NIO 的例子,并把相關代碼給大家解釋清楚了,下面我們再來看看 Java NIO 的核心原理是什么。

其實 Java NIO 的多路復用機制并不是 Java NIO 本身來實現(xiàn)的,而是通過操作系統(tǒng)實現(xiàn)的。Java NIO 在調(diào)用操作系統(tǒng)的 API 來實現(xiàn)多路復用。

服務端初始化過程

我們先看下服務端的初始化過程:

// 1.打開服務端
serverSocketChannel = ServerSocketChannel.open();
// 配置為非阻塞IO
serverSocketChannel.configureBlocking(false);
// 設置端口,
serverSocketChannel.socket().bind(new InetSocketAddress(9000),100);
// 打開 多路復用選擇器
selector = Selector.open();
// 2.把 serverSocketChannel 注冊到 selector 上。 監(jiān)聽 serverSocketChannel 的連接請求事件,與各個客戶端建立連接請求
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

首先,第一步會創(chuàng)建一個 ServerSocketChannel 的實例,然后配置為非阻塞。下一步,調(diào)用 serverSocketChannel.socket(),大家可以看到這個調(diào)用的返回是與 ServerSocket,其實本質(zhì)上就是與 ServerSocketChannel 相關聯(lián)的 TCP 的 Socket,然后調(diào)用 bind() 方法,設置 Socket 的端口,這樣就有了 Socket 的端口。

所以說, ServerSocketChannel 的作用是 TCP 協(xié)議下用來監(jiān)聽對 TCP 某個端口的連接請求。

好,服務端初始化過程基本給大家講解完了,下面給大家講解另一個重要的內(nèi)容:Selector 是如何注冊 Channel的。

Selector 工作原理

Selector 也是基于底層操作系統(tǒng)來實現(xiàn)的,我們看一下 Selector.open() 這個方法就知道了。

public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}

上面的代碼就是打開一個 Selector,其實底層也是基于操作系統(tǒng)來實現(xiàn)的。操作系統(tǒng)實現(xiàn)了 select 機制,用 select 機制來監(jiān)聽注冊到自己上面的 channel 有沒有網(wǎng)絡事件。當每次注冊某個 Channel 的某個網(wǎng)絡事件到 Selector 上的時候,都會為網(wǎng)絡事件指定一個 SelectionKey。

SelectionKey 是一個枚舉類型,有下面幾種類型。

  • OP_CONNECT:客戶端的請求連接的事件??蛻舳说?SocketChannel 會向 Selector 注冊這個事件,需要 Selector 監(jiān)聽服務端是否連接準備好了。
  • OP_ACCEPT:服務端的接收連接的事件。服務端的 ServerSocketChannel 會向 Selector 注冊這個事件,需要 Selector 監(jiān)聽接收到客戶端的請求連接。
  • OP_READ:服務端和客戶端的網(wǎng)絡讀事件。服務端和客戶端的 SocketChannel 都會向 Selector 注冊這個事件。需要 Selector 監(jiān)聽是否接收到對方發(fā)送的數(shù)據(jù)了。
  • OP_WRITE:服務端和客戶端的網(wǎng)絡寫事件。服務端和客戶端的 SocketChannel 都會向 Selector 注冊這個事件。需要 Selector 監(jiān)聽是否可以向?qū)Ψ桨l(fā)送數(shù)據(jù)了。

也就是說,我們可以根據(jù)需要指定需要監(jiān)控的網(wǎng)絡事件。隨著注冊在 Selector 上的 Channel 越來越多,隨之而來的注冊在 Selector 上的網(wǎng)絡事件越來越多,那么,Selector 就能夠監(jiān)聽很多請求連接及讀寫的網(wǎng)絡事件,這樣我們通過多路復用來響應海量客戶端,從而實現(xiàn)了高性能網(wǎng)絡服務端的目的。

當然,當我們對某個網(wǎng)絡事件不感興趣了,我們也可以取消對網(wǎng)絡事件的監(jiān)聽,比如當客戶端已經(jīng)連接到了服務端,就可以取消對 OP_CONNECT 事件的關注,好處是能夠通過減少關注的事件數(shù)量減少 Selector 的負載,從而提升多路復用的效率。

多路復用的原理(建立連接過程)

我們應該都知道 Java NIO 采用了多路復用的思想。那么,多路復用的原理是什么呢?

我先給大家解釋一下多路復用的原理,這里還是用一張圖來引出多路復用的原理

根據(jù)這幅圖,我們只體現(xiàn)了網(wǎng)絡連接事件(OP_ACCEPT 和 OP_CONNECT),讀寫事件忽略,這樣能讓大家看的更簡潔一些。

大家可以看到:客戶端向服務器連接前,服務端會把接收連接的事件(OP_ACCEPT)注冊到服務端的 NIO Selector,然后 NIO Selector 再把事件注冊到操作系統(tǒng)上,也就是說操作系統(tǒng)才是能真正實現(xiàn)多路復用。然后,服務端通過不斷循環(huán)來監(jiān)聽是否有客戶端發(fā)送連接的請求。如果有,操作系統(tǒng)會通過修改某些 JAVA 的屬性來異步通知給 JAVA 程序。 如果 NIO Selector 輪詢到某些屬性變化了,比如 key.isAcceptable() == true,那么就可以執(zhí)行相應的業(yè)務邏輯。

程序會使用組件 Selector 來創(chuàng)建和管理多個連接的網(wǎng)絡事件,最重要的是 Selector 是在一個線程里工作,這樣,服務端用一個線程就能管理 N 個客戶端的連接。也就是說,Selecot 組件是 JAVA NIO 多路復用的靈魂。

Selector 的 select()方法

如果說 Selector 是 JAVA NIO 多路復用的靈魂,那么 Selector 的 select 方法就是核心,這個方法負責收集監(jiān)聽到的網(wǎng)絡事件,我們先看看這個方法有幾種重載方式:

  • select(): 阻塞方法,返回值int。只要監(jiān)聽到有注冊在 Selector 上的事件準備好了,就會返回監(jiān)聽到的事件的數(shù)量,否則一直阻塞。 這個方法的優(yōu)點是,線程不會空轉(zhuǎn),只有監(jiān)聽到了事件才會執(zhí)行。缺點是線程阻塞,這個線程內(nèi)的其它功能不能及時執(zhí)行。
  • select(long timeout):限時阻塞方法。在一個時間段內(nèi)阻塞,如果在這個時間段內(nèi)監(jiān)聽到準備好的網(wǎng)絡事件就返回網(wǎng)絡事件的數(shù)量,否則一直等到這個時間結(jié)束在返回。 這個方法的優(yōu)點是,線程在一個時間段內(nèi)不會空轉(zhuǎn),只有監(jiān)聽到了事件才會執(zhí)行。缺點是還是會有線程阻塞,這個線程內(nèi)的其它功能不能及時執(zhí)行。
  • selectNow():不會阻塞的方法,只要執(zhí)行到這里就返回,如果沒有監(jiān)聽到事件就返回0。 這個方法的優(yōu)點是:由于沒有阻塞,線程內(nèi)的邏輯都會及時執(zhí)行,但是如果業(yè)務邏輯比較簡單,而且網(wǎng)絡事件出現(xiàn)的頻率不高這個線程就會一直空轉(zhuǎn),浪費了 CPU 的資源。

現(xiàn)在大家看起來可能有些復雜,建議大家先把 demo 代碼搞明白,然后再對照上圖,相信大家會理解的更透徹。

到此這篇關于Java經(jīng)典面試題之NIO多路復用的文章就介紹到這了,更多相關Java NIO多路復用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Java多線程通訊之wait,notify的區(qū)別詳解

    Java多線程通訊之wait,notify的區(qū)別詳解

    這篇文章主要介紹了Java多線程通訊之wait,notify的區(qū)別詳解,非常不錯,具有一定的參考借鑒借鑒價值,需要的朋友可以參考下
    2018-07-07
  • Elasticsearch term 查詢之精確值搜索功能實現(xiàn)

    Elasticsearch term 查詢之精確值搜索功能實現(xiàn)

    term查詢是Elasticsearch中用于精確值搜索的一種基本方式,通過了解 term 查詢的工作原理和使用方法,你可以更好地利用 Elasticsearch 進行結(jié)構(gòu)化數(shù)據(jù)的搜索和分析,本文將詳細介紹 term 查詢的工作原理、使用場景以及如何在 Elasticsearch 中應用它,感興趣的朋友一起看看吧
    2024-06-06
  • SpringBoot3集成Thymeleaf的過程詳解

    SpringBoot3集成Thymeleaf的過程詳解

    在現(xiàn)代的Web開發(fā)中,構(gòu)建靈活、動態(tài)的用戶界面是至關重要的,Spring Boot和Thymeleaf的結(jié)合為開發(fā)者提供了一種簡單而強大的方式來創(chuàng)建動態(tài)的Web應用,本文將介紹如何在Spring Boot項目中集成Thymeleaf,并展示一些基本的使用方法,需要的朋友可以參考下
    2024-01-01
  • Java ByteBuffer網(wǎng)絡編程用法實例解析

    Java ByteBuffer網(wǎng)絡編程用法實例解析

    這篇文章主要介紹了Java ByteBuffer網(wǎng)絡編程用法實例解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-10-10
  • 解析Java編程之Synchronized鎖住的對象

    解析Java編程之Synchronized鎖住的對象

    這篇文章主要介紹了解析Java編程之Synchronized鎖住的對象,具有一定參考價值,需要的朋友可以了解下。
    2017-10-10
  • spring boot 統(tǒng)一JSON格式的接口返回結(jié)果的實現(xiàn)

    spring boot 統(tǒng)一JSON格式的接口返回結(jié)果的實現(xiàn)

    這篇文章主要介紹了spring boot 統(tǒng)一JSON格式的接口返回結(jié)果的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-10-10
  • Java中Http連接的兩種方式(小結(jié))

    Java中Http連接的兩種方式(小結(jié))

    這篇文章主要介紹了Java中Http連接的兩種方式(小結(jié)),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-09-09
  • 如何讓Spring Rest 接口中路徑參數(shù)可選

    如何讓Spring Rest 接口中路徑參數(shù)可選

    這篇文章主要介紹了如何讓Spring Rest 接口中路徑參數(shù)可選,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • SpringBoot2.0解決Long型數(shù)據(jù)轉(zhuǎn)換成json格式時丟失精度問題

    SpringBoot2.0解決Long型數(shù)據(jù)轉(zhuǎn)換成json格式時丟失精度問題

    這篇文章主要介紹了SpringBoot2.0解決Long型數(shù)據(jù)轉(zhuǎn)換成json格式時丟失精度問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • SpringAMQP消息隊列(SpringBoot集成RabbitMQ方式)

    SpringAMQP消息隊列(SpringBoot集成RabbitMQ方式)

    這篇文章主要介紹了SpringAMQP消息隊列(SpringBoot集成RabbitMQ方式),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-04-04

最新評論

铁力市| 九龙县| 汉源县| 高要市| 大厂| 安仁县| 汽车| 新化县| 革吉县| 乌拉特后旗| 晋江市| 汝城县| 芜湖市| 凭祥市| 江都市| 安阳县| 扶余县| 镇坪县| 达日县| 西和县| 长宁县| 南京市| 承德县| 永安市| 宁晋县| 当阳市| 靖远县| 上思县| 吉林省| 云南省| 龙游县| 乐至县| 包头市| 河北省| 金湖县| 汤原县| 西宁市| 松潘县| 平泉县| 永吉县| 阿城市|