1. 程式人生 > 實用技巧 >netty前傳:從傳統I/O到Reactor的三種模型

netty前傳:從傳統I/O到Reactor的三種模型

netty前傳:從傳統I/O到Reactor的三種模型

對最近學習netty的一些知識的一個彙總,對前人知識總結的一些個人理解。

一. 傳統阻塞I/O

傳統的阻塞I/O的網路服務端架構如下:

這裡衍生出一些問題:

1.我們在處理客戶端讀到的資料時,會發生阻塞。

2.雖然,我們可以使用多執行緒的方式,來避免第一個問題,但每次收到一個client端的連線請求,就建立一個執行緒,如果client過多,那必然導致執行緒也很多,執行緒的建立、切換、銷燬,消耗都是很大的。

因此,這種模式,不能適用於一些高併發的場景,只適用於一些連線不多,且固定的場景。

二. NIO

正由於傳統I/O的缺點,引申出了NIO,NIO的大致架構如下:

詳解:

1.使用ServerSocketChannel的物件,監聽對應埠,並註冊到Selector中,用於監聽OP_ACCEPT事件。

2.Selector獲取到Client端連線請求,這個過程不阻塞,沒有請求到達的時候,selector.select(time)可以直接返回,然後程式可以去幹自己的事

3.獲取到Client請求後,通過ServerSocketChannel物件,去生成一個SocketChannel物件,並將它註冊到Selector中,用於監聽OP_READ事件。

4.同樣,Selector監聽Client發訊息的讀事件,此過程也不阻塞。收到訊息後,server端讀資料直接從Buffer中獲取,不需要像I/O那樣,使用read(),在那阻塞等待client端的資料。

5.server端拿到資料後,就可以去處理這些資料了,處理完後,Selector繼續監聽新的事件。

Server端的程式碼也很簡單,如下

public class Test {
    public static void main(String[] args) throws Exception {
        new Test().server();
    }

    public void server() throws Exception {
        Selector selector = Selector.open();

        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(5678));
        server.configureBlocking(false);
        server.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            int keys = selector.select();
            if (keys <= 0) {
                continue;
            }

            Set slKeys = selector.selectedKeys();
            Iterator iterator = slKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = (SelectionKey) iterator.next();
                deal(key, server, selector);
                iterator.remove();
            }
        }
    }

    public void deal(SelectionKey key, ServerSocketChannel server, Selector selector) throws IOException {
        if (key.isAcceptable()) {
            SocketChannel channel = server.accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {

            SocketChannel channel = (SocketChannel) key.channel();

            //實際業務資料操作

        }
    }
}

 

這種方式,解決了I/O的幾個問題

1.不需要每個Client連線就建立新的執行緒,一個selector,就可以輪詢的去獲取到client的連線事件,且不阻塞。

2.資料讀取不需要阻塞,有一個Buffer和SocketChannel綁定了,OP_READ事件到達的時候,直接從Buffer中取資料即可。

但是,NIO依舊存在一些問題:

1.編碼過於繁瑣

2.selector還是有瓶頸,如果Client數量過多,那每次拿到請求事件,然後再去把SocketChannel註冊到selector,然後selector得到讀事件,然後再去讀取資料,處理業務,這個過程是同步執行的,如果資料量大,處理業務複雜,那還是會影響到其他的client端

因此這種方式適用的場景依舊是,client不是特別多,並且業務邏輯,資料處理相對簡單的場景,比如聊天系統等。

針對NIO的一些問題,就產生了netty。針對netty,又有必要介紹reactor的三種執行緒模型

三. Reactor的三種執行緒模型

1.單Reactor單執行緒模型

一個執行緒採用selecor執行所有的accept請求、處理所有業務邏輯。

前面介紹的NIO用的就是這種

缺點:1)單個執行緒不能利用多核cpu優勢

   2)不適用高併發場景

3)如果執行緒異常或者有死迴圈,會導致整個系統通訊模組不可用。

2.單Reactor多執行緒模型

一個執行緒通過selector不斷輪詢接收新的請求,將連線交給執行緒池中某個執行緒來處理。

這種方式,其實就是將處理具體業務部分抽離出來,放到執行緒中去處理,但是資料的讀取和傳送,還是在那一個主執行緒中。

缺點同樣是,如果client太多,那麼accept的那個執行緒處理不過來。

3.主從Reactor多執行緒模型(netty就是使用這種)

就是在單reactor多執行緒基礎之上,再弄執行緒,一個執行緒(其實是執行緒池)來專門處理連線請求,一個執行緒(其實是執行緒池)用來處理read和send請求,然後具體的業務,再按照單reactor多執行緒的方式處理。

介紹了這麼多,其實Reactor的這些模型就是在原始NIO基礎之上形成的,總結來說,就是,一個執行緒池用來處理連線請求(mainReactor),一個執行緒池用來處理read和write(subReactor),另外的執行緒池用來處理具體業務邏輯