[Netty學習筆記]四、NIO核心組件Selector

Selector(選擇器)

Selector能夠檢測多個註冊的通道上是否有事件發生(多個channel以事件的方式可以註冊到同一個Selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個鏈接和請求。

只有在連接通道真正有讀寫事件發生時,纔會進行讀寫,這就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程了。並且避免了多線程之間的上下文切換導致的開銷。

在這裏插入圖片描述
圖解說明:

  1. 多路複用器Selector可以同時併發處理多個客戶端連接

  2. 當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務

  3. 線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出

    通道。

  4. 由於讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由於頻繁 I/O 阻塞導致的線程掛

    起。

  5. 一個 I/O 線程可以併發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接一線

    程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

Selector類相關方法

Selector類是一個抽象類,常用方法如下:

public static Selector open();//得到一個選擇器對象
public int select(long timeout);//監控所有註冊的通道,當其中有IO操作可以進行時,將對應的SelectionKey加入到內部集合中並返回,參數用來設置超時時間,即阻塞xx毫秒,在xx毫秒後返回
public Set<SelectionKey> selectedKeys();//從內部集合中得到所有的SelectionKey
public abstract int select() throws IOException;//阻塞
public abstract Selector wakeup();//喚醒Selector
public abstract int selectNow() throws IOException;//不阻塞,立馬返回
NIO非阻塞網絡編程關係梳理圖

圖片來自網絡
圖解說明:

  1. Selector進行監聽select方法,返回有事件發生的通道的個數
  2. 當客戶端連接時,會通過ServerSocketChannel得到SocketChannel
  3. 將socketChannel註冊到Selector上(一個Selector上可以註冊多個SocketChannel)
  4. 註冊後返回一個SelectionKey,會和該selector關聯
  5. 利用連接到服務器上的SelectionKey來判斷事件類型
  6. 判斷了事件之後,如是讀、寫事件,則根據SelectionKey獲取SocketChannel,進行業務處理。
SelectionKey

SelectionKey表示selector和網絡通道的註冊關係,分爲四種:

  • SelectionKey.OP_ACCEPT —— 接收連接繼續事件,表示服務器監聽到了客戶連接,服務器可以接收這個連接了

  • SelectionKey.OP_CONNECT —— 連接就緒事件,表示客戶端與服務器的連接已經建立成功

  • SelectionKey.OP_READ —— 讀就緒事件,表示通道中已經有了可讀的數據,可以執行讀操作了(通道目前有數據,可以進行讀操作了)

  • SelectionKey.OP_WRITE —— 寫就緒事件,表示已經可以向通道寫數據了(通道目前可以用於寫操作)

SelectionKey中常用的方法:

public abstract Selector selector();//得到與該Selectionkey關聯的Selector對象
public abstract SelectableChannel channel();//得到與該Selectionkey關聯的通道
public final Object attachment();//得到與該SelectionKey關聯的共享數據
public abstract SelectionKey intersetOps(int ops);//設置或改變監聽事件
public final boolean isAcceptable();//是否可以連接
public final boolean isReadable();//是否可以讀
public final boolean isWriteable();//是否可以寫
ServerSocketChannel類

ServerSocketChannel在服務器端監聽新的客戶端Socket連接

ServerSocketChannel常用方法:

public static ServerSocketChannel open() throws IOException;//得到一個ServerSocketChannel通道
public final ServerSocketChannel bind(SocketAddress local) throws IOException;//設置服務器端口號
public final SelectableChannel configureBlocking(boolean block);//設置阻塞或非阻塞模式,false表示非阻塞模式
public abstract SocketChannel accept() throws IOException;//接受一個連接,返回代表這個連接的通道對象
 public final SelectionKey register(Selector sel, int ops);//註冊一個選擇器並設置監聽事件
 public final SelectionKey register(Selector sel, int ops,Object att);//註冊一個選擇器並設置監聽事件,最後一個參數可以設置共享數據
SocketChannel類

SocketChannel,網絡 IO 通道,具體負責進行讀寫操作。NIO 把緩衝區的數據寫入通道,或者把通道里的數據讀到緩衝區。

SocketChannel常用方法:

public static ServerSocketChannel open() throws IOException;//得到一個SocketChannel通道
public final SelectableChannel configureBlocking(boolean block);//設置阻塞或非阻塞模式,false表示非阻塞模式
public boolean connect(SocketAddress remote);//連接服務器
public boolean finishConnect();//如果connect連接失敗,那麼要通過這個方法完成連接操作
public int write(ByteBuffer src);//往通道里寫數據
public int read(ByteBuffer det);//從通道里讀數據
public final SelectionKey register(Selector sel, int ops,Object att);//註冊一個選擇器並設置監聽事件,最後一個參數可以設置共享數據
public final void close();//關閉通道
例子

Demo1:利用NIO,實現服務器端和客戶端之間的簡單數據通訊

服務器端

/**
 * 2020/1/8 上午10:09
 * 實現服務器端和客戶端之間的數據通訊(非阻塞)
 */
public class NIOServer {
    private static String IP = "127.0.0.1";
    private static int PORT = 6668;

    public static void main(String[] args) throws Exception {

        Selector selector = Selector.open();

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(PORT));

        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {

            if (selector.select(1000L) == 0) {
                continue;
            }

            //如果客戶端連接上了
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {

                SelectionKey key = iterator.next();

                if (key.isAcceptable()) {
                    SocketChannel channel = serverSocketChannel.accept();
                    System.out.println(" 客 戶 端 連 接 成 功 生 成 了 一 個 socketChannel " +
                            channel.hashCode());
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
                }

                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    socketChannel.read(buffer);
                    System.out.println("from 客戶端:" + new String(buffer.array()).trim());
                }

                iterator.remove();

            }


        }

    }
}

selector輪詢事件,當客戶端連接上服務之後,即產生一個OP_ACCEPT事件,服務器輪詢到該事件後,獲取到當前客戶端連接的通道channel,併爲通道註冊Read事件。 selector隨後會輪詢到Read事件,便會執行Read事件的業務邏輯,比如向通道中寫數據。這樣客戶端就能讀取到通道中寫入的數據,即實現簡單的通訊。

客戶端

public class NIOClient {
    private static String IP = "127.0.0.1";
    private static int PORT = 6668;

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetAddress = new InetSocketAddress(IP, PORT);
        if (!socketChannel.connect(inetAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("因爲連接需要時間,客戶端不會阻塞,可以做其它工作..");
            }
            String str = "Hello";
            ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
            socketChannel.write(buffer);
          //客戶端阻塞在這裏
            System.in.read();
        }
    }
}

客戶端主要是執行連接服務器的操作

Demo2:實現一個簡單的服務器端與客戶端的羣聊程序

服務器端:

/**
 * 2020/1/8 上午11:18
 * 實現服務器端與客戶端之間的數據簡單通訊
 * 服務器端:檢測用戶上線、離線 並實現消息轉發功能
 */
public class NIOChatServer {

    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    public NIOChatServer() throws Exception {

        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(6669));

        selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

    }

    public static void main(String[] args) throws Exception {

        NIOChatServer server = new NIOChatServer();
        server.chat();
    }

    private void chat() throws Exception {

        String user = null;

        while (true) {

            int connectNum = selector.select();
            if (connectNum <= 0) {
                continue;
            }


            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {

                SelectionKey key = iterator.next();

                if (key.isAcceptable()) {

                    SocketChannel socketChannel = serverSocketChannel.accept();

                    System.out.println(socketChannel.getRemoteAddress() + "已經上線");

                    socketChannel.configureBlocking(false);

                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                }

                if (key.isReadable()) {
                    readMsg(key);
                }

                iterator.remove();


            }


        }

    }

    private void readMsg(SelectionKey key) {

        SocketChannel channel = null;
        try {

            channel = (SocketChannel) key.channel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);

            int len = channel.read(buffer);
            if (len > 0) {
                String content = new String(buffer.array(), 0, len);
                System.out.println(content.trim());
                sendMsgToOtherChannel(content,channel);
            }

        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 離線了..");
                key.cancel();
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }

        }

    }

	//發送消息給其他客戶端
    private void sendMsgToOtherChannel(String msg, SocketChannel self) throws IOException {
    	//獲取當前選擇器中發生的事件
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey key : keys) {
			//每個事件與channel是對應的,如果獲取到的Channel不是當前客戶端端的channel,那麼可以認爲是其他客戶端,就可以發送消息
            SelectableChannel targetChannel = key.channel();

            if (targetChannel instanceof SocketChannel && targetChannel != self) {
				
                SocketChannel socketChannel = (SocketChannel) targetChannel;
                socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
            }


        }
    }

}

客戶端:

/**
 * 2020/1/8 上午11:19
 * 無阻塞地發送消息給其他所有用戶,同時可以接受其他用戶發送的消息(服務器轉發)
 */
public class NIOChatClient {

    private SocketChannel socketChannel;
    private Selector selector;
    private String userName;

    public NIOChatClient() throws Exception {
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6669));
        socketChannel.configureBlocking(false);

        selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_READ);
        userName = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(userName + " is ok...");
    }

    public static void main(String[] args) throws Exception {
        NIOChatClient chatClient = new NIOChatClient();

        new Thread(() -> {
            try {
                while(true){
                    chatClient.readInfo();
                    Thread.sleep(3000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClient.sendInfo(msg);
        }

    }

    public void sendInfo(String info) throws IOException {
        info = userName.concat("說").concat(info);
        socketChannel.write(ByteBuffer.wrap(info.getBytes()));
    }

    public void readInfo() throws IOException {

        while (true) {

            int select = selector.select();
            if (select > 0) {

                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();

                    if (key.isReadable()) {

                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int len = channel.read(buffer);
                        if (len > 0) {
                            String msg = new String(buffer.array(), 0, len);
                            System.out.println(msg.trim());
                        }

                    }

                    iterator.remove();

                }


            }

        }

    }
}

注意:

1.當向通道中註冊SelectionKey.OP_READ事件後,如果客戶端又向緩存中write數據,下次輪詢時,則isReadable()=true;

2.當向通道中註冊SelectionKey.OP_WRITE事件後,如果不設置爲其他事件,這時你會發現當前輪詢線程中isWritable()一直爲ture(解決方式:write業務處理完之後,將通道註冊爲其他事件)

發佈了101 篇原創文章 · 獲贊 22 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章