
Java 網路IO模型(BIO NIO AIO)
BIO同步并阻塞(傳統阻塞型):一個連接一個執行緒,客戶端有連接請求時服務器端就需要啟動一個執行緒進行處理,執行緒開銷大,

NIO同步非阻塞:一個請求一個執行緒,但客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求時才啟動一個執行緒進行處理,

AIO異步非阻塞:一個有效請求一個執行緒,客戶端的 I/O 請求都是由 OS 先完成了再通知服務器應用去
啟動執行緒進行處理,
BIO與NIO的區別
BIO 是面向流的,NIO 是面向緩沖區的或者面向塊的;
BIO 的各種流是阻塞的,而 NIO 是非阻塞的;
BIO的 Stream 是單向的,而 NIO 的 channel 是雙向的,
阻塞和非阻塞
阻塞和非阻塞指的是執行一個操作是等操作結束再回傳,還是馬上回傳,
比如餐館的服務員為用戶點菜,當有用戶點完菜后,服務員將選單給后臺廚師,此時有兩種方式:
第一種:就在出菜視窗等待,直到廚師炒完菜后將菜送到視窗,然后服務員再將菜送到用戶手中;
第二種:等一會再到視窗來問廚師,某個菜好了沒?如果沒有先處理其他事情,等會再去問一次;
第一種就是阻塞方式,第二種則是非阻塞的,
同步和異步
同步和異步又是另外一個概念,它是事件本身的一個屬性,還拿前面點菜為例,服務員直接跟廚師打交道,菜出來沒出來,服務員直接指導,但只有當廚師將菜送到服務員手上,這個程序才算正常完成,這就是同步的事件,同樣是點菜,有些餐館有專門的傳菜人員,當廚師炒好菜后,傳菜員將菜送到傳菜視窗,并通知服務員,這就變成異步的了,其實異步還可以分為兩種:帶通知的和不帶通知的,前面說的那種屬于帶通知的,有些傳菜員干活可能主動性不是很夠,不會主動通知你,你就需要時不時的去關注一下狀態,這種就是不帶通知的異步,
對于同步的事件,你只能以阻塞的方式去做,而對于異步的事件,阻塞和非阻塞都是可以的,非阻塞又有兩種方式:主動查詢和被動接收訊息,被動不意味著一定不好,在這里它恰恰是效率更高的,因為在主動查詢里絕大部分的查詢是在做無用功,對于帶通知的異步事件,兩者皆可,而對于不帶通知的,則只能用主動查詢,
但是對于非阻塞和異步的概念有點混淆,非阻塞只是意味著方法呼叫不阻塞,就是說作為服務員的你不用一直在視窗等,非阻塞的邏輯是"等可以讀(寫)了告訴你",但是完成讀(寫)作業的還是呼叫者(執行緒)服務員的你等菜到視窗了還是要你親自去拿,而異步意味這你可以不用親自去做讀(寫)這件事,你的作業讓別人(別的執行緒)來做,你只需要發起呼叫,別人把作業做完以后,或許再通知你,它的邏輯是“我做完了 告訴/不告訴 你”,他和非阻塞的區別在于一個是"已經做完"另一個是"可以去做",
阻塞IO/非阻塞IO
阻塞IO是:拷貝-知道所有資料拷貝到發送緩沖區,
非阻塞IO是拷貝-回傳-再拷貝-再回傳
read總是在接受快取區有資料的時候直接回傳,而不是等到應用程式哥頂的資料充滿才回傳,如果此時緩沖區是空的,那么阻塞模式會等待,非阻塞則會回傳-1并有EWOULDBLOCK或EAGAIN錯誤
和read不太一樣的是,在阻塞模式下,write只有在發送緩沖區足矣容納應用程式的輸出位元組時才會回傳,在非阻塞的模式下,能寫入多少則寫入多少,并回傳實際寫入的位元組數

I/O多路復用機制
I/O多路復用就是通過一種機制,一個行程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作,但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫程序是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實作會負責把資料從內核拷貝到用戶空間,
select運行機制
當使用select函式的時候,先通知內核掛起行程,一旦一個或者多個IO事情發生,控制權將回傳給應用程式,然后由應用程式進行IO處理,
select()的機制中提供一種fd_set的資料結構,實際上是一個long型別的陣列,每一個陣列元素都能與一打開的檔案句柄(不管是Socket句柄,還是其他檔案或命名管道或設備句柄)建立聯系,建立聯系的作業由程式員完成,當呼叫select()時,由內核根據IO狀態修改fd_set的內容,由此來通知執行了select()的行程哪一Socket或檔案可讀,
從流程上來看,使用select函式進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及呼叫select函式的額外操作,效率更差,但是,使用select以后最大的優勢是用戶可以在一個執行緒內同時處理多個socket的IO請求,用戶可以注冊多個socket,然后不斷地呼叫select讀取被激活的socket,即可達到在同一個執行緒內同時處理多個IO請求的目的,而在同步阻塞模型中,必須通過多執行緒的方式才能達到這個目的,
select機制的問題
- 每次呼叫select,都需要把fd_set集合從用戶態拷貝到內核態,如果fd_set集合很大時,那這個開銷也很大
- 同時每次呼叫select都需要在內核遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大
- 為了減少資料拷貝帶來的性能損壞,內核對被監控的fd_set集合大小做了限制,并且這個是通過宏控制的,大小不可改變(限制為1024)
Poll運行機制
鑒于select所支持的描述符有限,隨后提出poll解決這個問題
poll和select不同之處在于,在select中,檔案描述符個數隨著fd_set的實作而固定,而在poll函式中,我們可以通過控制pollfd陣列的大小來改變描述符的個數
poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大檔案描述符數量的限制,也就是說,poll只解決了上面的問題3,并沒有解決問題1,2的性能開銷問題,
poll改變了檔案描述符集合的描述方式,使用了鏈表結構而不是select的陣列結構,使得poll支持的檔案描述符集合限制遠大于select的1024,
Epoll運行機制
epoll 通過監控注冊的多個描述字,來進行 I/O 事件的分發處理,不同于 poll 的是,epoll 不僅提供了默認的 level-triggered(條件觸發)機制,還提供了性能更為強勁的edge triggered(邊緣觸發)機制
epoll在Linux2.6內核正式提出,是基于事件驅動的I/O方式,相對于select來說,epoll沒有描述符個數限制,使用一個檔案描述符管理多個描述符,將用戶關心的檔案描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次,
當我們使用epoll_fd增加一個fd的時候,內核會為我們創建一個epitem實體,講這個實體作為紅黑樹的節點,隨后查找的每一個fd是否有事件發生就是通過紅黑樹的epitem來操作
epoll維護一個鏈表來記錄就緒事件,內核會當每個檔案有事件發生的時候將自己登記到這個就緒串列,然后通過內核自身的檔案file-eventpoll之間的回呼和喚醒機制,減少對內核描述字的遍歷,大俗事件通知和檢測的效率
elect、poll、epoll 區別總結:

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點,
1、表面上看epoll的性能最好,但是在連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函式回呼,
2、select低效是因為每次它都需要輪詢,但低效也是相對的,視情況而定,也可通過良好的設計改善,
NIO模型
BIO是基于位元組流或者字符流進行操作,而NIO基于channel通道和buffer緩沖區進行操作,資料總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中,Selector選擇器用于監聽多個通道的事件(比如:連接請求,資料到達等),因此就可以使用單個執行緒監聽多個客戶端通道,

NIO(JDK1.4提供的新API)
NIO三大核心部分:Channel(通道)、Buffer(緩沖區)、Selector(選擇器)
NIO面向緩沖區,或者面向 塊 編程的,資料讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理程序中的靈活性,使用它可以提供非阻塞式的高伸縮性網路,
Java NIO的非阻塞模式(單執行緒處理多任務),使一個執行緒從某通道發送請求或者讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什么都不會獲取,而不是保持執行緒阻塞,所以直至資料變的可以讀取之前,該執行緒可以繼續做其他事情,非阻塞寫也是如此,一個執行緒請求寫入一些資料到某通道,但不需要等待它完全寫入,這個執行緒可以去做其他事情,
NIO 的特點:事件驅動模型、單執行緒處理多任務、非阻塞 I/O,I/O 讀寫不再阻塞,而是返
回 0、基于 block 的傳輸比基于流的傳輸更高效、更高級的 IO 函式 zero-copy、IO 多路復用大大提高了 Java 網路應用的可伸縮性和實用性,基于 Reactor 執行緒模型,
在 Reactor 模式中,事件分發器等待某個事件或者可應用或個操作的狀態發生,事件分發
器就把這個事件傳給事先注冊的事件處理函式或者回呼函式,由后者來做實際的讀寫操
作,如在 Reactor 中實作讀:注冊讀就緒事件和相應的事件處理器、事件分發器等待事
件、事件到來,激活分發器,分發器呼叫事件對應的處理器、事件處理器完成實際的讀操
作,處理讀到的資料,注冊新的事件,然后返還控制權,
NIO的組成

Buffer:與 Channel 進行互動,資料是從 Channel 讀入緩沖區,從緩沖區寫入 Channel 中的DirectByteBuffer 可減少一次系統空間到用戶空間的拷貝,但 Buffer 創建和銷毀的成本更
高,不可控,通常會用記憶體池來提高性能,直接緩沖區主要分配給那些易受基礎系統的本
機 I/O 操作影響的大型、持久的緩沖區,如果資料量比較小的中小應用情況下,可以考慮
使用 heapBuffer,由 JVM 進行管理,
Channel:表示 IO 源與目標打開的連接,是雙向的,但不能直接訪問資料,只能與 Buffer
進行互動,通過原始碼可知,FileChannel 的 read 方法和 write 方法都導致資料復制了兩次!
Selector 可使一個單獨的執行緒管理多個 Channel,open 方法可創建 Selector,register 方法向多路復用器器注冊通道,可以監聽的事件型別:讀、寫、連接、accept,注冊事件后會產
生一個 SelectionKey:它表示 SelectableChannel 和 Selector 之間的注冊關系,wakeup 方
法:使尚未回傳的第一個選擇操作立即回傳,喚醒的原因是:注冊了新的 channel 或者事
件;channel 關閉,取消注冊;優先級更高的事件觸發(如定時器事件),希望及時處理,
Selector 在 Linux 的實作類是 EPollSelectorImpl,委托給 EPollArrayWrapper 實作,其中三個native 方法是對 epoll 的封裝,而 EPollSelectorImpl. implRegister 方法,通過呼叫 epoll_ctl向 epoll 實體中注冊事件,還將注冊的檔案描述符(fd)與 SelectionKey 的對應關系添加到fdToKey 中,這個 map 維護了檔案描述符與 SelectionKey 的映射,
fdToKey 有時會變得非常大,因為注冊到 Selector 上的 Channel 非常多(百萬連接);過期或失效的 Channel 沒有及時關閉,fdToKey 總是串行讀取的,而讀取是在 select 方法中進行的,該方法是非執行緒安全的,
Pipe:兩個執行緒之間的單向資料連接,資料會被寫到 sink 通道,從 source 通道讀取
NIO 的服務端建立程序:Selector.open():打開一個 Selector;ServerSocketChannel.open():創建服務端的 Channel;bind():系結到某個埠上,并配置非阻塞模式;register():注冊Channel 和關注的事件到 Selector 上;select()輪詢拿到已經就緒的事件
NIO與零拷貝
零拷貝是服務器網路編程的關鍵,任何性能優化都離不開,在 Java 程式員的世界,常用的零拷貝有 mmap 和 sendFile,
每一次的用戶態和內核態的背景關系切換就相當于一次80中斷(耗時)

傳統IO模型
上半部分表示用戶態和內核態的背景關系切換,下半部分表示資料復制操作

步驟:
1.read 呼叫導致用戶態到內核態的一次變化,同時,第一次復制開始:DMA(Direct Memory Access,直接記憶體存取,即不使用 CPU 拷貝資料到記憶體,而是 DMA 引擎傳輸資料到記憶體,用于解放 CPU) 引擎從磁盤讀取 index.html 檔案,并將資料放入到內核緩沖區,
2.發生第二次資料拷貝,即:將內核緩沖區的資料拷貝到用戶緩沖區,同時,發生了一次用內核態到用戶態的背景關系切換,
3.發生第三次資料拷貝,我們呼叫 write 方法,系統將用戶緩沖區的資料拷貝到 Socket 緩沖區,此時,又發生了一次用戶態到內核態的背景關系切換,
4.第四次拷貝,資料異步的從 Socket 緩沖區,使用 DMA 引擎拷貝到網路協議引擎,這一段,不需要進行背景關系切換,
5.write 方法回傳,再次從內核態切換到用戶態,
mmap 優化
mmap 通過記憶體映射,將檔案映射到內核緩沖區,同時,用戶空間可以共享內核空間的資料,這樣,在進行網路傳輸時,就可以減少內核空間到用戶控制元件的拷貝次數,如下圖:
只需要從內核緩沖區拷貝到 Socket 緩沖區即可,這將減少一次記憶體拷貝(從 4 次變成了 3 次),但不減少背景關系切換次數,
sendFile
資料根本不經過用戶態,直接從內核緩沖區進入到 Socket Buffer,同時,由于和用戶態完全無關,就減少了一次背景關系切換,
如上圖,我們進行 sendFile 系統呼叫時,資料被 DMA 引擎從檔案復制到內核緩沖區,然后呼叫,然后掉一共 write 方法時,從內核緩沖區進入到 Socket,這時,是沒有背景關系切換的,因為在一個用戶空間,最后,資料從 Socket 緩沖區進入到協議堆疊,
此時,資料經過了 3 次拷貝,3 次背景關系切換,
那么,還能不能再繼續優化呢? 例如直接從內核緩沖區拷貝到網路協議堆疊?
實際上,Linux 在 2.4 版本中,做了一些修改,避免了從內核緩沖區拷貝到 Socket buffer 的操作,直接拷貝到協議堆疊,從而再一次減少了資料拷貝,具體如下圖:

現在,進入到網路協議堆疊,只需 2 次拷貝:第一次使用 DMA 引擎從檔案拷貝到內核緩沖區,第二次從內核緩沖區將資料拷貝到網路協議堆疊;內核快取區只會拷貝一些 offset 和 length 資訊到 SocketBuffer,基本無消耗,
不是說零拷貝嗎?為什么還是要 2 次拷貝?
答:首先我們說零拷貝,是從作業系統的角度來說的,因為內核緩沖區之間,沒有資料是重復的(只有 kernel buffer 有一份資料,sendFile 2.1 版本實際上有 2 份資料,算不上零拷貝),例如我們剛開始的例子,內核快取區和 Socket 緩沖區的資料就是重復的,
而零拷貝不僅僅帶來更少的資料復制,還能帶來其他的性能優勢,例如更少的背景關系切換,更少的 CPU 快取偽共享以及無 CPU 校驗和計算,
mmap 和 sendFile 的區別
1.mmap 適合小資料量讀寫,sendFile 適合大檔案傳輸,
2.mmap 需要 4 次背景關系切換,3 次資料拷貝;sendFile 需要 3 次背景關系切換,最少 2 次資料拷貝,
3.sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket 緩沖區),
在這個選擇上:rocketMQ 在消費訊息時,使用了 mmap,kafka 使用了 sendFile,
Netty 的零拷貝
零拷貝的目的是為了減少IO流程中不必要的拷貝,以及減少用戶行程地址空間和內核地址空間之間因為背景關系切換而帶來的開銷,由于虛擬機不能直接操作內核,因此它的實作需要作業系統OS的支持,也就是需要kernel內核暴漏API
- Direct Buffers:Netty的接收和發送ByteBuffer采用直接緩沖區(Direct Buffer)實作零拷貝,直接在記憶體區域分配空間,避免了讀寫資料的二次記憶體拷貝,這就實作了讀寫Socket的零拷貝,
如果使用傳統的堆記憶體緩沖區(Heap Buffer)進行Socket讀寫,JVM會將堆記憶體Buffer拷貝到直接記憶體中,然后才寫入Socket中,相比堆外直接記憶體,訊息在發送程序中多了一次緩沖區的記憶體拷貝,
- CompositeByteBuf:它可以將多個ByteBuf封裝成ByteBuf,對外提供統一封裝后的ByteBuf介面,CompositeByteBuf并沒有真正將多個Buffer組合起來,而是保存了它們的參考,從而避免了資料的拷貝,實作了零拷貝
傳統的ByteBuffer,如果需要將兩個ByteBuffer中的資料組合到一起,我們需要首先創建一個size=size1+size2大小的新的陣列,然后將兩個陣列中的資料拷貝到新的陣列中,但是使用Netty提供的組合ByteBuf,就可以避免這樣的操作,
- Netty的檔案傳輸類DefaultFileRegion通過呼叫FileChannel.transferTo()方法實作零拷貝,檔案緩沖區的資料會直接發送給目標Channel,底層呼叫Linux作業系統中的sendfile()實作的,資料從檔案由DMA引擎拷貝到內核read緩沖區,;DMA從內核read緩沖區將資料拷貝到網卡介面(硬體)的緩沖區,由網卡進行網路傳輸,
Netty
Netty是一個異步的、基于事件驅動的網路應用框架,用以快速開發高性能、高可靠性的網路IO程式,
Netty主要針對在TCP協議下,面向Clinets端的高并發應用,或者Peer-to-Peer場景下的大量資料持續傳輸的應用,
Netty本質是一個NIO框架,適用于服務器通訊相關的多種應用場景,

Netty 的特點
- 一個高性能、異步事件驅動的 NIO 框架,它提供了對 TCP、UDP 和檔案傳輸的支持
- 使用更高效的 socket 底層,對 epoll 空輪詢引起的 cpu 占用飆升在內部進行了處理,避免了直接使用 NIO 的陷阱,簡化了 NIO 的處理方式,
- 采用多種 decoder/encoder 支持,對 TCP 粘包/分包進行自動化處理
- 可使用接受/處理執行緒池,提高連接效率,對重連、心跳檢測的簡單支持
- 可配置 IO 執行緒數、TCP 引數, TCP 接收和發送緩沖區使用直接記憶體代替堆記憶體,通過記憶體池的方式回圈利用 ByteBuf
- 通過參考計數器及時申請釋放不再參考的物件,降低了 GC 頻率
- 使用單執行緒串行化的方式,高效的 Reactor 執行緒模型
- 大量使用了 volitale、使用了 CAS 和原子類、執行緒安全類的使用、讀寫鎖的使用
Netty 執行緒模型
傳統阻塞IO服務模型
特點:1)采用阻塞IO模式獲取輸入資料
2)每個連接都需要獨立的執行緒完成資料的輸入,業務處理,資料回傳
問題:1)當并發數很大,就會創建大量的執行緒,占用很大系統資源
2)連接創建后,如果當前執行緒暫時沒有資料可讀,該執行緒會阻塞在read操作,造成執行緒資源浪費

Reactor模式:
針對傳統阻塞IO模型的2個缺點的解決方案:
1)基于IO復用模型:多個連接共用一個執行緒,應用程式只需要在一個阻塞物件等待,無需阻塞等待所有連接,當某個連接有新資料需處理時,作業系統通知應用程式,執行緒從阻塞狀態回傳,開始進行業務處理,
2)基于執行緒池復用執行緒資源:不必再為每個連接創建執行緒,將連接完成后的業務處理任務分配給執行緒進行處理,一個連接可以處理多個連接的業務,
1)Reactor模式,通過一個或者多個輸入同時傳遞給服務處理器的模式(基于事件驅動)
2)服務器端程式處理傳入的多個請求,并將它們同步分派到相應的處理執行緒,因此Reactor模式也叫dispatcher模式(分發者模式)
3)Reactor模式使用IO復用監聽事件,收到事件后,分發給某個執行緒(行程),這是網路服務器高并發處理關鍵
Reactor模式組成
1)Reactor:Reactor在一個單獨執行緒中運行,負責監聽和分發事件,分發給適當的處理程式來對IO事件做出反應
2)Handlers:處理程式執行IO事件要完成的實際事件,Reactor通過適度調度適當處理程式來回應IO事件,處理程式執行非阻塞操作,
Reactor模式分類
- 單Reactor單執行緒
所有 I/O 操作都由一個執行緒完成,即多路復用、事件分發和處理都是在一個Reactor 執行緒上完成的,既要接收客戶端的連接請求,向服務端發起連接,又要發送/讀取請求或應答/回應訊息,一個 NIO 執行緒同時處理成百上千的鏈路,性能上無法支撐,速度慢,若執行緒進入死回圈,整個程式不可用,對于高負載、大并發的應用場景不合適

1)Select是前面IO復用模型介紹的標準網路編程API,可以實作應用程式通過一個阻塞物件監聽多路連接請求
2)Reactor物件是通過Select監控客戶端請求事件,收到事件后通過Dispatch分發
3)如果是建立連接請求事件,則由Acceptor通過Accept處理連接請求,然后創建一個Handler物件處理連接完成后的后續業務處理
4)如果不是建立連接請求事件,則Reactor會分發呼叫連接對應的Handler來回應
5)Handler會完成Read->業務處理->Send的完整業務流程
- 單Reactor多執行緒
有一個 NIO 執行緒(Acceptor) 只負責監聽服務端,接收客戶端的 TCP 連接請求;NIO 執行緒池負責網路 IO 的操作,即訊息的讀取、解碼、編碼和發送;1 個 NIO 執行緒可以同時處理 N 條鏈路,但是 1 個鏈路只對應 1 個 NIO 執行緒,這是為了防止發生并發操作問題,但在并發百萬客戶端連接或需要安全認證時,一個 Acceptor 執行緒可能會存在性能不足問題,
1)Select是前面IO復用模型介紹的標準網路編程API,可以實作應用程式通過一個阻塞物件監聽多路連接請求
2)Reactor物件是通過Select監控客戶端請求事件,收到事件后通過Dispatch分發
3)如果是建立連接請求事件,則由Acceptor通過Accept處理連接請求,然后創建一個Handler物件處理連接完成后的后續業務處理
4)Handler只負責回應事件,不做具體的業務處理,通過read讀取資料后,會分發給后面的worker執行緒池的某個執行緒處理業務
5)worker執行緒池會分配獨立執行緒完成真正的業務,并將結果回傳給handler
6)handler收到回應后,通過send將結果回傳給client
- 主從Reactor多執行緒
Acceptor 執行緒用于系結監聽埠,接收客戶端連接,將 SocketChannel從主執行緒池的 Reactor 執行緒的多路復用器上移除,重新注冊到 Sub 執行緒池的執行緒上,用于處理 I/O 的讀寫等操作,從而保證 mainReactor 只負責接入認證、握手等操作;
1)Reactor主執行緒MainReactor物件通過select監聽連接事件,收到事件后,通過Acceptor處理連接事件
2)當Acceptor處理連接事件后,MainReactor將連接分配給SubReactor
3)SubReactor將連接加入到連接佇列進行監聽,并創建handler進行各種事件的處理
4)當有新事件發生時,SubReactor就會呼叫對應的handler處理
5)Handler通過read讀取資料后,會分發給后面的worker執行緒處理
6)worker執行緒池會分配獨立執行緒完成真正的業務,并將回傳結果
7)handler收到回應后,通過send將結果回傳給client
8)Reactor主執行緒可以對應多個Reactor子執行緒,即MainReactor可以關聯多個SubReactor
Netty模型
Netty執行緒模式主要基于主從Reactor多執行緒做了一定的改進,主從Reactor多執行緒模型有多個Reactor
Netty 通過 Reactor 模型基于多路復用器接收并處理用戶請求,內部實作了兩個執行緒池,
boss 執行緒池和 work 執行緒池,其中 boss 執行緒池的執行緒負責處理請求的 accept 事件,當接收到 accept 事件的請求時,把對應的 socket 封裝到一個 NioSocketChannel 中,并交給 work執行緒池,其中 work 執行緒池負責請求的 read 和 write 事件,由對應的 Handler 處理,

1)Netty抽象出兩組執行緒池BossGroup專門負責接收客戶端連接,WorkerGroup專門負責網路的讀寫
2)BossGroup和WorkerGroup型別都是NioEventLoopGroup
3)NioEventLoopGroup相當于一個事件回圈組,這個組中含有多個事件回圈,每一個回圈是NioEventLoop
4)NioEventLoop表示一個不斷回圈的執行處理任務的執行緒,每個NioEventLoop都有一個selector,用于監聽系結在其上的socket的網路通訊
5)NioEventLoopGroup可以有多個執行緒,即可以含有多個NioEventLoop
6)每個Boos NioEventLoop回圈執行步驟有3:
- 輪詢accept事件
- 處理accept事件,與client建立連接,生成NioScoketChannel,并將其注冊到某個worker NIOEventLoop上的selector
- 處理任務佇列的任務,即runAllTasks
7)每個Worker NioEventLoop回圈執行步驟:
- 輪詢read、write事件
- 處理IO事件,即read、write事件,在對應NioScoketChannel處理
- 處理任務佇列的任務,即runAllTasks
8)每個Worker NioEventLoop處理任務時,會使用pipeline(管道),pipeline包含了channel,即通過pipeline可以獲取對應通道,管道中維護了很多的處理器
Selector BUG:若 Selector 的輪詢結果為空,也沒有 wakeup 或新訊息處理,則發生空輪詢,CPU 使用率 100%,
Netty 的解決辦法:對 Selector 的 select 操作周期進行統計,每完成一次空的 select 操作進行一次計數,若在某個周期內連續發生 N 次空輪詢,則觸發了 epoll 死回圈 bug,重建
Selector,判斷是否是其他執行緒發起的重建請求,若不是則將原 SocketChannel 從舊的
Selector 上去除注冊,重新注冊到新的 Selector 上,并將原來的 Selector 關閉,
異步模型
1)與同步相對,當一個異步程序呼叫發出后,呼叫者不能立刻得到結果,實際上處理這個呼叫的組件在完成后,通過狀態、通知和回呼來通知呼叫者,
2)Netty中的IO操作是異步的,包括bind、write、connect 等操作會簡單回傳一個channelFuture
3)呼叫者不能立刻獲得結果,而是通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作的結果
4)Netty的異步模型是建立在future和callback的之上的,callback是回呼,重點說future,它的核心思想是:假設一個方法fun,計算程序可能非常耗時,等待fun回傳顯然不合適,那么可以在呼叫fun的時候,立馬回傳一個future,后續可以通過future去監控方法fun的處理程序(即Future-Listener機制)
Netty 的高性能
-
心跳,對服務端:會定時清除閑置會話 inactive(netty5),對客戶端:用來檢測會話是否斷開,是否重來,檢測網路延遲,其中 idleStateHandler 類 用來檢測會話狀態

-
串行無鎖化設計,即訊息的處理盡可能在同一個執行緒內完成,期間不進行執行緒切換,這樣就避免了多執行緒競爭和同步鎖,表面上看,串行化設計似乎 CPU 利用率不高,并發程度不夠,但是,通過調整 NIO 執行緒池的執行緒引數,可以同時啟動多個串行化的執行緒并行運行,這種區域無鎖化的串行執行緒設計相比一個佇列-多個作業執行緒模型性能更優,
-
可靠性,鏈路有效性檢測:鏈路空閑檢測機制,讀/寫空閑超時機制;記憶體保護機制:通過記憶體池重用 ByteBuf;ByteBuf 的解碼保護;優雅停機:不再接收新訊息、退出前的預處理操作、資源的釋放操作,
-
Netty 安全性:支持的安全協議:SSL V2 和 V3,TLS,SSL 單向認證、雙向認證和第三方 CA認證,
-
高效并發編程的體現:volatile 的大量、正確使用;CAS 和原子類的廣泛使用;執行緒安全容器的使用;通過讀寫鎖提升并發性能,IO 通信性能三原則:傳輸(AIO)、協議(Http)、執行緒(主從多執行緒)
-
流量整型的作用(變壓器):防止由于上下游網元性能不均衡導致下游網元被壓垮,業務流中斷;防止由于通信模塊接受訊息過快,后端業務執行緒處理不及時導致撐死問題,
-
TCP 引數配置:SO_RCVBUF 和 SO_SNDBUF:通常建議值為 128K 或者 256K;
SO_TCPNODELAY:NAGLE 演算法通過將緩沖區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網路,從而提高網路應用效率,但是對于時延敏感的應用場景需要關閉該優化演算法;
序列化
了解哪幾種序列化協議?
序列化(編碼)是將物件序列化為二進制形式(位元組陣列),主要用于網路傳輸、資料持久
化等;而反序列化(解碼)則是將從網路、磁盤等讀取的位元組陣列還原成原始物件,主要
用于網路傳輸物件的解碼,以便完成遠程呼叫,
影響序列化性能的關鍵因素:序列化后的碼流大小(網路帶寬的占用)、序列化的性能
(CPU 資源占用);是否支持跨語言(異構系統的對接和開發語言切換),
Java 默認提供的序列化:無法跨語言、序列化后的碼流太大、序列化的性能差
XML,優點:人機可讀性好,可指定元素或特性的名稱,缺點:序列化資料只包含資料本
身以及類的結構,不包括型別標識和程式集資訊;只能序列化公共屬性和欄位;不能序列
化方法;檔案龐大,檔案格式復雜,傳輸占帶寬,適用場景:當做組態檔存盤資料,實
時資料轉換,
JSON,是一種輕量級的資料交換格式,優點:兼容性高、資料格式比較簡單,易于讀寫、
序列化后資料較小,可擴展性好,兼容性好、與 XML 相比,其協議比較簡單,決議速度比
較快,缺點:資料的描述性比 XML 差、不適合性能要求為 ms 級別的情況、額外空間開銷
比較大,適用場景(可替代XML):跨防火墻訪問、可調式性要求高、基于 Web
browser 的 Ajax 請求、傳輸資料量相對小,實時性要求相對低(例如秒級別)的服務,
Fastjson,采用一種“假定有序快速匹配”的演算法,優點:介面簡單易用、目前 java 語言中
最快的 json 庫,缺點:過于注重快,而偏離了“標準”及功能性、代碼質量不高,檔案不
全,適用場景:協議互動、Web 輸出、Android 客戶端
Thrift,不僅是序列化協議,還是一個 RPC 框架,優點:序列化后的體積小, 速度快、支持
多種語言和豐富的資料型別、對于資料欄位的增刪具有較強的兼容性、支持二進制壓縮編
碼,缺點:使用者較少、跨防火墻訪問時,不安全、不具有可讀性,除錯代碼時相對困
難、不能與其他傳輸層協議共同使用(例如 HTTP)、無法支持向持久層直接讀寫資料,即
不適合做資料持久化序列化協議,適用場景:分布式系統的 RPC 解決方案
Avro,Hadoop 的一個子專案,解決了 JSON 的冗長和沒有 IDL 的問題,優點:支持豐富的資料型別、簡單的動態語言結合功能、具有自我描述屬性、提高了資料決議速度、快速可
壓縮的二進制資料形式、可以實作遠程程序呼叫 RPC、支持跨編程語言實作,缺點:對于
習慣于靜態型別語言的用戶不直觀,適用場景:在 Hadoop 中做 Hive、Pig 和 MapReduce
的持久化資料格式,
Protobuf,將資料結構以.proto 檔案進行描述,通過代碼生成工具可以生成對應資料結構的
POJO 物件和 Protobuf 相關的方法和屬性,優點:序列化后碼流小,性能高、結構化資料存盤格式(XML JSON 等)、通過標識欄位的順序,可以實作協議的前向兼容、結構化的檔案更容易管理和維護,缺點:需要依賴于工具生成代碼、支持的語言相對較少,官方只支持Java 、C++ 、python,適用場景:對性能要求高的 RPC 呼叫、具有良好的跨防火墻的訪問屬性、適合應用層物件的持久化
其它
protostuff 基于 protobuf 協議,但不需要配置 proto 檔案,直接導包即可
Jboss marshaling 可以直接序列化 java 類, 無須實 java.io.Serializable 介面
Message pack 一個高效的二進制序列化格式
Hessian 采用二進制協議的輕量級 remoting onhttp 工具
kryo 基于 protobuf 協議,只支持 java 語言,需要注冊(Registration),然后序列化(Output),反序列化(Input)
如何選擇序列化協議?
具體場景
對于公司間的系統呼叫,如果性能要求在 100ms 以上的服務,基于 XML 的 SOAP 協議是一個值得考慮的方案,
基于 Web browser 的 Ajax,以及 Mobile app 與服務端之間的通訊,JSON 協議是首選,對于性能要求不太高,或者以動態型別語言為主,或者傳輸資料載荷很小的的運用場景,JSON也是非常不錯的選擇,
對于除錯環境比較惡劣的場景,采用 JSON 或 XML 能夠極大的提高除錯效率,降低系統開
發成本,
當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro 之間具有一定的競爭關系,
對于 T 級別的資料的持久化應用場景,Protobuf 和 Avro 是首要選擇,如果持久化后的資料
存盤在 hadoop 子專案里,Avro 會是更好的選擇,
對于持久層非 Hadoop 專案,以靜態型別語言為主的應用場景,Protobuf 會更符合靜態類
型語言工程師的開發習慣,由于 Avro 的設計理念偏向于動態型別語言,對于動態語言為主
的應用場景,Avro 是更好的選擇,
如果需要提供一個完整的 RPC 解決方案,Thrift 是一個好的選擇,
如果序列化之后需要支持不同的傳輸層協議,或者需要跨防火墻訪問的高性能場景,
Protobuf 可以優先考慮,
protobuf 的資料型別有多種:bool、double、float、int32、int64、string、bytes、enum、
message,protobuf 的限定符:required: 必須賦值,不能為空、optional:欄位可以賦值,也
可以不賦值、repeated: 該欄位可以重復任意次數(包括 0 次)、列舉;只能用指定的常量
集中的一個值作為其值;
protobuf 的基本規則:每個訊息中必須至少留有一個 required 型別的欄位、包含 0 個或多
個 optional 型別的欄位;repeated 表示的欄位可以包含 0 個或多個資料;[1,15]之內的標識
號在編碼的時候會占用一個位元組(常用),[16,2047]之內的標識號則占用 2 個位元組,標識號一定不能重復、使用訊息型別,也可以將訊息嵌套任意多層,可用嵌套訊息型別來代替組,
protobuf 的訊息升級原則:不要更改任何已有的欄位的數值標識;不能移除已經存在的
required 欄位,optional 和 repeated 型別的欄位可以被移除,但要保留標號不能被重用,
新添加的欄位必須是 optional 或 repeated,因為舊版本程式無法讀取或寫入新增的required 限定符的欄位,
編譯器為每一個訊息型別生成了一個.java 檔案,以及一個特殊的 Builder 類(該類是用來創
建訊息類介面的),如:UserProto.User.Builder builder =UserProto.User.newBuilder();builder.build();
Netty 中的使用:ProtobufVarint32FrameDecoder 是用于處理半包訊息的解碼類;
ProtobufDecoder(UserProto.User.getDefaultInstance())這是創建的 UserProto.java 檔案中的解碼類;ProtobufVarint32LengthFieldPrepender 對 protobuf 協議的訊息頭上加上一個長度為32 的整形欄位,用于標志這個訊息的長度的類;ProtobufEncoder 是編碼類將 StringBuilder 轉換為 ByteBuf 型別:copiedBuffer()方法
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/123070.html
標籤:其他
上一篇:求助大佬們!!
下一篇:關于metabase的啟動問題
