Netty知識筆記

[TOC]算法

1、簡介

Netty是一個異步事件驅動的網絡應用框架,用於快速開發可維護的高性能服務器和客戶端。 編程

kcnmJ1.png

Netty是典型的Reactor模型結構,在實現上,Netty中的Boss類充當mainReactor,NioWorker類充當subReactor(默認NioWorker的個數是當前服務器的可用核數)。後端

在處理新來的請求時,NioWorker讀完已收到的數據到ChannelBuffer中,以後觸發ChannelPipeline中的ChannelHandler流。數組

Netty是事件驅動的,能夠經過ChannelHandler鏈來控制執行流向。由於ChannelHandler鏈的執行過程是在subReactor中同步的,因此若是業務處理handler耗時長,將嚴重影響可支持的併發數。緩存

kcn8dH.png

2、NIO基礎知識點

一、阻塞與非阻塞

阻塞與非阻塞是描述進程在訪問某個資源時,數據是否準備就緒的的一種處理方式。安全

  • 阻塞 :當數據沒有準備就緒時,線程持續等待資源中數據準備完成,直到返回響應結果。
  • 非阻塞: 線程直接返回結果,不會持續等待資源準備數據結束後才響應結果。

二、同步與異步

  • 同步:通常指主動請求並等待IO操做完成的方式
  • 異步:指主動請求數據後即可以繼續處理其它任務,隨後等待IO操做完畢的通知。

3、IO模型

一、傳統BIO模型

傳統BIO是一種同步的阻塞IO,IO在進行讀寫時,該線程將被阻塞,線程沒法進行其它操做。bash

二、僞異步IO模型

以傳統BIO模型爲基礎,經過線程池的方式維護全部的IO線程,實現相對高效的線程開銷及管理。服務器

三、NIO模型

NIO模型是一種同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(緩衝區),Selector網絡

傳統IO基於字節流和字符流進行操做,而NIO基於Channel和Buffer(緩衝區)進行操做,數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(好比:鏈接打開,數據到達)。所以,單個線程能夠監聽多個數據通道。多線程

NIO和傳統IO(一下簡稱IO)之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。

Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據。

IO的各類流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。 NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。

4、NIO模型要點

一、Channel(通道)

傳統IO操做對read()或write()方法的調用,可能會由於沒有數據可讀/可寫而阻塞,直到有數據響應。也就是說讀寫數據的IO調用,可能會無限期的阻塞等待,效率依賴網絡傳輸的速度。最重要的是在調用一個方法前,沒法直到是否會被阻塞。

NIO的Channel抽象了一個重要特徵就是能夠經過配置它的阻塞行爲,來實現非阻塞式的通道。

Channel是一個雙向通道,與傳統IO操做之容許單向的讀寫不一樣的是,NIO的Channel容許在一個通道上進行讀和寫的操做。

二、Buffer(緩衝區)

Bufer顧名思義,它是一個緩衝區,其實是一個容器,一個連續數組。Channel提供從文件、網絡讀取數據的渠道,可是讀寫的數據都必須通過Buffer。

kcuaN9.jpg

Buffer緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該模塊內存。爲了理解Buffer的工做原理,須要熟悉它的三個屬性:capacity、position和limit。

position和limit的含義取決於Buffer處在讀模式仍是寫模式。無論Buffer處在什麼模式,capacity的含義老是同樣的.

kcuB1x.png

  • capacity

做爲一個內存塊,Buffer有固定的大小值,也叫做「capacity」,只能往其中寫入capacity個byte、long、char等類型。一旦Buffer滿了,須要將其清空(經過讀數據或者清楚數據)才能繼續寫數據。

  • position

當你寫數據到Buffer中時,position表示當前的位置。初始的position值爲0,當寫入一個字節數據到Buffer中後,position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity-1。當讀取數據時,也是從某個特定位置讀,講Buffer從寫模式切換到讀模式,position會被重置爲0。當從Buffer的position處讀取一個字節數據後,position向前移動到下一個可讀的位置。

  • limit

在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。 寫模式下,limit等於Buffer的capacity。

當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。所以,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到以前寫入的全部數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)

  • Buffer的分配 對Buffer對象的操做必須首先進行分配,Buffer提供一個allocate(int capacity)方法分配一個指定字節大小的對象。

  • Buffer寫數據

    • 方式一: channel寫到buffer
    • 方式二:經過Buffer的put()方法寫到Buffer

方式1:

int bytes = channel.read(buf); //將channel中的數據讀取到buf中
複製代碼

方式2:

buf.put(byte); //將數據經過put()方法寫入到buf中
複製代碼
  • flip()方法

將Buffer從寫模式切換到讀模式,調用flip()方法會將position設置爲0,並將limit設置爲以前的position的值。

  • Buffer讀數據
    • 從buffer讀取到channel
    • 經過buffer的get獲取數據

方式1:

int bytes = channel.write(buf); //將buf中的數據讀取到channel中
複製代碼

方式2:

byte bt = buf.get(); //從buf中讀取一個byte
複製代碼
  • rewind()方法

Buffer.rewind()方法將position設置爲0,使得能夠重讀Buffer中的全部數據,limit保持不變。

  • clear() 與compact() 方法

一旦讀完Buffer中的數據,須要讓Buffer準備好再次被寫入,能夠經過clear()或compact()方法完成。

若是調用的是clear()方法,position將被設置爲0,limit設置爲capacity的值。可是Buffer並未被清空,只是經過這些標記告訴咱們能夠從哪裏開始往Buffer中寫入多少數據。若是Buffer中還有一些未讀的數據,調用clear()方法將被"遺忘 "。compact()方法將全部未讀的數據拷貝到Buffer起始處,而後將position設置到最後一個未讀元素的後面,limit屬性依然設置爲capacity。可使得Buffer中的未讀數據還能夠在後續中被使用。

  • mark() 與reset()方法

經過調用Buffer.mark()方法能夠標記一個特定的position,以後能夠經過調用Buffer.reset()恢復到這個position上。

4.Selector(多路複用器)

Selector與Channel是相互配合使用的,將Channel註冊在Selector上以後,才能夠正確的使用Selector,但此時Channel必須爲非阻塞模式。Selector能夠監聽Channel的四種狀態(Connect、Accept、Read、Write),當監聽到某一Channel的某個狀態時,才容許對Channel進行相應的操做。

5、NIO開發的問題

一、NIO類庫和API複雜,使用麻煩。
二、須要具有Java多線程編程能力(涉及到Reactor模式)。
三、客戶端斷線重連、網絡不穩定、半包讀寫、失敗緩存、網絡阻塞和異常碼流等問題處理難度很是大
四、存在部分BUG
複製代碼

NIO進行服務器開發的步驟:

一、建立ServerSocketChannel,配置爲非阻塞模式;
	二、綁定監聽,配置TCP參數;
	三、建立一個獨立的IO線程,用於輪詢多路複用器Selector;
	四、建立Selector,將以前建立的ServerSocketChannel註冊到Selector上,監聽Accept事件;
	五、啓動IO線程,在循環中執行Select.select()方法,輪詢就緒的Channel;
	六、當輪詢處處於就緒狀態的Channel時,須要對其進行判斷,若是是OP_ACCEPT狀態,說明有新的客戶端接入,則調用ServerSocketChannel.accept()方法接受新的客戶端;
	七、設置新接入的客戶端鏈路SocketChannel爲非阻塞模式,配置TCP參數;
	八、將SocketChannel註冊到Selector上,監聽READ事件;
	九、若是輪詢的Channel爲OP_READ,則說明SocketChannel中有新的準備就緒的數據包須要讀取,則構造ByteBuffer對象,讀取數據包;
	十、若是輪詢的Channel爲OP_WRITE,則說明還有數據沒有發送完成,須要繼續發送。
複製代碼

6、Netty的有點

一、API使用簡單,開發門檻低;
	二、功能強大,預置了多種編解碼功能,支持多種主流協議;
	三、定製功能強,能夠經過ChannelHandler對通訊框架進行靈活的擴展;
	四、性能高,經過與其餘業界主流的NIO框架對比,Netty綜合性能最優;
	五、成熟、穩定,Netty修復了已經發現的NIO全部BUG;
	六、社區活躍;
	七、經歷了不少商用項目的考驗。
複製代碼

7、粘包/拆包問題

TCP是一個「流」協議,所謂流,就是沒有界限的一串數據。能夠想象爲河流中的水,並無分界線。TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的實際狀況進行包的劃分,因此在業務上認爲,一個完整的包可能會被TCP拆分紅多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。

k7zyaF.png

假設客戶端分別發送了兩個數據包D1和D2給服務端,因爲服務端一次讀取到的字節數是不肯定的,可能存在如下4中狀況。

1.服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包;
 2.服務端一次接收到了兩個數據包,D1和D2粘合在一塊兒,被稱爲TCP粘包;
 3.服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的D1包和D2包的部份內容,第二次讀取到了D2包的剩餘部份內容,這被稱爲TCP拆包;
 4.服務端分兩次讀取到了兩個數據包,第一次讀取到了D1包的部份內容D1_1,第二次讀取到了D1包的剩餘內容D1_1和D2包的完整內容;
複製代碼

若是此時服務器TCP接收滑窗很是小,而數據包D1和D2比較大,頗有可能發生第五種狀況,既服務端分屢次才能將D1和D2包接收徹底,期間發生屢次拆包;

問題的解決策略

因爲底層的TCP沒法理解上層的業務數據,因此在底層是沒法保證數據包不被拆分和重組的,這個問題只能經過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案可概括以下:

1.消息定長,例如每一個報文的大小爲固定長度200字節,若是不夠,空位補空格;
 2.在包尾增長回車換行符進行分割,例如FTP協議;
 3.將消息分爲消息頭和消息體,消息頭中包含消息總長度(或消息體總長度)的字段,一般設計思路爲消息頭的第一個字段使用int32來表示消息的總程度;
 4.更復雜的應用層協議;
複製代碼
  • LineBasedFrameDecoder
爲了解決TCP粘包/拆包致使的半包讀寫問題,Netty默認提供了多種編解碼器用於處理半包。

LinkeBasedFrameDecoder的工做原理是它一次遍歷ByteBuf中的可讀字節,判斷看是否有「\n」、「\r\n」,若是有,就一次位置爲結束位置,從可讀索引到結束位置區間的字節就組成一行。它是以換行符爲結束標誌的編解碼,支持攜帶結束符或者不攜帶結束符兩種解碼方式,同時支持配置單行的最大長度。若是連續讀取到最大長度後任然沒有發現換行符,就會拋出異常,同時忽略掉以前讀到的異常碼流。
複製代碼
  • DelimiterBasedFrameDecoder
實現自定義分隔符做爲消息的結束標誌,完成解碼。
複製代碼
  • FixedLengthFrameDecoder
是固定長度解碼器,可以按照指定的長度對消息進行自動解碼,開發者不須要考慮TCP的粘包/拆包問題。
複製代碼

8、Netty的高性能之道

1.異步非阻塞通訊

在IO編程過程當中,當須要同時處理多個客戶端接入請求時,能夠利用多線程或者IO多路複用技術進行處理。IO多路複用技術經過把多個IO的阻塞複用到同一個Selector的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求。與傳統的多線程/多進程模型相比,IO多路複用的最大優點是系統開銷小,系統不須要建立新的額外進程或者線程,也不須要維護這些進程和線程的運行,下降了系統的維護工做量,節省了系統資源。

Netty的IO線程NioEventLoop因爲聚合了多路複用器Selector,能夠同時併發處理成百上千個客戶端SocketChannel。因爲讀寫操做都是非阻塞的,這就能夠充分提高IO線程的運行效率,避免由頻繁的IO阻塞致使的線程掛起。另外,因爲Netty採用了異步通訊模式,一個IO線程能夠併發處理N個客戶端鏈接和讀寫操做,這從根本上解決了傳統同步阻塞IO中 一鏈接一線程模型,架構的性能、彈性伸縮能力和可靠性都獲得了極大的提高。

二、高效的Reactor線程模型

經常使用的Reactor線程模型有三種,分別以下:

  • C一、Reactor單線程模型

kOzIhT.png

Reactor單線程模型,指的是全部的IO操做都在同一個NIO線程上面完成,NIO線程職責以下:

一、做爲NIO服務端,接收客戶端的TCP鏈接;

二、做爲NIO客戶端,向服務端發起TCP鏈接;

三、讀取通訊對端的請求或者應答消息;

四、向通訊對端發送請求消息或者應答消息;

因爲Reactor模式使用的是異步非阻塞IO,全部的IO操做都不會致使阻塞,理論上一個線程能夠獨立處理全部IO相關操做。從架構層面看,一個NIO線程確實能夠完成其承擔的職責。例如,經過Acceptor接收客戶端的TCP鏈接請求消息,鏈路創建成功以後,經過Dispatch將對應的ByteBuffer派發到指定的Handler上進行消息編碼。用戶Handler能夠經過NIO線程將消息發送給客戶端。

對於一些小容量應用場景,可使用單線程模型,可是對於高負載、大併發的應用卻不合適,主要緣由以下:

①、一個NIO線程同時處理成百上千的鏈路,性能上沒法支撐。即使NIO線程的CPU負荷達到100%,也沒法知足海量消息的編碼、解碼、讀取和發送;

②、當NIO線程負載太重後,處理速度將變慢,這會致使大量客戶端鏈接超時,超時以後每每會進行重發,這更加劇了NIO線程的負載,最終會致使大量消息積壓和處理超時,NIO線程會成爲系統的性能瓶頸;

③、可靠性問題。一旦NIO線程意外進入死循環,會致使整個系統通訊模塊不可用,不能接收和處理外部消息,形成節點故障。

爲了解決這些問題,從而演進出了Reactor多線程模型。

  • C二、Reactor多線程模型
    kXpteA.png

Reactor多線程模型與單線程模型最大的區別就是有一組NIO線程處理IO操做,特色以下:

①有一個專門的NIO線程——Acceptor線程用於監聽服務端,接收客戶端TCP鏈接請求;

②網絡IO操做——讀、寫等由一個NIO線程池負責,線程池能夠採用標準的JDK線程池實現,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、編碼、解碼和發送;

③1個NIO線程能夠同時處理N條鏈路,可是1個鏈路只對應1個NIO線程,防止發生併發操做問題。

在絕大多數場景下,Reactor多線程模型均可以知足性能需求;可是,在極特殊應用場景中,一個NIO線程負責監聽和處理全部的客戶端鏈接可能會存在性能問題。例如百萬客戶端併發鏈接,或者服務端須要對客戶端的握手消息進行安全認證,認證自己很是損耗性能。在這類場景下,單獨一個Acceptor線程可能會存在性能不足問題,爲了解決性能問題,產生了第三種Reactor線程模型——主從Reactor多線程模型。

  • C三、主從Reactor多線程模型
    kXpyLj.png

主從Reactor線程模型的特色是:

服務端用於接收客戶端鏈接的再也不是一個單獨的NIO線程,而是一個獨立的NIO線程池。Acceptor接收到客戶端TCP鏈接請求處理完成後(可能包含接入認證等),將新建立的SocketChannel註冊到IO線程池(subReactor線程池)的某個IO線程上,由它負責SocketChannel的讀寫和編解碼工做。Acceptor線程池只用於客戶端的登陸、握手和安全認證,一旦鏈路創建成功,就將鏈路註冊到後端subReactor線程池的IO線程上,由IO線程負責後續的IO操做。
複製代碼

利用主從NIO線程模型,能夠解決1個服務端監聽線程沒法有效處理全部客戶端鏈接的性能不足問題。Netty官方推薦使用該線程模型。它的工做流程總結以下:

①從主線程池中隨機選擇一個Reactor線程做爲Acceptor線程,用於綁定監聽端口,接收客戶端鏈接;

②Acceptor線程接收客戶端鏈接請求以後,建立新的SocketChannel,將其註冊到主線程池的其餘Reactor線程上,由其負責接入認證、IP黑白名單過濾、握手等操做;

③而後也業務層的鏈路正式創建成功,將SocketChannel從主線程池的Reactor線程的多路複用器上摘除,從新註冊到Sub線程池的線程上,用於處理IO的讀寫操做。

三、無鎖化的串行設計

在大多數場景下,並行多線程處理能夠提高系統的併發性能。可是,若是對於共享資源的併發訪問處理不當,會帶來嚴重的鎖競爭,這最終會致使性能的降低。爲了儘量地避免鎖競爭帶來的性能損耗,能夠經過串行化設計,既消息的處理儘量在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。

爲了儘量提高性能,Netty採用了串行無鎖化設計,在IO線程內部進行串行操做,避免多線程競爭致使的性能降低。表面上看,串行化設計彷佛CPU利用率不高,併發程度不夠。可是,經過調整NIO線程池的線程參數,能夠同時啓動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列——多個工做線程模型性能更優。

kjGsf0.png

Netty的NioEventLoop讀取到消息後,直接調用ChannelPipeline的fireChannelRead(Object msg),只要用戶不主動切換線程,一直會由NioEventLoop調用到用戶的Handler,期間不進行線程切換。這種串行化處理方式避免了多線程致使的鎖競爭,從性能角度看是最優的

四、高效的併發編程

Netty中高效併發編程主要體現:

  • volatile的大量、正確使用
  • CAS和原子類的普遍使用
  • 線程安全容器的使用
  • 經過讀寫鎖提高併發性能

五、高效的序列化框架

影響序列化性能的關鍵因素總結以下:

  • 序列化後的碼流大小(網絡寬帶的佔用)
  • 序列化與反序列化的性能(CPU資源佔用)
  • 是否支持跨語言(異構系統的對接和開發語言切換) Netty默認提供了對GoogleProtobuf的支持,經過擴展Netty的編解碼接口,用戶能夠實現其餘的高性能序列化框架

六、零拷貝

Netty的「零拷貝」主要體如今三個方面:

  • Direct buffers

Netty的接收和發送ByteBuffer採用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不須要進行字節緩衝區的二次拷貝。若是使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,而後才寫入Socket中。相比於堆外直接內存,消息在發送過程當中多了一次緩衝區的內存拷貝

  • CompositeByteBuf

第二種「零拷貝 」的實現CompositeByteBuf,它對外將多個ByteBuf封裝成一個ByteBuf,對外提供統一封裝後的ByteBuf接口

  • 文件傳輸

第三種「零拷貝」就是文件傳輸,Netty文件傳輸類DefaultFileRegion經過transferTo方法將文件發送到目標Channel中。不少操做系統直接將文件緩衝區的內容發送到目標Channel中,而不須要經過循環拷貝的方式,這是一種更加高效的傳輸方式,提高了傳輸性能,下降了CPU和內存佔用,實現了文件傳輸的「零拷貝」。

七、內存池

隨着JVM虛擬機和JIT即時編譯技術的發展,對象的分配和回收是個很是輕量級的工做。可是對於緩衝區Buffer,狀況卻稍有不一樣,特別是對於堆外直接內存的分配和回收,是一件耗時的操做。爲了儘可能重用緩衝區,Netty提供了基於內存池的緩衝區重用機制。

八、靈活的TCP參數配置能力

Netty在啓動輔助類中能夠靈活的配置TCP參數,知足不一樣的用戶場景。合理設置TCP參數在某些場景下對於性能的提高能夠起到的顯著的效果,總結一下對性能影響比較大的幾個配置項:

1)、SO_RCVBUF和SO_SNDBUF:一般建議值爲128KB或者256KB;
    2)、TCP_NODELAY:NAGLE算法經過將緩衝區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,從而提升網絡應用效率。可是對於時延敏感的應用場景須要關閉該優化算法;
    3)、軟中斷:若是Linux內核版本支持RPS(2.6.35以上版本),開啓RPS後能夠實現軟中斷,提高網絡吞吐量。RPS根據數據包的源地址,目的地址以及目的和源端口,計算出一個hash值,而後根據這個hash值來選擇軟中斷運行的CPU。從上層來看,也就是說將每一個鏈接和CPU綁定,並經過這個hash值,來均衡軟中斷在多個CPU上,提高網絡並行處理性能。

複製代碼

9、項目實踐

相關文章
相關標籤/搜索