Netty學習記錄-入門篇

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

模塊介紹

  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地址://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請求提供者返回數據。