天天看點

Java NIO:Selector詳解以及在網絡程式設計中的應用概述示例

概述

在NIO網絡程式設計中,Selector&Channel&Buffer三者的關系是十分緊密的,Buffer從Channel中讀寫,Channel注冊在Selector中。在以往的網絡程式設計中,通常都是通過建立一個線程來維護一個socket通訊,在業務量較小時,是可以很好的完成工作的,但是一旦用戶端增多,建立的線程也随之增多,對硬體的開銷是非常大的。這時候NIO的Selector就展現出了價值:

Selector在Java NIO中可以檢測到一個或者多個Channel,并能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的線程可以管理多個Channel,進而管理多個網絡連接配接。這樣的單個線程管理管理多個Channel可以極大的減少線程間切換的開銷。

示例

package com.leolee.nio;

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.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Set;

/**
 * @ClassName SelectorTest
 * @Description: NIO socket程式設計demo,用于了解Selector
 * @Author LeoLee
 * @Date 2020/9/22
 * @Version V1.0
 **/
public class SelectorTest {

    //端口數組,用于和多個用戶端建立連接配接後配置設定端口
    int[] ports = null;

    //起始端口
    int tempPort = 5000;

    //構造器初始化 端口數組ports,并從起始端口tempPort開始配置設定[size]個端口号
    public SelectorTest (int size) {
        this.ports = new int[size];
        for (int i = 0; i < size; i++) {
            this.ports[i] = tempPort + i;
        }
    }


    public void selectorTest () throws IOException {

        Selector selector = Selector.open();

        //windows系統下是sun.nio.ch.WindowsSelectorProvider,如果是linux系統,則是KQueueSelectorProvider
        //由于Selector.open()的源碼涉及 sun 包下的代碼,是非開源代碼,具體實作不得而知
//        System.out.println(SelectorProvider.provider().getClass());//sun.nio.ch.WindowsSelectorProvider
//        System.out.println(sun.nio.ch.DefaultSelectorProvider.create().getClass());//sun.nio.ch.WindowsSelectorProvider

        for (int i = 0; i < ports.length; i++) {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);//非阻塞模式
            ServerSocket serverSocket = serverSocketChannel.socket();
            //綁定端口
            InetSocketAddress address = new InetSocketAddress("127.0.0.1", ports[i]);
            serverSocket.bind(address);

            //注冊selector
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("[step1]監聽端口:" + ports[i]);
        }

        //阻塞代碼,始終監聽來自用戶端的連接配接請求
        while (true) {
            //擷取我們“感興趣的時間”已經準備好的通道,上面代碼感興趣的是SelectionKey.OP_ACCEPT,這裡擷取的就是SelectionKey.OP_ACCEPT事情類型準備好的通道
            //number為該“感興趣的事件“的通道數量
            int number = selector.select();
            System.out.println("number:" + number);
            if (number > 0) {
                //由于selector中會有多個通道同時準備好,是以這裡selector.selectedKeys()傳回的是一個set集合
                Set<SelectionKey> selectionKeys =  selector.selectedKeys();
                System.out.println("[step2]selectionKeys:" + selectionKeys);
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //由于我們”感興趣“的是SelectionKey.OP_ACCEPT,是以如下判斷
                    if (selectionKey.isAcceptable()) {
                        //selectionKey.channel()傳回是ServerSocketChannel的爺爺類SelectableChannel,是以做強制類型轉換
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);//非阻塞模式

                        //重點重點重點重點重點重點重點重點重點重點
                        //将接收到的channel同樣也注冊到Selector上,Selector<--->channel<--->buffer,三者是雙向的
                        socketChannel.register(selector, SelectionKey.OP_READ);//這時候”感興趣的事件“是讀操作,因為要接收用戶端的資料了
                        //重點重點重點重點重點重點重點重點重點重點
                        //當以上代碼執行完畢後,已經建立了服務端與用戶端的socket連接配接,這時候就要移除Set集合中的selectionKey,以免之後重複建立該selectionKey對應的通道
                        iterator.remove();

                        System.out.println("[step3]成功擷取用戶端的連接配接:" + socketChannel);
                    } else if (selectionKey.isReadable()) {//判斷selectionKey可讀狀态
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        int byteRead = 0;
                        while (true) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
                            byteBuffer.clear();
                            int read = socketChannel.read(byteBuffer);
                            //判斷資料是否讀完
                            if (read <= 0) {
                                socketChannel.register(selector, SelectionKey.OP_READ);
                                break;
                            }

                            //寫回資料,這裡為了簡單:讀取什麼資料,就寫回什麼資料
                            byteBuffer.flip();
                            socketChannel.write(byteBuffer);
                            byteRead += read;
                        }
                        System.out.println("[step4]讀取:" + byteRead + ",來自與:" + socketChannel);

                        //重點重點重點重點重點重點重點重點重點重點
                        //當以上代碼執行完畢後,已經完成了對某一個已經“讀準備好”通道的讀寫操作,這時候就要移除Set集合中的selectionKey,以免之後重複讀寫該selectionKey對應的通道
                        iterator.remove();
                    }
                }
            }
        }
    }


    /*
     * 功能描述: <br> 使用nc指令連接配接服務端:nc 127.0.0.1 5000
     * 〈〉
     * @Param: [args]
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/9/23 12:59
     */
    public static void main(String[] args) throws IOException {

        SelectorTest selectorTest = new SelectorTest(5);
        selectorTest.selectorTest();
    }
}
           

基本思路:

  1. 通過構造方法定義5個監聽端口
  2. 建立Selector,并将已經初始化完成的ServerSocketChannel注冊在Selector上,Selector開始監聽Channel
  3. 構造while死循環(阻塞代碼),始終監聽來自用戶端的請求,通過判斷Selector注冊通道之後傳回的SelectionKey集合中每一個SelectionKey狀态,來處理不同的操作(建立連接配接、讀、寫)

”感興趣的事件“是一個需要特别注意的概念:

主要分為四種,在SelectionKey類中定義為了四個常量

  1. Connect
  2. Accept
  3. Read
  4. Write

Channel向Selector注冊的時候都要給定一個 int 類型的 參數 [ops],代表了監聽“感興趣”的通道類型,說人話就是之後傳回的SelectionKey的狀态。可以是複合狀态:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
           

這是通過一種事件的形式,當某些事件完成後,辨別了Channel的狀态随之改變,是以SelectionKey所代表的Channel的狀态也發生了改變,通過區分判斷不同的狀态,我們知道應該對這些Channel做對應的操作(建立連接配接、讀、寫)。

運作

運作服務端demo,服務端監聽了5個端口

[step1]監聽端口:5000
[step1]監聽端口:5001
[step1]監聽端口:5002
[step1]監聽端口:5003
[step1]監聽端口:5004
           

使用nc指令來連接配接服務端

Java NIO:Selector詳解以及在網絡程式設計中的應用概述示例

服務端5000端口監聽到用戶端建立連接配接的請求并建立連接配接:

[step2]selectionKeys:[[email protected]]
[step3]成功擷取用戶端的連接配接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]
           

用戶端發送消息到服務端,當服務端收到消息後,将消息内容原封不動的傳回給了用戶端

Java NIO:Selector詳解以及在網絡程式設計中的應用概述示例
[step2]selectionKeys:[[email protected]]
[step4]讀取:13,來自與:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]
           

PS.

可以嘗試多建立幾個用戶端,連接配接不同的端口來感受一下代碼思路

需要代碼的來這裡拿嗷:demo項目位址