netty極簡教程(五):Netty的Reactor模型演進及JDK nio聊天室實現

介紹了jdk實現nio的關鍵Selector以及SelectableChannel,瞭解了它的原理,就明白了netty爲什麼是事件驅動模型:(netty極簡教程(四):Selector事件驅動以及SocketChannel
的使用
,接下來將它的使用更深入一步, nio reactor模型演進以及聊天室的實現;


示例源碼: https://github.com/jsbintask22/netty-learning

nio server

對於io消耗而言,我們知道提升效率的關鍵在於服務端對於io的使用;而nio壓榨cpu的關鍵在於使用Selector實現的reactor事件模型以及多線程的加入時機:

單線程reactor模型


省略Selector以及ServerSocketChannel的獲取註冊; 將所有的操作至於reactor主線程

 while (true) {   // 1
    if (selector.select(1000) == 0) {   // 2
        continue;
    }

    Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();    // 3
    while (selectedKeys.hasNext()) {
        SelectionKey selectionKey = selectedKeys.next();
        SelectableChannel channel = selectionKey.channel();

        if (selectionKey.isAcceptable()) {    // 4
            ServerSocketChannel server = (ServerSocketChannel) channel;
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
            String serverGlobalInfo = "系統消息:用戶[" + client.getRemoteAddress() + "]上線了";
            System.err.println(serverGlobalInfo);

            forwardClientMsg(serverGlobalInfo, client);   //  5
        } else if (selectionKey.isReadable()) {

                SocketChannel client = (SocketChannel) channel;
                SocketAddress remoteAddress = null;
                try {
                    remoteAddress = client.getRemoteAddress();
                    String clientMsg = retrieveClientMsg(selectionKey);
                    if (clientMsg.equals("")) {
                        return;
                    }
                    System.err.println("收到用戶[" + remoteAddress + "]消息:" + clientMsg);

                    forwardClientMsg("[" + remoteAddress + "]:" + clientMsg, client);   // 6
                } catch (Exception e) {
                    String msg = "系統消息:" + remoteAddress + "下線了";
                    forwardClientMsg(msg, client);            
                    System.err.println(msg);
                    selectionKey.cancel();    // 7
                    try {
                        client.close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
        }

        selectedKeys.remove();
    }
}
  1. 開啓一個while循環,讓Selector不斷的詢問操作系統是否有對應的事件已經準備好
  2. Selector檢查事件(等待時間爲1s),如果沒有直接開啓下一次循環
  3. 獲取已經準備好的事件(SelectionKey),然後依次循環遍歷處理
  4. 如果是Accept事件,說明是ServerSocketChannel註冊的,說明新的連接已經建立好了,從中獲取新的連接並將新連接再次註冊到Selector
  5. 註冊後,然後生成消息給其它Socket,表示有新用戶上線了
  6. 如果是Read事件,說明客戶端Socket有新的數據可讀取,讀取然後廣播該消息到其它所有客戶端
  7. 如果發生異常,表示該客戶端斷開連接了(粗略的處理),同樣廣播一條消息,並且將該Socket從Selector上註銷

讀取以及廣播消息方法如下:

SocketChannel client = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
        int len = client.read(buffer);
        if (len == 0) {
            return "";
        }
        buffer.flip();
        byte[] data = new byte[buffer.remaining()];
        int index = 0;
        while (len != index) {
            data[index++] = buffer.get();
        }
        buffer.clear();
        return new String(data, StandardCharsets.UTF_8);

Set<SelectionKey> allClient = selector.keys();
allClient.forEach(selectionKey -> {
    SelectableChannel channel = selectionKey.channel();
    if (!(channel instanceof ServerSocketChannel) && channel != client) {  // 1
        SocketChannel otherClient = (SocketChannel) channel;
        try {
            otherClient.write(ByteBuffer.wrap(clientMsg.getBytes(StandardCharsets.UTF_8)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

從Selector上獲取所有註冊的Channel然後遍歷,如果不是ServerSocketChannel或者當前消息的Channel,就將消息發送出去.


以上,所有代碼放在同一線程中,對於單核cpu而言,相比於bio的Socket編程,我們主要有一個方面的改進

  • 雖然accept方法依然是阻塞的,可是我們已經知道了肯定會有新的連接進來,所以調用改方法不會再阻塞而是直接獲取一個新連接
  • 對於read方法而言同樣如此,雖然該方法依然是一個阻塞的方法,可是我們已經知道了接下來調用必定會有有效數據,這樣cpu不用再進行等待
  • 通過Selector在一個線程中便管理了多個Channel

而對於多核cpu而言,Selector雖然能夠有效規避accept和read的無用等待時間,可是它依然存在一些問題;

  1. 上面的操作關鍵在於Selector的select操作,該方法必須能夠快速循環調用,不宜和其它io讀取寫入放在一起
  2. channel的io(read和write)操作較爲耗時,不宜放到同一線程中處理

多線程reactor模型


基於上面的單線程問題考慮,我們可以將io操作放入線程池中處理:

  1. 將accept事件的廣播放入線程池中處理
  2. 將read事件的所有io操作放入線程池中處理
if (selectionKey.isAcceptable()) {
        ServerSocketChannel server = (ServerSocketChannel) channel;
        SocketChannel client = server.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
        String serverGlobalInfo = "系統消息:用戶[" + client.getRemoteAddress() + "]上線了";
        System.err.println(serverGlobalInfo);

        executorService.submit(() -> {    // 1
            forwardClientMsg(serverGlobalInfo, client);
        });
    } else if (selectionKey.isReadable()) {

        executorService.submit(() -> {    // 2
            SocketChannel client = (SocketChannel) channel;
            SocketAddress remoteAddress = null;
            try {
                remoteAddress = client.getRemoteAddress();
                String clientMsg = retrieveClientMsg(selectionKey);
                if (clientMsg.equals("")) {
                    return;
                }
                System.err.println("收到用戶[" + remoteAddress + "]消息:" + clientMsg);

                forwardClientMsg("[" + remoteAddress + "]:" + clientMsg, client);  
            } catch (Exception e) {
                String msg = "系統消息:" + remoteAddress + "下線了";
                forwardClientMsg(msg, client);
                System.err.println(msg);
                selectionKey.cancel();
                try {
                    client.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
    }

    selectedKeys.remove();
}

在 1與2處,我們加入了線程池處理,不再在reactor主線程中做任何io操作。 這便是reactor多線程模型


雖然模型2有效利用了多核cpu優勢,可是依然能夠找到瓶頸

  • 雖然廣播消息是在一個獨立線程中,可是我們需要將Selector上註冊的所有的channel全部遍歷,如果Selector註冊了太多的channel,依舊會有效率問題
  • 因爲Selector註冊了過多的Channel,所以在進行select選取時對於主線程而言依舊會有很多的循環操作,存在瓶頸

基於以上問題,我們可以考慮引入多個Selector,這樣主Selector只負責讀取accept操作,而其他的io操作均有子Selector負責,這便是多Reactor多線程模型

多Reactor多線程模型

基於上面的思考,我們要在單Reactor多線程模型上主要需要以下操作

  1. 對於accept到的新連接不再放入主Selector,將其加入多個子Selector
  2. 子Selector操作應該在異步線程中進行.
  3. 所有子Selector只進行read write操作

基於以上,會增加一個子Selector列表,並且將原來的accept以及讀取廣播分開;
private List<Selector> subSelector = new ArrayList<>(8); 定義一個包含8個子selector的列表並進行初始化


如圖,分別開啓了一個reactor主線程,以及8個子selector子線程,其中,主線程現在只進行accept然後添加至子selector

 while (true) {
    if (mainSelector.select(1000) == 0) {
        continue;
    }

    Iterator<SelectionKey> selectedKeys = mainSelector.selectedKeys().iterator();
    while (selectedKeys.hasNext()) {
        SelectionKey selectionKey = selectedKeys.next();
        SelectableChannel channel = selectionKey.channel();

        if (selectionKey.isAcceptable()) {

            ServerSocketChannel server = (ServerSocketChannel) channel;
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(subSelector.get(index++), SelectionKey.OP_READ,     // 1
                    ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
            if (index == 8) {   // 2
                index = 0;
            }

            String serverGlobalInfo = "系統消息:用戶[" + client.getRemoteAddress() + "]上線了";
            System.err.println(serverGlobalInfo);

            forwardClientMsg(serverGlobalInfo, client);
        }
    }

    selectedKeys.remove();
}
  1. 將新連接註冊至從Selector.
  2. 如果當前的selector已經全部添加了一遍則重新從第一個開始

所有的從Selector只進行io操作,並且本身已經在異步線程中運行

while (true) {
    if (subSelector.select(1000) == 0) {
        continue;
    }

    Iterator<SelectionKey> selectedKeys = subSelector.selectedKeys().iterator();
    while (selectedKeys.hasNext()) {
        SelectionKey selectionKey = selectedKeys.next();
        SelectableChannel channel = selectionKey.channel();

        if (selectionKey.isReadable()) {
            SocketChannel client = (SocketChannel) channel;
            SocketAddress remoteAddress = null;
            try {
                remoteAddress = client.getRemoteAddress();
                String clientMsg = retrieveClientMsg(selectionKey);  // 1
                if (clientMsg.equals("")) {
                    return;
                }
                System.err.println("收到用戶[" + remoteAddress + "]消息:" + clientMsg);
            
                forwardClientMsg("[" + remoteAddress + "]:" + clientMsg, client);  // 2
            } catch (Exception e) {
                String msg = "系統消息:" + remoteAddress + "下線了";
                forwardClientMsg(msg, client);
                System.err.println(msg);
                selectionKey.cancel();
                try {
                    client.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }

        selectedKeys.remove();
    }
  1. 讀取消息
  2. 廣播消息
    啓動server,並且打開三個客戶端:





    如圖所示,上線通知,消息轉發,下線通知成功, 主Selector與從Selector交互成功

netty線程模型思考

事實上,在netty的線程模型中,與上方的多Reactor多線程模型類似,一個改進版的多路複用多Reactor模型; Reactor主從線程模型

  1. 一個主線程不斷輪詢進行accept操作,將channel註冊至子Selector
  2. 一個線程持有一個Selector
  3. 一個子Selector又可以管理多個channel
  4. 在斷開連接前,一個channel總是在同一個線程中進行io操作處理

基於以上思考,我們將在後面在netty源碼中進行一一驗證。

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