Netty快速上手:Netty沒有你想象的那麼難

該文章是Netty相關文章。目的是讓讀者可以快速的瞭解netty的相關知識以及開發方法。所以本文章在正式介紹Netty開發前先介紹了Netty的前置相關內容:線程模型,JavaNIO,零拷貝等。本文章以大綱框架的形式總體介紹了Netty,但願對讀者有些幫助。文中圖片多來自於百度網絡,若是有侵權,能夠聯繫我進行刪除。內容如有不當歡迎在評論區指出。

Netty

netty是由JBOSS提供的一個Java開源框架,是一個異步的,基於事件驅動的網絡應用框架,用以快速開發高性能,高可靠性的網絡IO程序.java

NIO模型

  1. 阻塞IO:發起請求就一直等待,直到數據返回。在IO執行的兩個階段都被block了

  1. 非阻塞IO:應用程序不斷在一個循環裏調用recvfrom,輪詢內核,看是否準備好了數據,比較浪費CPU

  1. io複用:一個或一組線程處理多個鏈接能夠同時對多個讀/寫操做的IO函數進行輪詢檢測,直到有數據可讀或可寫時,才真正調用IO操做函數

  1. 信號驅動IO:事先發出一個請求,當有數據後會返回一個標識回調,而後經過recvfrmo去請求數據

  1. 異步io:發出請求就返回,剩下的事情會異步自動完成,不須要作任何處理

異步 I/O 與信號驅動 I/O 的區別在於,異步 I/O 的信號是通知應用進程 I/O 完成,而信號驅動 I/O 的信號是通知應用進程能夠開始 I/O。

Java NIO

  1. 三大核心Channel(通道),Buffer(緩衝區),Selector(選擇器)。數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中,Selector用於監聽多個通道的事件。
  2. Channel:是雙向的,既能夠用來進行讀操做,又能夠用來進行寫操做bootstrap

    • FileChannel 文件IO,不支持非阻塞模式,沒法同Selector一同使用。
    • DatagramChannel 用於處理UDP的鏈接。
    • SocketChannel 用於處理TCP客戶端的鏈接。
    • ServerSocketChannel 用於處理TCP服務端的鏈接。
  3. Buffer:它經過幾個變量來保存這個數據的當前位置狀態:設計模式

    • capacity:緩衝區數組的總長度
    • position:下一個要操做的數據元素的位置
    • limit:緩衝區數組中不可操做的下一個元素的位置
  4. 向Buffer中寫數據:api

    • 從Channel寫到Buffer (fileChannel.read(buf))
    • 經過Buffer的put()方法 (buf.put(…))
  5. 從Buffer中讀取數據:數組

    • 從Buffer讀取到Channel (channel.write(buf))
    • 使用get()方法從Buffer中讀取數據 (buf.get())
  6. Buffer經常使用方法緩存

    1. flip():寫模式下調用flip()以後,Buffer從寫模式變成讀模式。limit設置爲position,position將被設回0
    2. clear()方法:position將被設回0,limit設置成capacity,Buffer被清空了,但Buffer中的數據並未被清除。
    3. compact():將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素正後面,limit設置成capacity,準備繼續寫入。讀模式變成寫模式
    4. Buffer.rewind()方法將position設回0,因此你能夠重讀Buffer中的全部數據
  7. Selector:Selector一塊兒使用時,Channel必須處於非阻塞模式下。經過channel.register,將channel登記到Selector上,同時添加關注的事件(SelectionKey),經常使用方法以下:安全

    • select()阻塞到至少有一個通道在你註冊的事件上就緒了。
    • select(long timeout)和select()同樣,除了最長會阻塞timeout毫秒(參數)。
    • selectNow()不會阻塞,無論什麼通道就緒都馬上返回
    • selectedKeys()方法訪問就緒的通道。Selector不會本身從已選擇鍵集中移除SelectionKey實例。

NIO其餘功能:

  1. MappedByteBuffer是NIO引入的文件內存映射方案,讀寫性能極高。
  2. transferFrom & transferTo:FileChannel的transferFrom()方法能夠將數據從源通道傳輸到FileChannel中.
  3. 分散(scatter)從Channel中讀取是指在讀操做時將讀取的數據寫入多個buffer中。所以,Channel將從Channel中讀取的數據「分散(scatter)」到多個Buffer中。
  4. 彙集(gather)寫入Channel是指在寫操做時將多個buffer的數據寫入同一個Channel,所以,Channel 將多個Buffer中的數據「彙集(gather)」後發送到Channel。

Linux的NIO:

  1. select:阻塞地同時探測一組支持非阻塞的IO設備,直至某一個設備觸發了事件或者超過了指定的等待時間。當select函數返回後能夠遍歷文件描述符,找到就緒的描述符

缺點:服務器

1. 單進程所打開的FD是具備必定限制的,
2. 套接字比較多的時候,每次select()都要經過遍歷Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間
3. 每次都須要把fd集合從⽤用戶態拷貝到內核態,這個開銷在fd不少時會很⼤大
  1. poll:本質上和select沒有區別,fd使用鏈表實現,沒有最大鏈接數的限制。網絡

    • 缺點:多線程

      1. 大量的fd數組都須要從用戶態拷貝到內核態。
      2. poll的「水平觸發」:若是報告了fd後,沒有被處理,則下次poll還會再次報告該fd。
  2. epoll:
    epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是「高速」模式。

    • LT(水平觸發)模式下,只要這個文件描述符還有數據可讀,每次 epoll都會返回它的事件,提醒用戶程序去操做;
    • ET(邊緣觸發)模式下,對於每個被通知的文件描述符,如可讀,則必須將該文件描述符一直讀到空,不然下次的 epoll不會返回餘下的數據,會丟掉事件(只通知一次)。
**epoll底層原理**:調用epoll_create後,內核cache裏建了個紅黑樹用於存儲之後epoll_ctl傳來的socket,創建一個rdllist雙向鏈表,用於存儲準備就緒的事件。在epoll_wait調用時,僅僅觀察這個rdllist雙向鏈表裏有沒有數據便可。有數據就返回,沒有數據就阻塞。

零拷貝:

對一個操做系統進程來講,它既有內核空間(與其餘進程共享),也有用戶空間(進程私有),它們都是處於虛擬地址空間中。進程沒法直接操做I/O設備,必須經過操做系統調用請求內核來協助完成I/O動做。將靜態文件展現給用戶須要先將靜態內容從磁盤中拷貝出來放到內存buf中,而後再將這個buf經過socket發給用戶
問題:經歷了4次copy過程,4次內核切換

1. 用戶態到內核態:調用read,文件copy到內核態內存
2. 內核態到用戶態:內核態內存數據copy到用戶態內存
3. 用戶態到內核態:調用writer:用戶態內存數據到內核態socket的buffer內存中
4. 最後內核模式下的socket模式下的buffer數據copy到網卡設備中傳送
5. 從內核態回到用戶態執行下一個循環

Linux:零拷貝技術消除傳輸數據在存儲器之間沒必要要的中間拷貝次數,減小了用戶進程地址空間和內核地址空間之間由於上下文切換而帶來的開銷。

常見零拷貝技術

  • mmap():應用程序調用mmap(),磁盤上的數據會經過DMA被拷貝到內核緩衝區,而後操做系統會把這段內核緩衝區與應用程序共享,這樣就不須要把內核緩衝區的內容往用戶空間拷貝。數據向網絡中寫時,只須要把數據從這塊共享的內核緩衝區中拷貝到socket緩衝區中去就好了,這些操做都發生在內核態.
  • sendfile():DMA將磁盤數據複製到kernel buffer,而後將內核中的kernel buffer直接拷貝到socket buffer;一旦數據全都拷貝到socket buffer,sendfile()系統調用將會return、表明數據轉化的完成。
  • splice():從磁盤讀取到內核buffer後,在內核空間直接與socket buffer創建pipe管道,不須要內核支持。
  • DMA scatter/gather:批量copy

零拷貝不只僅帶來更少的數據複製,還能帶來其餘的性能優點,例如更少的上下文切換,更少的 CPU 緩存僞共享以及無 CPU 校驗和計算。

netty 介紹

Netty 對 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題。

  1. 設計優雅:適用於各類傳輸類型的統一 API 阻塞和非阻塞 Socket;基於靈活且可擴展的事件模型,能夠清晰地分離關注點;
  2. 高度可定製的線程模型 - 單線程,一個或多個線程池.
  3. 使用方便:詳細記錄的 Javadoc,用戶指南和示例;沒有其餘依賴項,JDK 5(Netty 3.x)或 6(Netty 4.x)就 足夠了。
  4. 高性能、吞吐量更高:延遲更低;減小資源消耗;最小化沒必要要的內存複製。
  5. 安全:完整的 SSL/TLS 和 StartTLS 支持。
  6. 社區活躍、不斷更新:社區活躍,版本迭代週期短,發現的 Bug 能夠被及時修復,同時,更多的新功能會被加入
  7. Java原生NIO使用起碼麻煩須要本身管理線程,Netty對JDK自帶的NIO的api進行了封裝,提供了更簡單優雅的實現方式。因爲netty5使用ForkJoinPool增長了複雜性,而且沒有顯示出明顯的性能優點,因此netty5如今被廢棄掉了。

netty線程模型

Reactor模式:是事件驅動的,多個併發輸入源。它有一個服務處理器,有多個請求處理器;這個服務處理器會同步的將輸入的客戶端請求事件多路複用的分發給相應的請求處理器。

單Reactor單線程:多路複用、事件分發和消息的處理都是在一個Reactor線程上完成。

* 優勢:
    * 模型簡單,實現方便
* 缺點:
    
    * 性能差:單線程沒法發揮多核性能,
    * 可靠性差:線程意外終止或死循環,則整個模塊不可用

單Reactor多線程
一個Reactor線程負責監聽服務端的鏈接請求和接收客戶端的TCP讀寫請求;NIO線程池負責消息的讀取、解碼、編碼和發送

優勢:能夠充分的利用多核cpu的處理能

缺點:Reactor處理全部事件的監聽和響應,在單線程運行,在高併發場景容易出現性能瓶頸.

主從 Reactor 多線程
MainReactor負責監聽服務端的鏈接請求,接收到客戶端的鏈接後,將SocketChannel從MainReactor上移除,從新註冊到SubReactor線程池的線程上。SubReactor處理I/O的讀寫操做,NIO線程池負責消息的讀取、解碼、編碼和發送。

netty工做原理圖

NioEventLoopGroup:主要管理 eventLoop 的生命週期,能夠理解爲一個線程池,內部維護了一組線程,每一個線程(NioEventLoop)負責處理多個 Channel 上的事件,而一個 Channel 只對應於一個線程
ChannelHandler用於處理Channel對應的事件
示例代碼

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

        //bossGroup和workerGroup分別對應mainReactor和subReactor
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workGroup)
                //用來指定一個Channel工廠,mainReactor用來包裝SocketChannel.
                .channel(NioServerSocketChannel.class)
                //用於指定TCP相關的參數以及一些Netty自定義的參數
                .option(ChannelOption.SO_BACKLOG, 100)
                //childHandler()用於指定subReactor中的處理器,相似的,handler()用於指定mainReactor的處理器
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    //ChannelInitializer,它是一個特殊的Handler,功能是初始化多個Handler。完成初始化工做後,netty會將ChannelInitializer從Handler鏈上刪除。
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //addLast(Handler)方法中不指定線程池那麼將使用默認的subReacor即woker線程池執行處理器中的業務邏輯代碼。
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new MyServerHandler());
                    }
                });
        //sync() 同步阻塞直到bind成功
        ChannelFuture f = bootstrap.bind(8888).sync();
        //sync()同步阻塞直到netty工做結束
        f.channel().closeFuture().sync();

    }
}

線程組

  • NioEventLoopGroup:

    1. NioEventLoopGroup初始化時未指定線程數,那麼會使用默認線程數。
    2. 每一個NioEventLoopGroup對象內部都有一組可執行的NioEventLoop數組。
    3. 當有IO事件來時,須要從線程池中選擇一個線程出來執行,這時候的NioEventLoop選擇策略是由EventExecutorChooser實現的,並調用該類的next()方法。
    4. 每一個NioEventLoopGroup對象都有一個NioEventLoop選擇器與之對應,其會根據NioEventLoop的個數,EventExecutorChooser(若是是2的冪次方,則按位運算,不然使用普通的輪詢)
  • NioEventLoop
    NioEventLoop 肩負着兩種任務:

    1. 做爲 IO 線程, 執行與 Channel 相關的 IO 操做, 包括 調用 select 等待就緒的 IO 事件、讀寫數據與數據的處理等;
    2. 做爲任務隊列, 執行 taskQueue 中的任務, 例如用戶調用 eventLoop.schedule 提交的定時任務也是這個線程執行的

BootStrap和ServerBootstrap

ServerBootstrap是一個工具類,用來配置netty

  1. channel():提供一個ChannelFactory來建立channel,不一樣協議的鏈接有不一樣的 Channel 類型與之對應,常見的Channel類型:

    • NioSocketChannel, 表明異步的客戶端 TCP Socket 鏈接.
    • NioServerSocketChannel, 異步的服務器端 TCP Socket 鏈接.
    • NioDatagramChannel, 異步的 UDP 鏈接
  2. group():配置工做線程組,用於處理channel的事件
  3. ChannelHandler():用戶自定義的事件處理器

出站和入站:

ChannelHandler下主要是兩個子接口

  1. ChannelInboundHandler(入站): 處理輸入數據和Channel狀態類型改變。

    • 適配器: ChannelInboundHandlerAdapter(適配器設計模式)
    • 經常使用的: SimpleChannelInboundHandler
  2. ChannelOutboundHandler(出站): 處理輸出數據

    • 適配器: ChannelOutboundHandlerAdapter

ChannelPipeline 是一個 Handler 的集合,它負責處理和攔截 inbound 或者 outbound 的事件和操做,一個貫穿 Netty 的鏈。每一個新的通道Channel,Netty都會建立一個新的ChannelPipeline,並將器pipeline附加到channel中。DefaultChinnelPipeline它的Handel頭部和尾部的Handel是固定的,咱們所添加的Handel是添加在這個頭和尾以前的Handel。

ChannelHandlerContext:ChannelPipeline並非直接管理ChannelHandler,而是經過ChannelHandlerContext來間接管理。

image

Netty編碼器

網絡中都是以字節碼的數據形式來傳輸數據的,服務器編碼數據後發送到客戶端,客戶端須要對數據進行解碼

  • encoder 負責把業務數據轉換成字節碼數據
  • decoder 負責把字節碼數據轉換成業務數據

Netty提供了一些默認的編碼器:
StringEncoder:對字符串數據進行編碼
ObjectEncoder:對 Java 對象進行編碼
StringDecoder:對字符串數據進行解碼
ObjectDecoder:對 Java 對象進行解碼

抽象解碼器

  1. ByteToMessageDecoder: 用於將字節轉爲消息,須要檢查緩衝區是否有足夠的字節
  2. ReplayingDecoder: 繼承ByteToMessageDecoder,不須要檢查緩衝區是否有足夠的字節,可是ReplayingDecoder速度略慢於ByteToMessageDecoder,同時不是全部的ByteBuf都支持。

    • 選擇:項目複雜性高則使用ReplayingDecoder,不然使用 ByteToMessageDecoder
  3. MessageToMessageDecoder: 用於從一種消息解碼爲另一種消息

TCP粘包:

UDP是基於幀的,包的首部有數據報文的長度.TCP是基於字節流,沒有邊界的。TCP的首部沒有表示數據長度的字段。

  • 發生TCP粘包或拆包的緣由:

    1. 要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。
    2. 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
    3. 要發送的數據小於TCP發送緩衝區的大小,TCP將屢次寫入緩衝區的數據一次發送出去,將會發生粘包。
    4. 接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。
  • 解決方式:
  1. 發送定長消息,若是位置不夠,填充特殊字符
  2. 在每個包的尾部加一個特殊分割符
  3. 發送端給每一個數據包添加包首部,首部中應該至少包含數據包的長度。
  • Netty 已經提供了編碼器用於解決粘包。

    1. LineBasedFrameDecoder 能夠基於換行符解決。
    2. DelimiterBasedFrameDecoder可基於分隔符解決。
    3. FixedLengthFrameDecoder可指定長度解決。

netty的零拷貝

Netty徹底工做在用戶態的,Netty的零拷貝更多的對數據操做的優化。

Netty的零拷貝(或者說ByteBuf的複用)主要體如今如下幾個方面:

  1. DirectByteBuf經過直接在堆外分配內存的方式,避免了數據從堆內拷貝到堆外的過程
  2. 經過組合ByteBuf類:即CompositeByteBuf,將多個ByteBuf合併爲一個邏輯上的ByteBuf, 而不須要進行數據拷貝
  3. 經過各類包裝方法, 將 byte[]、ByteBuffer等包裝成一個ByteBuf對象,而不須要進行數據的拷貝
  4. 經過slice方法, 將一個ByteBuf分解爲多個共享同一個存儲區域的ByteBuf, 避免了內存的拷貝,這在須要進行拆包操做時很是管用
  5. 經過FileRegion包裝的FileChannel.tranferTo方法進行文件傳輸時, 能夠直接將文件緩衝區的數據發送到目標Channel, 減小了經過循環write方式致使的內存拷貝。可是這種方式是須要獲得操做系統的零拷貝的支持的,若是netty所運行的操做系統不支持零拷貝的特性,則netty仍然沒法作到零拷貝。
相關文章
相關標籤/搜索