Netty學習記錄-入門篇

2022-10-28 18:01:21

你如果,緩緩把手舉起來,舉到頂,再突然張開五指,那恭喜你,你剛剛給自己放了個煙花。

模組介紹

  1. netty-bio: 阻塞型網路通訊demo。

  2. netty-nio: 引入channel(通道)、buffer(緩衝區)、selector(選擇器)的概念,採用事件驅動的方式,使用單個執行緒就可以監聽多個使用者端通道,改進bio模式下執行緒阻塞等待造成的資源浪費

  3. netty-demo: Netty小demo,認識Netty初體驗。

  4. netty-groupchat: 使用Netty編寫一個群聊系統。

  5. netty-http: Netty的HTTP呼叫demo。

  6. netty-bytebuf: Netty緩衝區使用demo。

  7. netty-decoder: Netty編解碼,handler呼叫鏈使用範例。

  8. netty-idlestate: Netty心跳包使用範例。

  9. netty-sticking: 自定義協定與handler,解決TCP傳輸粘包與拆包問題。

  10. netty-rpc: 使用Netty自定義實現RPC通訊。

Demo地址:https://gitee.com/LHDAXIE/netty

netty-bio模組

模擬測試採用socket的bio方式進行網路通訊。

blocking io同步並阻塞,伺服器實現模式為一個連線一個執行緒,即使用者端有連線請求時伺服器就需要啟動一個執行緒進行處理,如果這個連線不做任何事情就會進入阻塞等待狀態,造成不必要的執行緒開銷。

適用於連線資料小且連線固定的系統架構。

架構示意圖:

netty-nio模組

non-blocking io同步非阻塞,在bio的架構上進行改進,引入channel(通道)、buffer(緩衝區)、selector(選擇器)的概念,採用事件驅動的方式,使用單個執行緒就可以監聽多個使用者端通道,改進bio模式下執行緒阻塞等待造成的資源浪費。

架構示意圖:

關鍵:select會根據不同的事件,在各個channel通道上進行切換

緩衝區buffer

本質上是一個可以讀寫資料(關鍵)的記憶體塊,nio的讀取與寫入資料都必須是經過buffer的。

通道channel

把通道看做流、把通道看做流、把通道看做流,重要的事情說三遍,會很好理解。 nio引入的通道類似bio中流的概念,不同之處在於:

  • 通道可以同時進行讀寫操作,而流只能讀或者寫

  • 通道可以實現非同步讀寫資料

  • 通道可以從緩衝區讀資料,也可以寫資料到緩衝區(雙向的概念)

NIOFileOper01: 本地檔案寫資料

使用ByteBufferFileChannel,將「hello,李嘉圖」NIOFileOper01.txt檔案中。

NIOFileOper02: 本地檔案讀資料

使用ByteBuffer(緩衝) 和 FileChannel(通道), 將 NIOFileOper01.txt中的資料讀入到程式,並顯示在控制檯螢幕

NIOFileOper03: 使用一個Buffer完成檔案讀取

使用 FileChannel(通道) 和 方法 read , write,完成檔案的拷貝

NIOFileCopy:拷貝檔案 transferFrom 方法

使用 FileChannel(通道) 和 方法 transferFrom ,完成檔案的拷貝

選擇器Selector

核心:selector能夠檢測多個註冊的通道上是否有事件發生(多個channel以事件的方式可以註冊到同一個selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。 這樣就可以做到只使用一個單執行緒去管理多個通道。

只有在連線/通道真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,並且不必為每個連線都建立一個執行緒,不用去維護多個執行緒。

原理圖:

說明:

  1. 當用戶端連線時,會通過ServerSocketChannel得到SocketChannel

  2. Selector進行監聽select方法,返回有事件發生的通道的個數。

  3. socketChannel註冊到Selector上,register(),一個selector上可以註冊多個SocketChannel

  4. 註冊後返回一個selectionKey,會和該selector關聯。

  5. 進一步得到各個selectionKey(有事件發生)。

  6. 再通過selectionKey反向獲取socketChannel,方法channel()。

  7. 可以通過得到的channel,完成業務邏輯。

Netty概述

非同步的基於事件驅動的網路應用程式框架,用以快速開發高效能、高可靠的網路IO程式。

有了NIO為什麼還需要Netty?

不需要過於關注底層的邏輯,對下面的sdk等進行封裝,相當於簡化和流程化了NIO的開發過程springspringboot的關係差不多。

因為 Netty 5出現重大bug,已經被官網廢棄了,目前推薦使用的是Netty 4.x的穩定版本。

Netty高效能架構設計

執行緒模型基本介紹

傳統阻塞 I/O 服務模型

模型特點:

  • 採用阻塞IO模式獲取輸入的資料

  • 每個連線都需要獨立的執行緒完成資料的輸入,業務處理,資料返回

問題分析:

  • 當並行數很大,就會建立大量的執行緒,佔用很大系統資源

  • 連線建立後,如果當前執行緒暫時沒有資料可讀,該執行緒會阻塞在read操作,造成執行緒資源浪費

Reactor 模式

I/O 複用結合執行緒池,就是 Reactor 模式基本設計思想。

Reactor在一個單獨的執行緒中執行,負責監聽和分發事件,分發給適當的處理程式來對IO事件作出反應。它像公司的電話接線員,接聽來自客戶的電話並將線路轉譯到適當的聯絡人。

單 Reactor 單執行緒

  • 優點:模型簡單,沒有多執行緒、程序通訊、競爭問題,全部都在一個執行緒中完成。

  • 缺點:效能問題,只有一個執行緒,無法完全發揮多核CPU效能。Handler在處理某個連線上的業務時,整個程序無法處理其他連線事件,很容易導致效能瓶頸。

單 Reactor 多執行緒

在上一代的問題上進行修改,Reactor主執行緒只負責響應事件,不做具體的業務處理,通過read讀取資料後,會分發給後面的worker執行緒池的某個執行緒處理業務。

  • 優點:充分利用多核CPU的處理能力。

  • 缺點:多執行緒資料共用和存取比較複雜,Reactor處理所有的事件監聽與響應,在單執行緒執行,在高並行場景容易出現效能瓶頸。

主從 Reactor 多執行緒

針對單 Reactor 多執行緒模型中,Reactor 在單執行緒中執行,高並行場景下容易成為效能瓶頸,可以讓 Reactor 在多執行緒中執行。

Reactor主執行緒MainReactor物件通過select監聽連線事件,收到事件後,通過Acceptor處理連線事件。當Acceptor處理連線事件後,MainReactor將連線分配給 SubReactor,SubReactor將連線加入到連線佇列進行監聽,並建立Handler進行各種事件處理。

  • 優點:父執行緒與子執行緒的資料互動簡單職責明確,父執行緒只需要接收新連線,子執行緒完成後續的業務處理,無需返回資料給主執行緒

  • 缺點:程式設計複雜度較高。

Reactor模式小結

  1. 單Reactor單執行緒,前臺接待員和服務員是同一個人,全程為客戶服務。

  2. 單Reactor多執行緒,1個前臺接待員,多個服務員,接待員只負責接待。

  3. 主從Reactor多執行緒,多個前臺接待員,多個服務生。

Netty 模型

  • Netty抽象出兩組執行緒池,BossGroup專門負責接收使用者端的連線,WorkerGroup專門負責網路的讀寫。

  • 每個worker nioEventLoop處理業務時,會使用pipeline(管道),pipeline中包含了channel,即通過pipeline可以獲取到對應通道,管道中維護了很多的處理器。

非同步模型

基本介紹

  • 非同步的概念和同步相對。當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果。實際處理這個呼叫的元件完成後,通過狀態、通知和回撥來通知呼叫者。

  • Netty中的I/O操作是非同步的,包括Bind、Write、Connect等操作會簡單的返回一個 ChannelFuture

  • 呼叫者不能立刻獲得結果,而是通過 Future-Listener機制,使用者可以方便地主動獲取或者通過通知機制獲得I/O操作結果。

  • Netty的非同步模型是建立在future和callback(回撥)之上的。重點是future,它的核心思想:假設一個方法func,計算過程可能非常耗時,等待func返回顯然不合適。那麼在 呼叫func的時候,立刻返回一個future,後續可以

    通過future去監控方法func的處理過程(即:Future-Listener機制)

    • ChannelFuture是一個介面:Public interface ChannelFuture extends Future

    • 可以新增監聽器,當監聽的事件發生時,就會通知到監聽器。

  • 在使用Netty進行程式設計時,攔截操作和轉換出入站資料只需要你提供callback或利用future即可。這使得鏈式操作簡單、高效、並有利於編寫可重用、通用的程式碼。

Future-Listener機制

當Future物件剛剛建立好時,處於非完成狀態,呼叫者可以通過返回的channelFuture來獲取操作執行的狀態,註冊監聽函數來執行完成後的操作。

常見的操作:

- 通過 isDone 方法來判斷當前操作是否完成。
- 通過 isSuccess 方法來判斷已完成的當前操作是否成功。
- 通過 getCause 方法來獲取已完成的當前操作失敗的原因。
- 通過 isCancelled 方法來判斷已完成的當前操作是否被取消。
- 通過 addListener 方法來註冊監聽器,當操作已完成(isDone),將會通知指定的監聽器。

小結:相比於傳統阻塞I/O,執行I/O操作後執行緒會被阻塞住,直到操作完成。非同步處理的好處是不會造成執行緒阻塞,執行緒在I/O操作期間可以執行別的程式,在高並行情形下會 更穩定和更高的吞吐量。

Netty 核心模組元件

ServerBootstrap、Bootstrap

Bootstrap意思是引導,一個Netty應用通常由一個Bootstrap開始,主要作用是設定整個Netty程式,串聯各個元件,Netty中Bootstrap類是使用者端程式的啟動引導類, ServerBootstrap是伺服器啟動引導類。

常用方法:

- public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup):用於伺服器端,用來設定兩個EventLoop
- public B group(EventLoopGroup group):該方法用於使用者端,用來設定一個EventLoop
- public B channel(Class<? extends C> channelClass):該方法用來設定一個伺服器端的通道實現
- public B option(ChannelOption option, T value):用來給ServerChannel新增設定
- public ServerBootstrap childOption(ChannelOption childOption, T value):用來給接收的通道新增設定
- public ServerBootstrap childHandler(ChannelHandler childHandler):業務處理類,自定義handler
- public ChannelFuture bind(int inetPort):用於伺服器端,用來設定佔用的埠號
- public ChannelFuture connect(String inetHost, int inetPort):用於使用者端,用來連線伺服器端

Future、ChannelFuture

Netty中所有的IO操作都是非同步的,不能立刻得知訊息是否被正確處理。但是可以過一會等它執行完成或者直接註冊一個監聽,具體的實現就是通過Future和ChannelFuture, 他們可以註冊一個監聽,當操作執行成功或失敗時監聽會自動觸發註冊的監聽事件

常用的方法:

- Channel channel():返回當前正在進行IO操作的通道
- ChannelFuture sync():等待非同步操作執行完畢

Channel

Netty網路通訊的元件,能夠用於執行網路 I/O 操作。 通過 Channel 可獲得當前網路連線的通道的狀態。 通過 Channel 可獲得 網路連線的設定引數 (例如接收緩衝區大小)。 Channel 提供非同步的網路 I/O 操作(如建立連線,讀寫,繫結埠),非同步呼叫意味著任何 I/O 呼叫都將立即返回,並且不保證在呼叫結束時所請求的 I/O 操作已完成 呼叫立即返回一個 ChannelFuture範例,通過註冊監聽器到ChannelFuture上,可以 I/O 操作成功、失敗或取消時回撥通知呼叫方。 不同協定、不同的阻塞型別的連線都有不同的 Channel 型別與之對應,常用的 Channel 型別:

- NioSocketChannel,非同步的使用者端 TCP Socket 連線。
- NioServerSocketChannel,非同步的伺服器端 TCP Socket 連線。
- NioDatagramChannel,非同步的 UDP 連線。
- NioSctpChannel,非同步的使用者端 Sctp 連線。
- NioSctpServerChannel,非同步的 Sctp 伺服器端連線,這些通道涵蓋了 UDP TCP 網路 IO 以及檔案 IO。

實際開發過程中,在拿到channel之後,做一個判斷,看是什麼連線,如(channel instanceof SocketChannel/DatagramChannel),就可以做不同的業務處理。

Selector

Netty基於Selector物件實現I/O多路複用,通過Selector一個執行緒可以監聽多個連線的Channel事件。當向一個Selector中註冊Channel後, Selector內部的機制就可以自動不斷地查詢(Select)這些註冊的Channel是否有已就緒的I/O事件(例如可讀,可寫,網路連線完成等), 這樣程式就可以很簡單地使用一個執行緒高效地管理多個Channel。

ChannelHandler 及其實現類

ChannelHandler是一個介面,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程式

ChannelHandler及其實現類一覽圖:

- ChannelInboundHandler 用於處理入站 I/O 事件。
- ChannelOutboundHandler 用於處理出站 I/O 操作。
- ChannelInboundHandlerAdapter 用於處理入站 I/O 事件。
- ChannelOutboundHandlerAdapter 用於處理出站 I/O 操作。
- ChannelDuplexHandler 用於處理入站和出站事件。

Pipeline 和 ChannelPipeline

ChannelPipeline 是一個 Handler 的集合,它負責處理和攔截 inbound 或者 outbound 的事件和操作,相當於一個貫穿 Netty 的鏈。(也可以這樣理解:ChannelPipeline 是 儲存 ChannelHandler 的 List,用於處理或攔截 Channel 的入站事件和出站操作)。

ChannelPipeline 實現了一種高階形式的攔截過濾器模式,使使用者可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互互動。

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

一個 Channel 包含了一個 ChannelPipeline,而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向連結串列,並且每個ChannelHandlerContext中又關聯著一個 ChannelHandler

入站事件和出站事件在一個雙向連結串列中,入站事件會從連結串列 head 往後傳遞到最後一個入站的 handler,出站事件會從連結串列 tail 往前傳遞到最前一個出站的 handler,兩種型別的 handler 互不干擾。

常用方法:

- ChannelPipeline addFirst(ChannelHandler... handlers),把一個業務處理類(handler)新增到鏈中的第一個位置。
- ChannelPipeline addLast(ChannelHandler... handlers),把一個業務處理類(handler)新增到鏈中的最後一個位置。

ChannelHandlerContext

儲存Channel相關的所有上下文資訊,同時關聯一個ChannelHandler物件ChannelHandlerContext中包含一個具體的事件處理器ChannelHandler,同時ChannelHandlerContext 中也繫結了對應的pipeline和Channel的資訊,方便對ChannelHandler進行呼叫。

常用方法:

- ChannelFuture close(): 關閉通道
- ChannelOutboundInvoker flush(): 重新整理
- ChannelFuture writeAndFlush(Object msg): 將資料寫到ChannelPipeline中當前ChannelHandler的下一個ChannelHandler開始處理。

ChannelOption

  • ChannelOption.SO_BACKLOG

    • 對應TCP/IP協定listen函數中的backlog引數,用來初始化伺服器可連線佇列大小。伺服器端處理使用者端連線請求時順序處理的,所以同一時間只能處理一個使用者端連線。 多個使用者端來的時候,伺服器將不能處理的使用者端連線請求放在佇列中等待處理,backlog引數指定了佇列的大小。

  • ChannelOption.SO_KEEPALIVE

    • 一直保持連線活動狀態。

EventLoopGroup 和其實現類 NioEventLoopGroup

  • BoosEventLoopGroup通常是一個單執行緒的EventLoopEventLoop維護著一個註冊了ServerSocketChannel的Selector範例,BossEventLoop不斷輪詢將連線事件分離出來。

  • 通常是OP_ACCEPT事件,然後將接收到的SocketChannel交給WorkerEventLoopGroup

  • WorkerEventLoopGroup會由next選擇其中一個EventLoop來將這個SocketChannel註冊到其維護的Selector並對其後續的IO事件進行處理。

常用方法:

- public NioEventLoopGroup(): 構造方法
- public Future<?> shutdownGracefully(): 斷開連線,關閉執行緒

Unpooled類

Netty提供一個專門用來操作緩衝區(即Netty的資料容器)的工具類

常用方法如下:

public static ByteBuf copiedBuffer(CharSequence String, Charset charset):通過給定的資料和字元編碼返回一個ByteBuf物件(類似於NIO中的ByteBuffer)

Google Protobuf

Netty本身自帶的 ObjectDecoderObjectEncoder可以用來實現POJO物件或各種業務物件的編碼和解碼,底層使用的仍然是Java序列化技術,而Java序列化技術本身效率就不高,存在如下問題:

  • 無法跨語言

  • 序列化後的體積太大,是二進位制的5倍多

  • 序列化效能太低 引出新的解決方案:Google的Protobuf

Netty編解碼器和handler的呼叫機制

程式碼範例:netty-decoder模組

使用自定義的編碼器和解碼器來說明Netty的handler呼叫機制

  • 使用者端傳送long -> 伺服器

  • 伺服器傳送long -> 使用者端

結論:

  • 不論解碼器handler還是編碼器handler接收的訊息型別必須與待處理的訊息型別一致,否則該handler不會被執行

  • 在解碼器進行資料解碼時,需要判斷快取區(ByteBuf)的資料是否足夠,否則接收到的結果會與期望的結果可能不一致。

    • ReplayingDecoder擴充套件了ByteToMessageDecoder類,使用這個類,我們不必呼叫readableBytes()方法。引數S指定了使用者狀態管理的型別,其中Void代表不需要狀態管理。

    • ReplayingDecoder使用方便,但它也有一些侷限性:

    • 並不是所有的 ByteBuf操作都被支援,如果呼叫了一個不被支援的方法,將會丟擲一個 UnsupportedOperationException

    • ReplayingDecoder 在某些情況下可能稍慢於 ByteToMessageDecoder,例如網路緩慢並且訊息格式複雜時,訊息會被拆成了多個碎片,速度變慢。

TCP粘包與拆包及解決方案

  • TCP是面向連線的,面向流的,提供高可靠性服務。收發兩端(使用者端和伺服器端)都要有——成對的socket,因此,傳送端為了將多個傳送給接收端的包,更有效的傳送給對方, 使用了優化演演算法(Nagle演演算法),將多次間隔較小且資料量小的資料,合併成一個大的資料塊,然後進行封包。這樣做雖然提高了效率,但是接收端就難於分辨出完整的封包了, 因為面向流的通訊是無訊息保護邊界的

TCP粘包與拆包解決方案

  • 使用 自定義協定 + 編解碼器 來解決

  • 關鍵就是要解決 伺服器端每次讀取資料長度的問題,這個問題解決,就不會出現伺服器多讀或少讀資料的問題,從而避免TCP粘包、拆包。

程式碼範例:

  • 要求使用者端傳送5個Message物件,使用者端每次傳送一個Message物件

  • 伺服器端每次接收一個Message,分5次進行解碼,每讀取到一個Message,會回覆一個Message物件給使用者端

Netty 核心原始碼剖析

只有看過Netty原始碼,才能說是真的掌握了Netty框架。

判斷是否為 2 的 n 次方

private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
}

原始碼解析:

  • Netty啟動過程原始碼剖析

  • Netty接受請求過程原始碼剖析

  • Pipeline Handler HandlerContext建立原始碼剖析

  • ChannelPipeline是如何排程handler的

  • Netty心跳(heartbeat)服務原始碼剖析

  • Netty核心元件EventLoop原始碼剖析

  • handler中加入執行緒池和Context中新增執行緒池的原始碼剖析

用Netty 自己 實現 dubbo RPC

  • RPC(Remote Procedure call) - 遠端程式呼叫,是一個計算機通訊協定。該協定允許執行與一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計。

  • 兩個或多個應用程式都分佈在不同的伺服器上,它們之間的呼叫都像是本地方法呼叫一樣。

  • 常見的PRC框架有:阿里的Dubbo、Google的gRPC、Go語言的rpcx,spring的Spring cloud。

  • RPC的目標就是將 2-8 這些步驟都封裝起來,使用者無需關心這些細節,可以像呼叫本地方法一樣即可完成遠端服務呼叫。

自己實現 Dubbo RPC(基於Netty)

需求說明:

  • Dubbo底層使用了Netty作為網路通訊框架,要求用Netty實現一個簡單的RPC框架

  • 模仿Dubbo,消費者和提供者約定介面和協定,消費者遠端呼叫提供者的服務,提供者返回一個字串,消費者列印提供者返回的資料

設計說明:

  • 建立一個介面,定義抽象方法,用於消費者和提供者之間的約定。

  • 建立一個提供者,該類需要監聽消費者請求,並按照約定返回資料。

  • 建立一個消費者,該類需要透明的呼叫自己不存在的方法,內部需要使用Netty請求提供者返回資料。