5、深入剖析Java NIO之Selector(多路複用器)

章節概覽

Netty源碼分析章節概覽


1、概述

關於多路複用的基本原理,在大白話分析BIO,NIO,AIO中簡單的介紹了關於多路複用技術的理解。這章節,我們深入理解分析多路複用技術。以及JDK的部分源碼作爲參考。


2、多路複用快速認知

爲了快速理解多路複用技術,我們以生活中的小案例進行說明。老張開大排檔,剛剛起步的時候,客人比較少。接待,炒菜,上菜都是老張一個人負責。老張的手藝不錯,炒出來的菜味道可以。客人越來越多,每來個客人,老張都得花時間去接待,忙不過來。於是老張就招了服務員,服務員收集每桌需要點的菜,然後把菜單交給老張,老張只負責做菜即可。在這裏,服務員就充當了選擇器,客戶把自己的要求告訴服務員,服務員告訴老張。
在這裏插入圖片描述


3、深入理解Linux底層epoll的實現原理

首先我們觀察下Linux底層epoll的3個實現函數:

  1. int epoll_create(int size);
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

epoll_create:創建一個epoll對象。參數size是內核保證能處理最大的文件句柄數,在socket編程裏面就是處理的最大連接數。返回的int代表當前的句柄指針,當然創建一個epoll對象的時候,也會相應的消耗一個fd,所以在使用完成的時候,一定要關閉,不然會耗費大量的文件句柄資源。

epoll_ctl:可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。其中epfd,就是創建的文件句柄指針,op是要做的操作,例如刪除,更新等,event 就是我們需要監控的事件。

epoll_wait:在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。

epoll的高效就在於,當我們調用epoll_ctl往裏塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄發送給用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。

那麼,這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。(當網卡里面有數據的時候,會發起硬件中斷,提醒內核有數據到來可以拷貝數據。當網卡通知內核有數據的時候,會產生一個回調函數,這個回調函數是epoll_ctl創建的時候,向內核裏面註冊的。回調函數會把當前有數據的socket(文件句柄)取出,放到list列表中。這樣就可以把存放着數據的socket發送給用戶態,減少遍歷的時間,和數據的拷貝)


4、java NIO 編程詳解

4.1 NIOClient
public class NIOClient {

    /*標識數字*/
    private static int flag = 0;
    /*緩衝區大小*/
    private static int BLOCK = 4096;
    /*接受數據緩衝區*/
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    /*發送數據緩衝區*/
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
    /*服務器端地址*/
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
            "localhost", 8888);

    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        // 打開socket通道
        SocketChannel socketChannel = SocketChannel.open();
        // 設置爲非阻塞方式
        socketChannel.configureBlocking(false);
        // 打開選擇器
        Selector selector = Selector.open();
        // 註冊連接服務端socket動作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 連接
        socketChannel.connect(SERVER_ADDRESS);
        // 分配緩衝區大小內存

        Set<SelectionKey> selectionKeys;
        Iterator<SelectionKey> iterator;
        SelectionKey selectionKey;
        SocketChannel client;
        String receiveText;
        String sendText;
        int count=0;

        while (true) {
            //選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。
            //此方法執行處於阻塞模式的選擇操作。
            selector.select();
            //返回此選擇器的已選擇鍵集。
            selectionKeys = selector.selectedKeys();
            //System.out.println(selectionKeys.size());
            iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                selectionKey = iterator.next();
                if (selectionKey.isConnectable()) {
                    System.out.println("client connect");
                    client = (SocketChannel) selectionKey.channel();
                    // 判斷此通道上是否正在進行連接操作。
                    // 完成套接字通道的連接過程。
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                        System.out.println("完成連接!");
                        sendbuffer.clear();
                        sendbuffer.put("Hello,Server".getBytes());
                        sendbuffer.flip();
                        client.write(sendbuffer);
                    }
                    client.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    client = (SocketChannel) selectionKey.channel();
                    //將緩衝區清空以備下次讀取
                    receivebuffer.clear();
                    //讀取服務器發送來的數據到緩衝區中
                    count=client.read(receivebuffer);
                    if(count>0){
                        receiveText = new String( receivebuffer.array(),0,count);
                        System.out.println("客戶端接受服務器端數據--:"+receiveText);
                        client.register(selector, SelectionKey.OP_WRITE);
                    }

                } else if (selectionKey.isWritable()) {
                    sendbuffer.clear();
                    client = (SocketChannel) selectionKey.channel();
                    sendText = "message from client--" + (flag++);
                    sendbuffer.put(sendText.getBytes());
                    //將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
                    sendbuffer.flip();
                    client.write(sendbuffer);
                    System.out.println("客戶端向服務器端發送數據--:"+sendText);
                    client.register(selector, SelectionKey.OP_READ);
                }
            }
            selectionKeys.clear();
        }
    }
}

4.2、NIOServer
public class NIOServer {

    /*標識數字*/
    private  int flag = 0;
    /*緩衝區大小*/
    private  int BLOCK = 4096;
    /*接受數據緩衝區*/
    private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    /*發送數據緩衝區*/
    private  ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);

    private Selector selector;

    public NIOServer(int port) throws IOException {

        /**
         * 以下的所有說明均已linux系統底層進行說明:
         *      nio 的底層實現是 epoll 模式,採用多路複用技術,對nio的代碼進行深入分析,結合epoll的底層實現
         * 進行詳細的說明
         *      1.linux網絡編程是兩個進程之間的通信,跨集羣合網絡
         *      2.開啓一個socket線程,在linux系統上任何操作均以文件句柄數表示,默認情況下
         *        一個線程可以打開1024個句柄,也就說最多同時支持1024個網絡連接請求。阿里雲默認打開65535個文件
         *        句柄,通常情況下,1G內存最多可以打開10w個句柄數
         *
         *
         */

        // 打開服務器套接字通道
        // 底層: 在linux上面開啓socket服務,啓動一個線程。綁定ip地址和端口號
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 服務器配置爲非阻塞
        serverSocketChannel.configureBlocking(false);
        // 檢索與此通道關聯的服務器套接字
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 進行服務的綁定
        serverSocket.bind(new InetSocketAddress(port));
        // 通過open()方法找到Selector
        // 底層: 開啓epoll,爲當前socket服務創建epoll服務,epoll_create
        selector = Selector.open();
        // 註冊到selector,等待連接
        /**
         * 底層:
         *      1.將當前的epoll,服務器地址,端口號綁定,如果有連接請求,直接添加到epoll中,epoll的底層是紅黑樹,
         *  可以快速的實現連接的查找和狀態更新。如果有新的連接過來,直接存放到epoll中。如果有連接過期,中斷,
         *  會從epoll中刪除。
         *      2.通過epoll_ctl添加到epoll的同時,會註冊一個回調函數給內核,當網卡有數據來的時候,會通知內核,內核
         *      調用回調函數,將當前內核數據的事件狀態添加到list鏈表中
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server Start----8888:");
    }


    // 監聽
    private void listen() throws IOException {
        while (true) {
            // 選擇一組鍵,並且相應的通道已經打開
            /**
             * epoll底層維護一個鏈表,rdlist,基於事件驅動模式,當網卡有數據請求過來,會發起硬件中斷,通知內核已經有來了。內核調用
             * 回調函數,將當前的事件添加到rdlist中,將當前可用的rdlist列表發送給用戶態,用戶去遍歷rdlist中的事件,進行處理
             */
            selector.select();
            // 返回此選擇器的已選擇鍵集。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 獲得當前epoll的rdlist複製到用戶態,遍歷,同事刪除當前rdlist中的事件
                iterator.remove();
                handleKey(selectionKey);
            }
        }
    }

    // 處理請求
    private void handleKey(SelectionKey selectionKey) throws IOException {
        // 接受請求
        ServerSocketChannel server = null;
        SocketChannel client = null;
        String receiveText;
        String sendText;
        int count=0;
        // 測試此鍵的通道是否已準備好接受新的套接字連接。
        if (selectionKey.isAcceptable()) {
            // 返回爲之創建此鍵的通道。
            server = (ServerSocketChannel) selectionKey.channel();
            // 接受到此通道套接字的連接。
            // 此方法返回的套接字通道(如果有)將處於阻塞模式。
            client = server.accept();
            // 配置爲非阻塞
            client.configureBlocking(false);
            // 註冊到selector,等待連接
            client.register(selector, SelectionKey.OP_READ);
        } else if (selectionKey.isReadable()) {
            // 返回爲之創建此鍵的通道。
            client = (SocketChannel) selectionKey.channel();
            //將緩衝區清空以備下次讀取
            receivebuffer.clear();
            //讀取服務器發送來的數據到緩衝區中
            count = client.read(receivebuffer);
            if (count > 0) {
                receiveText = new String( receivebuffer.array(),0,count);
                System.out.println("服務器端接受客戶端數據--:"+receiveText);
                client.register(selector, SelectionKey.OP_WRITE);
            }
        } else if (selectionKey.isWritable()) {
            //將緩衝區清空以備下次寫入
            sendbuffer.clear();
            // 返回爲之創建此鍵的通道。
            client = (SocketChannel) selectionKey.channel();
            sendText="message from server--" + flag++;
            //向緩衝區中輸入數據
            sendbuffer.put(sendText.getBytes());
            //將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
            sendbuffer.flip();
            //輸出到通道
            client.write(sendbuffer);
            System.out.println("服務器端向客戶端發送數據--:"+sendText);
            client.register(selector, SelectionKey.OP_READ);
        }
    }

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        int port = 8888;
        NIOServer server = new NIOServer(port);
        server.listen();
    }
}

以上是簡單的NIO 客戶端和服務端進行通信的demo。具體過程都已經註解說明。


5、小結

本章節詳細的描述了多路複用技術的底層原理,以及實現了nio的demo,並且在nio基礎上配合底層epoll進行了詳解。如有問題歡迎諮詢。本文參考了大量的博客,由於時間已久,當時沒有記錄博客的來源,這裏說聲感謝。如果需要備註博客,歡迎博客作者提醒,謝謝!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章