Netty學習記錄-入門篇
- 2022 年 10 月 28 日
- 筆記
- 網絡通信框架Netty
模塊介紹
-
netty-bio
:阻塞型
網絡通信demo。 -
netty-nio
: 引入channel(通道)、buffer(緩衝區)、selector(選擇器)的概念,採用事件驅動的方式
,使用單個線程就可以監聽多個客戶端通道,改進bio模式下線程阻塞等待造成的資源浪費
。 -
netty-demo
: Netty小demo,認識Netty初體驗。 -
netty-groupchat
: 使用Netty編寫一個群聊系統。 -
netty-http
: Netty的HTTP調用demo。 -
netty-bytebuf
: Netty緩衝區使用demo。 -
netty-decoder
: Netty編解碼,handler調用鏈使用示例。 -
netty-idlestate
: Netty心跳包使用示例。 -
netty-sticking
: 自定義協議與handler,解決TCP傳輸粘包與拆包問題。 -
netty-rpc
: 使用Netty自定義實現RPC
通信。
Demo地址://gitee.com/LHDAXIE/netty
netty-bio
模塊
模擬測試採用socket的bio
方式進行網絡通信。
blocking io
:同步並阻塞
,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器就需要啟動一個線程進行處理,如果這個連接不做任何事情就會進入阻塞等待狀態,造成不必要的線程開銷。
適用於連接數據小且連接固定
的系統架構。
架構示意圖:
netty-nio
模塊
non-blocking io
:同步非阻塞
,在bio
的架構上進行改進,引入channel(通道)、buffer(緩衝區)、selector(選擇器)的概念,採用事件驅動的方式,使用單個線程就可以監聽多個客戶端通道,改進bio
模式下線程阻塞等待造成的資源浪費。
架構示意圖:
關鍵:select會根據不同的事件,在各個channel通道上進行切換。
本質上是一個可以讀寫數據(關鍵)的內存塊,nio
的讀取與寫入數據都必須是經過buffer的。
通道channel
把通道看做流、把通道看做流、把通道看做流,重要的事情說三遍,會很好理解。 nio
引入的通道類似bio
中流的概念,不同之處在於:
-
通道可以同時進行讀寫操作,而流只能讀或者寫
-
通道可以實現異步讀寫數據
-
通道可以從緩衝區讀數據,也可以寫數據到緩衝區(雙向的概念)
NIOFileOper01
: 本地文件寫數據
使用ByteBuffer
與FileChannel
,將「hello,李嘉圖」NIOFileOper01.txt
文件中。
NIOFileOper02
: 本地文件讀數據
使用ByteBuffer
(緩衝) 和 FileChannel
(通道), 將 NIOFileOper01.txt
中的數據讀入到程序,並顯示在控制台屏幕
NIOFileOper03
: 使用一個Buffer完成文件讀取
使用 FileChannel
(通道) 和 方法 read , write,完成文件的拷貝
NIOFileCopy
:拷貝文件 transferFrom
方法
使用 FileChannel
(通道) 和 方法 transferFrom
,完成文件的拷貝
選擇器Selector
核心:selector能夠檢測多個註冊的通道上是否有事件發生(多個channel以事件的方式可以註冊到同一個selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。 這樣就可以做到只使用一個單線程去管理多個通道。
只有在連接/通道真正有讀寫事件發生時,才會進行讀寫
,就大大地減少了系統開銷,並且不必為每個連接都創建一個線程,不用去維護多個線程。
原理圖:
說明:
-
當客戶端連接時,會通過
ServerSocketChannel
得到SocketChannel
。 -
Selector進行監聽select方法,返回有事件發生的通道的個數。
-
將
socketChannel
註冊到Selector上,register(),一個selector上可以註冊多個SocketChannel
。 -
註冊後返回一個
selectionKey
,會和該selector關聯。 -
進一步得到各個
selectionKey
(有事件發生)。 -
再通過
selectionKey
反向獲取socketChannel
,方法channel()。 -
可以通過得到的channel,完成業務邏輯。
Netty概述
異步的、基於事件驅動的網絡應用程序框架,用以快速開發高性能、高可靠的網絡IO程序。
有了NIO
為什麼還需要Netty?
不需要過於關注底層的邏輯,對下面的sdk等進行封裝,相當於簡化和流程化了NIO的開發過程
。spring
和springboot
的關係差不多。
因為 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模式小結
-
單Reactor單線程,前台接待員和服務員是同一個人,全程為客戶服務。
-
單Reactor多線程,1個前台接待員,多個服務員,接待員只負責接待。
-
主從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
通常是一個單線程的EventLoop
,EventLoop
維護着一個註冊了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本身自帶的 ObjectDecoder
和ObjectEncoder
可以用來實現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
,消費者和提供者約定接口和協議,消費者遠程調用提供者的服務,提供者返回一個字符串,消費者打印提供者返回的數據
設計說明:
-
創建一個接口,定義抽象方法,用於消費者和提供者之間的約定。
-
創建一個提供者,該類需要監聽消費者請求,並按照約定返回數據。
-