netty總結

IO網絡相關

I/0 操作 主要分成兩部分
① 數據準備,將數據加載到內核緩存
② 將內核緩存中的數據加載到用戶緩存

I/O的四種模型

Synchronous blocking I/O

 

 Synchronous non-blocking I/O

Asynchronous blocking I/O 

Asynchronous non-blocking I/O 

 

堵塞、非堵塞的區別是在於第一階段,即數據準備階段。無論是堵塞還是非堵塞,都是用應用主動找內核要數據,而read數據的過程是‘堵塞’的,直到數據讀取完。

同步、異步的區別在於第二階段,若由請求者主動的去獲取數據,則爲同步操作,需要說明的是:read/write操作也是‘堵塞’的,直到數據讀取完。

若數據的read都由kernel內核完成了(在內核read數據的過程中,應用進程依舊可以執行其他的任務),這就是異步操作。

NIO一個重要的特點是:socket主要的讀、寫、註冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
NIO是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎。 

 I/O 多路複用之select、poll、epoll

I/O多路複用是指使用一個線程來檢查多個文件描述符(Socket)的就緒狀態,比如調用select和poll函數,傳入多個文件描述符,如果有一個文件描述符就緒,則返回,否則阻塞直到超時。得到就緒狀態後進行真正的操作可以在同一個線程裏執行,也可以啓動線程執行(比如使用線程池)。

當一個read操作發生時,它會經歷兩個階段:
1. 等待數據準備 (Waiting for the data to be ready)
2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)

select,poll,epoll都是IO多路複用的機制。I/O多路複用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

select

select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設爲null即可),函數返回。當select函數返回後,可以 通過遍歷fdset,來找到就緒的描述符。

使用方法總共分三步:

1.三個fd_set初始化,用FD_ZERO FD_SET
2.調用select
3.用fd遍歷每一個fd_set使用FD_ISSET。如果成功就處理。 

poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。 和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。

epoll

相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

epoll是直接在內核裏的,用戶調用系統調用去註冊,因此省去了每次的複製和輪詢的消耗。這兒用了三個系統調用,epollcreate只要每次調用開始調用一次創造一個epoll就可以了。然後用epoll_ctl來進行添加事件,其實就是註冊到內核管理的epoll裏。然後直接epoll_wait就可以了。系統會返回系統調用的。

select的缺點:

  1. 單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由於select採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在linux內核頭文件中,有這樣的定義:#define __FD_SETSIZE    1024)
  2. 內核 / 用戶空間內存拷貝問題,select需要複製大量的句柄數據結構,產生巨大的開銷;
  3. select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;
  4. select的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO操作,那麼之後每次select調用還是會將這些文件描述符通知進程。

poll:

優勢:

1.無上限1024。
2.由於它不修改pollfd裏的數據,所以它可以不用每次都填寫了。
3.方便的知道遠程的狀態比如宕機

缺點:

1、還要輪巡
2、不能動態修改set。
其實大多數client不用考慮這個,除非p2p應用。一些server端用不用考慮這個問題。
大多時候他都比select更好。甚至如下場景比epoll還好:

  • 你要跨平臺,因爲epoll只支持linux。
  • socket數目少於1000個。
  • 大於1000但是是socket壽命比較短。
  • 沒有其他線程干擾的時候。

相比select模型,poll使用鏈表保存文件描述符,因此沒有了監視文件數量的限制,但select三個缺點依然存在。

拿select模型爲例,假設我們的服務器需要支持100萬的併發連接,則在__FD_SETSIZE 爲1024的情況下,則我們至少需要開闢1k個進程才能實現100萬的併發連接。除了進程間上下文切換的時間消耗外,從內核/用戶空間大量的無腦內存拷貝、數組輪詢等,是系統難以承受的。因此,基於select模型的服務器程序,要達到10萬級別的併發訪問,是一個很難完成的任務。

epoll:

優點:

  1. 只返回觸發的事件。少了拷貝消耗,迭代輪訓消耗。
  2. 可以綁定更多上下文,不僅僅是socket。
  3. 任何時間處理socket。這些問題都是有內核來處理。了。這個還需要繼續學習啊。
  4. 可以邊緣觸發。
  5. 多線程可以在同一個epoll wait裏等待。

缺點:

  1. 讀寫狀態變更之類的就要麻煩些,在poll裏只要改一個bit就可以了。在這裏面則需要改更多的位數。並且都是system call。
  2. 創建socket也需要兩次系統調用,麻煩。
  3. 只有linux下可以使用
  4. 複雜難調試

適合場景

  1. 多線程,多連接。在單線程還不如poll
  2. 大量線程監控1000上,
  3. 相對長壽命的連接。系統調用會很耗時。
  4. linux依賴的事情。

具體內容參考:http://www.cnblogs.com/duanxz/p/5155926.html

JAVA NIO

Java NIO是在jdk1.4開始使用的,它既可以說成“新I/O”,也可以說成非阻塞式I/O。下面是java NIO的工作原理:

1. 由一個專門的線程來處理所有的 IO 事件,並負責分發。 
2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。 
3. 線程通訊:線程之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的線程切換。 

java nio工作原理圖如下所示:

java NIO採用了雙向通道(channel)進行數據傳輸,而不是單向的流(stream),在通道上可以註冊我們感興趣的事件。一共有以下四種事件:

 

事件名 對應值
服務端接收客戶端連接事件 SelectionKey.OP_ACCEPT(16)
客戶端連接服務端事件 SelectionKey.OP_CONNECT(8)
讀事件 SelectionKey.OP_READ(1)
寫事件 SelectionKey.OP_WRITE(4)

服務端和客戶端各自維護一個管理通道的對象,我們稱之爲selector,該對象能檢測一個或多個通道 (channel) 上的事件。我們以服務端爲例,如果服務端的selector上註冊了讀事件,某時刻客戶端給服務端發送了一些數據,阻塞I/O這時會調用read()方法阻塞地讀取數據,而NIO的服務端會在selector中添加一個讀事件。服務端的處理線程會輪詢地訪問selector。 

Java NIO 由以下幾個核心部分組成:

  • Channels和Buffers: 傳統IO基於字節流和字符流進行操作,而NIO基於Channel和Buffer(緩衝區)進行操作,數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中
  • Selectors:Selector(選擇區)用於監聽多個通道的事件(比如:連接打開,數據到達)

Channel和Buffer有好幾種類型。下面是JAVA NIO中的一些主要Channel的實現:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

以下是Java NIO裏關鍵的Buffer實現:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

這些Buffer覆蓋了你能通過IO發送的基本數據類型:byte, short, int, long, float, double 和 char。

Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。

這是在一個單線程中使用一個Selector處理3個Channel的圖示:

要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。

服務器端NIO使用步驟:

 

參考代碼:

public class ServerConnect
{
    private static final int BUF_SIZE=1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;
    public static void main(String[] args)
    {
        selector();
    }
    public static void handleAccept(SelectionKey key) throws IOException{
        ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));
    }
    public static void handleRead(SelectionKey key) throws IOException{
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buf = (ByteBuffer)key.attachment();
        long bytesRead = sc.read(buf);
        while(bytesRead>0){
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if(bytesRead == -1){
            sc.close();
        }
    }
    public static void handleWrite(SelectionKey key) throws IOException{
        ByteBuffer buf = (ByteBuffer)key.attachment();
        buf.flip();
        SocketChannel sc = (SocketChannel) key.channel();
        while(buf.hasRemaining()){
            sc.write(buf);
        }
        buf.compact();
    }
    public static void selector() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try{
            selector = Selector.open();
            ssc= ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress(PORT));
            ssc.configureBlocking(false);
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            while(true){
                if(selector.select(TIMEOUT) == 0){
                    System.out.println("==");
                    continue;
                }
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey key = iter.next();
                    if(key.isAcceptable()){
                        handleAccept(key);
                    }
                    if(key.isReadable()){
                        handleRead(key);
                    }
                    if(key.isWritable() && key.isValid()){
                        handleWrite(key);
                    }
                    if(key.isConnectable()){
                        System.out.println("isConnectable = true");
                    }
                    iter.remove();
                }
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(selector!=null){
                    selector.close();
                }
                if(ssc!=null){
                    ssc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

 JAVA NIO 的缺點:

  • NIO 的類庫和 API 繁雜,使用麻煩:你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  • 需要具備其他的額外技能做鋪墊:例如熟悉 Java 多線程編程,因爲 NIO 編程涉及到 Reactor 模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的 NIO 程序。
  • 可靠性能力補齊,開發工作量和難度都非常大:例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等。NIO 編程的特點是功能開發相對容易,但是可靠性能力補齊工作量和難度都非常大。
  • JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。官方聲稱在 JDK 1.6 版本的 update 18 修復了該問題,但是直到 JDK 1.7 版本該問題仍舊存在,只不過該 Bug 發生概率降低了一些而已,它並沒有被根本解決。

NIO中epoll空輪詢表現:

public static void main(String[] args) {
        Selector selector = Selector.open();
        System.out.println(selector.isOpen());
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8080);
        socketChannel.bind(inetSocketAddress);
        socketChannel.configureBlocking(false);
        int ops = socketChannel.validOps();
        SelectionKey selectionKey = socketChannel.register(selector, ops, null);
        Set selectedKeys = selector.selectedKeys();
        for (;;) {
            System.out.println("等待...");
            /**
             * 通常是阻塞的,但是在epoll空輪詢的bug中,
             * 之前處於連接狀態突然被斷開,select()的
             * 返回值noOfKeys應該等於0,也就是阻塞狀態
             * 但是,在此bug中,select()被喚醒,而又
             * 沒有數據傳入,導致while (itr.hasNext())
             * 根本不會執行,而後就進入for (;;) {的死循環
             * 但是,正常狀態下應該阻塞,也就是隻輸出一個waiting...
             * 而此時進入死循環,不斷的輸出waiting...,程序死循環
             * cpu自然很快飆升到100%狀態。
             */
            int noOfKeys = selector.select();
            System.out.println("selected keys:" + noOfKeys);
            Iterator itr = selectedKeys.iterator();
            while (itr.hasNext()) {
                SelectionKey key = (SelectionKey) itr.next();
                if (key.isAcceptable()) {
                    SocketChannel client = socketChannel.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("The new connection is accepted from the client: " + client);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    client.read(buffer);
                    String output = new String(buffer.array()).trim();
                    System.out.println("Message read from client: " + output);
                    if (output.equals("Bye Bye")) {
                        client.close();
                        System.out.println("The Client messages are complete; close the session.");
                    }
                }
                itr.remove();
            }
        }
    }

在部分Linux的2.6的kernel中,poll和epoll對於突然中斷的連接socket會對返回的eventSet事件集合置爲POLLHUP,也可能是POLLERR,eventSet事件集合發生了變化,這就可能導致Selector會被喚醒。

netty的解決辦法:

long currentTimeNanos = System.nanoTime();
for (;;) {
    // 1.定時任務截止事時間快到了,中斷本次輪詢
    ...
    // 2.輪詢過程中發現有任務加入,中斷本次輪詢
    ...
    // 3.阻塞式select操作
    selector.select(timeoutMillis);
    // 4.解決jdk的nio bug
    long time = System.nanoTime();
    if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
        selectCnt = 1;
    } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {

        rebuildSelector();
        selector = this.selector;
        selector.selectNow();
        selectCnt = 1;
        break;
    }
    currentTimeNanos = time; 
    ...
 }

netty 會在每次進行 selector.select(timeoutMillis) 之前記錄一下開始時間currentTimeNanos,在select之後記錄一下結束時間,判斷select操作是否至少持續了timeoutMillis秒(這裏將time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或許更好理解一些),
如果持續的時間大於等於timeoutMillis,說明就是一次有效的輪詢,重置selectCnt標誌,否則,表明該阻塞方法並沒有阻塞這麼長時間,可能觸發了jdk的空輪詢bug,當空輪詢的次數超過一個閥值的時候,默認是512,就開始重建selector

Reactor模型

Reactor模型

在Reactor模式中,有5個關鍵的參與者。

  • Handle(句柄或描述符,在Windows下稱爲句柄,在Linux下稱爲描述符):本質上表示一種資源(比如說文件描述符,或是針對網絡編程中的socket描述符),是由操作系統提供的;該資源用於表示一個個的事件,事件既可以來自於外部,也可以來自於內部;外部事件比如說客戶端的連接請求,客戶端發送過來的數據等;內部事件比如說操作系統產生的定時事件等。它本質上就是一個文件描述符,Handle是事件產生的發源地。
  • Synchronous Event Demultiplexer(同步事件分離器):它本身是一個系統調用,用於等待事件的發生(事件可能是一個,也可能是多個)。調用方在調用它的時候會被阻塞,一直阻塞到同步事件分離器上有事件產生爲止。對於Linux來說,同步事件分離器指的就是常用的I/O多路複用機制,比如說select、poll、epoll等。在Java NIO領域中,同步事件分離器對應的組件就是Selector;對應的阻塞方法就是select方法。
  • Event Handler(事件處理器):本身由多個回調方法構成,這些回調方法構成了與應用相關的對於某個事件的反饋機制。在Java NIO領域中並沒有提供事件處理器機制讓我們調用或去進行回調,是由我們自己編寫代碼完成的。Netty相比於Java NIO來說,在事件處理器這個角色上進行了一個升級,它爲我們開發者提供了大量的回調方法,供我們在特定事件產生時實現相應的回調方法進行業務邏輯的處理,即,ChannelHandler。ChannelHandler中的方法對應的都是一個個事件的回調。
  • Concrete Event Handler(具體事件處理器):是事件處理器的實現。它本身實現了事件處理器所提供的各種回調方法,從而實現了特定於業務的邏輯。它本質上就是我們所編寫的一個個的處理器實現。
  • Initiation Dispatcher(初始分發器):實際上就是Reactor角色。它本身定義了一些規範,這些規範用於控制事件的調度方式,同時又提供了應用進行事件處理器的註冊、刪除等設施。它本身是整個事件處理器的核心所在,Initiation Dispatcher會通過Synchronous Event Demultiplexer來等待事件的發生。一旦事件發生,Initiation Dispatcher首先會分離出每一個事件,然後調用事件處理器,最後調用相關的回調方法來處理這些事件。Netty中ChannelHandler裏的一個個回調方法都是由bossGroup或workGroup中的某個EventLoop來調用的。

① 初始化Initiation Dispatcher,然後將若干個Concrete Event Handler註冊到Initiation Dispatcher中。當應用向Initiation Dispatcher註冊Concrete Event Handler時,會在註冊的同時指定感興趣的事件,即,應用會標識出該事件處理器希望Initiation Dispatcher在某些事件發生時向其發出通知,事件通過Handle來標識,而Concrete Event Handler又持有該Handle。這樣,事件 ————> Handle ————> Concrete Event Handler 就關聯起來了。
② Initiation Dispatcher 會要求每個事件處理器向其傳遞內部的Handle。該Handle向操作系統標識了事件處理器。
③ 當所有的Concrete Event Handler都註冊完畢後,應用會調用handle_events方法來啓動Initiation Dispatcher的事件循環。這是,Initiation Dispatcher會將每個註冊的Concrete Event Handler的Handle合併起來,並使用Synchronous Event Demultiplexer(同步事件分離器)同步阻塞的等待事件的發生。比如說,TCP協議層會使用select同步事件分離器操作來等待客戶端發送的數據到達連接的socket handler上。
比如,在Java中通過Selector的select()方法來實現這個同步阻塞等待事件發生的操作。在Linux操作系統下,select()的實現中 a)會將已經註冊到Initiation Dispatcher的事件調用epollCtl(epfd, opcode, fd, events)註冊到linux系統中,這裏fd表示Handle,events表示我們所感興趣的Handle的事件;b)通過調用epollWait方法同步阻塞的等待已經註冊的事件的發生。不同事件源上的事件可能同時發生,一旦有事件被觸發了,epollWait方法就會返回;c)最後通過發生的事件找到相關聯的SelectorKeyImpl對象,並設置其發生的事件爲就緒狀態,然後將SelectorKeyImpl放入selectedSet中。這樣一來我們就可以通過Selector.selectedKeys()方法得到事件就緒的SelectorKeyImpl集合了。
④ 當與某個事件源對應的Handle變爲ready狀態時(比如說,TCP socket變爲等待讀狀態時),Synchronous Event Demultiplexer就會通知Initiation Dispatcher。
⑤ Initiation Dispatcher會觸發事件處理器的回調方法,從而響應這個處於ready狀態的Handle。當事件發生時,Initiation Dispatcher會將被事件源激活的Handle作爲『key』來尋找並分發恰當的事件處理器回調方法。
⑥ Initiation Dispatcher會回調事件處理器的handle_event(type)回調方法來執行特定於應用的功能(開發者自己所編寫的功能),從而相應這個事件。所發生的事件類型可以作爲該方法參數並被該方法內部使用來執行額外的特定於服務的分離與分發。

Reactor模型的實現方式

單線程Reactor模式

使用工作者線程池

 

與單線程Reactor模式不同的是,添加了一個工作者線程池,並將非I/O操作從Reactor線程中移出轉交給工作者線程池來執行。這樣能夠提高Reactor線程的I/O響應,不至於因爲一些耗時的業務邏輯而延遲對後面I/O請求的處理。

使用線程池的優勢:
① 通過重用現有的線程而不是創建新線程,可以在處理多個請求時分攤在線程創建和銷燬過程產生的巨大開銷。
② 另一個額外的好處是,當請求到達時,工作線程通常已經存在,因此不會由於等待創建線程而延遲任務的執行,從而提高了響應性。
③ 通過適當調整線程池的大小,可以創建足夠多的線程以便使處理器保持忙碌狀態。同時還可以防止過多線程相互競爭資源而使應用程序耗盡內存或失敗。

注意,在上圖的改進的版本中,所以的I/O操作依舊由一個Reactor來完成,包括I/O的accept()、read()、write()以及connect()操作。

多Reactor線程模式

Reactor線程池中的每一Reactor線程都會有自己的Selector、線程和分發的事件循環邏輯。
mainReactor可以只有一個,但subReactor一般會有多個。mainReactor線程主要負責接收客戶端的連接請求,然後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通信。

流程:
① 註冊一個Acceptor事件處理器到mainReactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣mainReactor會監聽客戶端向服務器端發起的連接請求事件(ACCEPT事件)。啓動mainReactor的事件循環。
② 客戶端向服務器端發起一個連接請求,mainReactor監聽到了該ACCEPT事件並將該ACCEPT事件派發給Acceptor處理器來進行處理。Acceptor處理器通過accept()方法得到與這個客戶端對應的連接(SocketChannel),然後將這個SocketChannel傳遞給subReactor線程池。
③ subReactor線程池分配一個subReactor線程給這個SocketChannel,即,將SocketChannel關注的READ事件以及對應的READ事件處理器註冊到subReactor線程中。當然你也註冊WRITE事件以及WRITE事件處理器到subReactor線程中以完成I/O寫操作。Reactor線程池中的每一Reactor線程都會有自己的Selector、線程和分發的循環邏輯。
④ 當有I/O事件就緒時,相關的subReactor就將事件派發給響應的處理器處理。注意,這裏subReactor線程只負責完成I/O的read()操作,在讀取到數據後將業務邏輯的處理放入到線程池中完成,若完成業務邏輯後需要返回數據給客戶端,則相關的I/O的write操作還是會被提交回subReactor線程來完成。

注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依舊還是在Reactor線程(mainReactor線程 或 subReactor線程)中完成的。Thread Pool(線程池)僅用來處理非I/O操作的邏輯。

多Reactor線程模式將“接受客戶端的連接請求”和“與該客戶端的通信”分在了兩個Reactor線程來完成。mainReactor完成接收客戶端連接請求的操作,它不負責與客戶端的通信,而是將建立好的連接轉交給subReactor線程來完成與客戶端的通信,這樣一來就不會因爲read()數據量太大而導致後面的客戶端連接請求得不到即時處理的情況。並且多Reactor線程模式在海量的客戶端併發請求的情況下,還可以通過實現subReactor線程池來將海量的連接分發給多個subReactor線程,在多核的操作系統中這能大大提升應用的負載和吞吐量。

 

 Netty 與 Reactor模式

mainReactor ———— bossGroup(NioEventLoopGroup) 中的某個NioEventLoop
subReactor ———— workerGroup(NioEventLoopGroup) 中的某個NioEventLoop
acceptor ———— ServerBootstrapAcceptor
ThreadPool ———— 用戶自定義線程池

① 當服務器程序啓動時,會配置ChannelPipeline,ChannelPipeline中是一個ChannelHandler鏈,所有的事件發生時都會觸發Channelhandler中的某個方法,這個事件會在ChannelPipeline中的ChannelHandler鏈裏傳播。然後,從bossGroup事件循環池中獲取一個NioEventLoop來現實服務端程序綁定本地端口的操作,將對應的ServerSocketChannel註冊到該NioEventLoop中的Selector上,並註冊ACCEPT事件爲ServerSocketChannel所感興趣的事件。
② NioEventLoop事件循環啓動,此時開始監聽客戶端的連接請求。
③ 當有客戶端向服務器端發起連接請求時,NioEventLoop的事件循環監聽到該ACCEPT事件,Netty底層會接收這個連接,通過accept()方法得到與這個客戶端的連接(SocketChannel),然後觸發ChannelRead事件(即,ChannelHandler中的channelRead方法會得到回調),該事件會在ChannelPipeline中的ChannelHandler鏈中執行、傳播。
④ ServerBootstrapAcceptor的readChannel方法會該SocketChannel(客戶端的連接)註冊到workerGroup(NioEventLoopGroup) 中的某個NioEventLoop的Selector上,並註冊READ事件爲SocketChannel所感興趣的事件。啓動SocketChannel所在NioEventLoop的事件循環,接下來就可以開始客戶端和服務器端的通信了。

Netty框架

netty介紹

Netty是由JBOSS提供的一個java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

Netty 是一個基於NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶、服務端應用。Netty相當於簡化和流線化了網絡應用的編程開發過程,例如:基於TCP和UDP的socket服務開發。

Channel

Channel

Channel主要負責基本的IO操作(bind(),connect(),read(),write()),它依賴於底層網絡傳輸所提供的原語。Channel相當於Java網絡編程中的Socket。Channel簡化了Socket的使用,並且提供了許多預定義的實現:

  • EmbeddedChannel  
  • LocalServerChannel
  • NioDatagramChannel
  • NioSocketChannel

Channel生命週期:

  • ChannelUnregistered:Channel已經被創建,但還未註冊到EventLoop。
  • ChannelRegistered:Channel已經被註冊到了EventLoop。
  • ChannelActive:Channel處於活動狀態(已經連接到它的遠程節點)。它現在可以接收和發送數據了。
  • ChannelInactive:Channel沒有連接到遠程節點

ChannelPipeline

ChannelUnregistered:Channel已經被創建,但還未註冊到EventLoop。 ChannelRegistered:Channel已經被註冊到了EventLoop。 ChannelActive:Channel處於活動狀態(已經連接到它的遠程節點)。它現在可以接收和發送數據了。 ChannelInactive:Channel沒有連接到遠程節點。

下圖示了一個典型的同時具有入站和出站 ChannelHandler 的 ChannelPipeline 的布 局,並且印證了我們之前的關於 ChannelPipeline 主要由一系列的 ChannelHandler 所組成的 說法。ChannelPipeline 還提供了通過 ChannelPipeline 本身傳播事件的方法。如果一個入站 事件被觸發,它將被從 ChannelPipeline 的頭部開始一直被傳播到 Channel Pipeline 的尾端。 

ChannelHandler

Netty 的主要組件是ChannelHandler,它充當了所有處理入站和出站數據的應用程序邏輯的容器。這是可行的,因爲ChannelHandler 的方法是由網絡事件(其中術語“事件”的使用非常廣泛)觸發的。事實上,ChannelHandler 可專門用於幾乎任何類型的動作,例如將數據從一種格式轉換爲另外一種格式,或者處理轉換過程中所拋出的異常。

 ChannelHandler 可以通過添加、刪除或者替換其他的 ChannelHandler 來實時地修改 ChannelPipeline 的佈局。(它也可以將它自己從 ChannelPipeline 中移除。)這是 ChannelHandler 最重要的能力之一,所以我們將仔細地來看看它是如何做到的。

名 稱 描 述

AddFirstaddBefore

addAfteraddLast

 將一個ChannelHandler 添加到ChannelPipeline 中

remove

 將一個ChannelHandler 從ChannelPipeline 中移除
 replace  將 ChannelPipeline 中的一個 ChannelHandler 替換爲另一個 ChannelHandler

 

ChannelHandlerContext

ChannelHandlerContext代表了一個ChannelHandler和一個ChannelPipeline之間的關係,它在ChannelHandler被添加到ChannelPipeline時被創建。ChannelHandlerContext的主要功能是負責它對應的ChannelHandler和屬於同一個ChannelPipeline的其他ChannelHandler之間的交互。

通過示例說明調用Channel 上的 write()方法將會導致寫入事件從尾端到頭部地流經 ChannelPipeline。

示例一:從 ChannelHandlerContext 訪問 Channel

//獲取到與 ChannelHandlerContext相關聯的 Channel 的引用
ChannelHandlerContext ctx = ..;
Channel channel = ctx.channel();
//通過 Channel 寫入緩衝區
channel.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

通過 ChannelHandlerContext 訪問 ChannelPipeline

//獲取到與 ChannelHandlerContext相關聯的 ChannelPipeline 的引用
ChannelHandlerContext ctx = ..;
ChannelPipeline pipeline = ctx.pipeline();
//通過 ChannelPipeline寫入緩衝區
pipeline.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

 重要的是要注意到,雖然被調用的 Channel 或 ChannelPipeline 上的 write()方法將一直傳播事件通過整個 ChannelPipeline,但是在 ChannelHandler 的級別上,事件從一個 ChannelHandler到下一個 ChannelHandler 的移動是由 ChannelHandlerContext 上的調用完成的。

ChannelHandlerContext的一些方法也存在於Channel和ChannelPipeline上, 但是不同的是,如果調用Channel或者ChannelPipeline上的方法,事件將沿着整 個ChannelPipeline進行傳播。而調用位於ChnnelHandlerContext上的相同方法 ,則將從當前所關聯的ChannelHandler開始,並且只會傳播給位於該 ChannelPipeline中的下一個能夠處理該事件的ChannelHandler。因此 ChannelHandlerContext的方法能夠獲得較短的事件流。

Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContext關係

每一個新創建的 Channel 都將會被分配一個新的 ChannelPipeline。這項關聯是永久性 的;Channel 既不能附加另外一個 ChannelPipeline,也不能分離其當前的。在 Netty 組件 的生命週期中,這是一項固定的操作,不需要開發人員的任何干預。

 在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應, 它們的組成關係如下:

一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表. 這個鏈表的頭是 HeadContext, 鏈表的尾是 TailContext, 並且每個 ChannelHandlerContext 中又關聯着一個 ChannelHandler. 

EventLoop

EventLoop 定義了Netty 的核心抽象,用於處理連接的生命週期中所發生的事件。

Netty使用EventLoop來處理連接上的讀寫事件,而一個連接上的所有請求都保證在一個EventLoop中被處理,一個EventLoop中只有一個Thread,所以也就實現了一個連接上的所有事件只會在一個線程中被執行(一個Channel只會綁定一個EventLoop而一個EventLoop可以被多個Channel綁定)。一個EventLoop相當於一個線程, EventLoopGroup則類似於線程池。

Netty的服務端使用了兩個EventLoopGroup,而第一個EventLoopGroup通常只有一個EventLoop,通常叫做bossGroup,負責客戶端的連接請求,然後打開Channel,交給後面的EventLoopGroup中的一個EventLoop來負責這個Channel上的所有讀寫事件,一個Channel只會被一個EventLoop處理,而一個EventLoop可能會被分配給多個Channel來負責上面的事件。

下面給出一個簡單的例子

Netty Server bootstrap :

EventLoopGroup group = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
 
try {
   ServerBootstrap b = new ServerBootstrap();
   b.group(group, workGroup)
         .channel(NioServerSocketChannel.class)
         .localAddress(new InetSocketAddress(port))
         .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
               ch.pipeline().addLast(new EchoServerHandler());
            }
         });
 
   ChannelFuture f = b.bind().sync();
   System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
   f.channel().closeFuture().sync();
} finally {
   group.shutdownGracefully().sync();
}

netty server handler:

public class EchoServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
 
   @Override
   public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
      ByteBuf in = (ByteBuf) msg;
      System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
      ctx.write(Unpooled.copiedBuffer("Response from server. You have input \"" + in.toString(CharsetUtil.UTF_8) + "\"!", CharsetUtil.UTF_8));
      ctx.flush();
   }
 
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
      cause.printStackTrace();
      ctx.close();
   }
 
}

netty client bootstrap:

EventLoopGroup group = new NioEventLoopGroup();
 
try {
    Bootstrap b = new Bootstrap();
    b.group(group)
     .channel(NioSocketChannel.class)
     .remoteAddress(new InetSocketAddress(host, port))
     .handler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch)
             throws Exception {
             ch.pipeline().addLast(
                     new EchoClientHandler());
         }
     });
 
    ChannelFuture f = b.connect().sync();
    if (f.channel().isActive()) {
        f.channel().writeAndFlush(Unpooled.copiedBuffer("Hello Casper!", CharsetUtil.UTF_8));
    }
 
    Thread.sleep(1000);
 
} finally {
    group.shutdownGracefully().sync();
}

 netty client handler:

public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
 
   @Override
   public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
      System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
   }
 
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
      cause.printStackTrace();
      ctx.close();
   }
 
}

 

 EventLoop是一個接口,它在繼承了ScheduledExecutorService等多個類的同時,僅僅提供了一個方法parent,這個方法返回它屬於哪個EventLoopGroup。

  • SingleThreadEventLoopGroup  一個線程處理所有的Channel
  • ThreadPerChannelLoopGroup  每個線程處理一個channel
  • MultiThreadEventLoopGroup 通過線程組處理channel

根據Selecter的不同實現,不同的處理策略。NIOEventLoopGroup,默認採用sellect方式,參考JAVA NIO實現。EpollEventLoopGroup只能在linux平臺使用,更高效。

 


 

 

這些關係是: 

  • 一個EventLoopGroup 包含一個或者多個EventLoop;
  • 一個EventLoop 在它的生命週期內只和一個Thread 綁定;
  • 所有由EventLoop 處理的I/O 事件都將在它專有的Thread 上被處理;
  • 一個Channel 在它的生命週期內只註冊於一個EventLoop;
  • 一個EventLoop 可能會被分配給一個或多個Channel。

ByteBuf

網絡數據的基本單位總是字節。Java NIO 提供了 ByteBuffer 作爲它 的字節容器,但是這個類使用起來過於複雜,而且也有些繁瑣。

Netty 的 ByteBuffer 替代品是 ByteBuf,一個強大的實現,既解決了 JDK API 的侷限性, 又爲網絡應用程序的開發者提供了更好的 API。

下面是一些 ByteBuf API 的優點:

  •  它可以被用戶自定義的緩衝區類型擴展;
  •  通過內置的複合緩衝區類型實現了透明的零拷貝;
  •  容量可以按需增長(類似於 JDK 的 StringBuilder);
  •  在讀和寫這兩種模式之間切換不需要調用 ByteBuffer 的 flip()方法;
  •  讀和寫使用了不同的索引;
  •  支持方法的鏈式調用;
  • 支持引用計數;
  • 支持池化。

ByteBuf 維護了兩個不同的索引:一個用於讀取,一個用於寫入。當你從 ByteBuf 讀取時, 它的 readerIndex 將會被遞增已經被讀取的字節數。同樣地,當你寫入 ByteBuf 時,它的 writerIndex 也會被遞增。

ByteBuf 與JDK中的 ByteBuffer 的最大區別之一就是: 
(1)netty的ByteBuf採用了讀/寫索引分離,一個初始化的ByteBuf的readerIndex和writerIndex都處於0位置。 
(2)當讀索引和寫索引處於同一位置時,如果我們繼續讀取,就會拋出異常IndexOutOfBoundsException。 
(3)對於ByteBuf的任何讀寫操作都會分別單獨的維護讀索引和寫索引。maxCapacity最大容量默認的限制就是Integer.MAX_VALUE。 

使用模式

JDK中的Buffer的類型 有heapBuffer和directBuffer兩種類型,但是在netty中除了heap和direct類型外,還有composite Buffer(複合緩衝區類型)。

Heap Buffer 堆緩衝區

這是最常用的類型,ByteBuf將數據存儲在JVM的堆空間,通過將數據存儲在數組中實現的。 
1)堆緩衝的優點是:由於數據存儲在JVM的堆中可以快速創建和快速釋放,並且提供了數組的直接快速訪問的方法。

2)堆緩衝缺點是:每次讀寫數據都要先將數據拷貝到直接緩衝區再進行傳遞。

這種模式被稱爲支撐數組 (backing array),它能在沒有使用池化的情況下提供快速的分配和釋放。

Direct Buffer 直接緩衝區

Direct Buffer在堆之外直接分配內存,直接緩衝區不會佔用堆的容量。事實上,在通過套接字發送它之前,JVM將會在內部把你的緩衝 區複製到一個直接緩衝區中。所以如果使用直接緩衝區可以節約一次拷貝。

1)Direct Buffer的優點是:在使用Socket傳遞數據時性能很好,由於數據直接在內存中,不存在從JVM拷貝數據到直接緩衝區的過程,性能好。

(2)缺點是:相對於基於堆的緩衝區,它們的分配和釋放都較爲昂貴。如果你 正在處理遺留代碼,你也可能會遇到另外一個缺點:因爲數據不是在堆上,所以你不得不進行一 次複製。

Composite Buffer 複合緩衝區

第三種也是最後一種模式使用的是複合緩衝區,它爲多個 ByteBuf 提供一個聚合視圖。在 這裏你可以根據需要添加或者刪除 ByteBuf 實例,這是一個 JDK 的 ByteBuffer 實現完全缺 失的特性。

Netty 通過一個 ByteBuf 子類——CompositeByteBuf——實現了這個模式,它提供了一 個將多個緩衝區表示爲單個合併緩衝區的虛擬表示

Netty提供了Composite ByteBuf來處理複合緩衝區。例如:一條消息由Header和Body組成,將header和body組裝成一條消息發送出去。下圖顯示了Composite ByteBuf組成header和body: 

參考文檔: 

https://www.jianshu.com/p/1ccbc6a348db

http://www.cnblogs.com/duanxz/p/6759689.html

https://www.cnblogs.com/duanxz/p/3698530.html

https://www.jianshu.com/p/66f64b127495

https://www.jianshu.com/p/128ddc36e713

https://www.cnblogs.com/duanxz/p/3724448.html

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