5.1 線程模型基本介紹
-
不同的線程模式,對程序的性能有很大影響,爲了搞清 Netty 線程模式,瞭解下 各個線程模式,最後看看 Netty 線程模型有什麼優越性.
-
目前存在的線程模型有:傳統阻塞 I/O 服務模型,Reactor 模式
-
根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現
- 單 Reactor 單線程;
- 單 Reactor 多線程;
- 主從 Reactor 多線程
- Netty 線程模式(Netty 主要基於主從 Reactor 多線程模型做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor)
5.2 傳統阻塞 I/O 服務模型
工作原理圖
- 黃色的框表示對象, 藍色的框表示線程
- 白色的框表示方法(API)
模型特點
- 採用阻塞 IO 模式獲取輸入的數據
- 每個連接都需要獨立的線程完成數據的輸入,業務處理,數據返回
問題分析
- 當併發數很大,就會創建大量的線程,佔用很大系統資源
- 連接創建後,如果當前線程暫時沒有數據可讀,該線程會阻塞在 read方法的操作,造成線程資源浪費
5.3 Reactor 模式
5.3.1針對傳統阻塞 I/O 服務模型的 2 個缺點,解決的基礎方案:
-
基於 I/O 複用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理 ----解決BIO的read方法阻塞問題
Reactor 對應的叫法: 1. 反應器模式 2. 分發者模式(Dispatcher) 3. 通知者模式(notifier) -
基於線程池複用線程資源:不必再爲每個連接創建線程,將連接完成後的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。----解決BIO的一個Client一個Thread
5.3.2 I/O 複用結合線程池,就是 Reactor 模式基本設計思想,如圖
說明:
-
Reactor 模式,通過一個或多個輸入同時傳遞給服務處理器的模式(基於事件驅動)
-
服務器端程序處理傳入的多個請求,並將它們同步分派到相應的處理線程, 因此 Reactor 模式也叫 Dispatcher模式,上圖中的ServiceHandler 就類似 Reactor 反應器
-
Reactor 模式使用 IO 複用監聽事件, 收到事件後,分發給某個線程(進程), 這點就是網絡服務器高併發處理關鍵
5.3.3 Reactor 模式中 核心組成:
-
Reactor:Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對 IO 事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯繫人;
-
Handlers:處理程序執行 I/O 事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作。
5.3.4 Reactor 模式分類:
根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現
- 單 Reactor 單線程
- 單 Reactor 多線程
- 主從 Reactor 多線程
5.4 單 Reactor 單線程
原理圖,並使用 NIO 羣聊系統驗證,之前的羣聊系統就是單 Reactor 單線程模型
5.4.1方案說明:
- Select 是前面 I/O 複用模型介紹的標準網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求
- Reactor 對象通過 Select 監控客戶端請求事件,收到事件後通過 Dispatch 進行分發
- 如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然後創建一個Handler 對象處理連接完成後的後續業務處理
- 如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應
- Handler 會完成 Read→業務處理→Send 的完整業務流程
結合實例:服務器端用一個線程通過多路複用搞定所有的 IO 操作(包括連接,讀、寫等),編碼簡單,清晰明瞭,但是如果客戶端連接數量較多,將無法支撐,前面的 NIO 案例就屬於這種模型
5.4.2方案優缺點分析:
- 優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成
- 缺點:性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸
- 缺點:可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障
- 使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis 在業務處理的時間複雜度O(1) 的情況
5.5 單 Reactor 多線程
5.5.1 原理圖
5.5.2 對上圖的小結
- Reactor 對象通過 select 監控客戶端請求事件, 收到事件後,通過 dispatch 進行分發
- 如果建立連接請求, 則右 Acceptor 通過accept 處理連接請求, 然後創建一個 Handler 對象處理完成連接後的各種事件
- 如果不是連接請求,則由 reactor 分發調用連接對應的 handler 來處理
- handler 只負責響應事件,不做具體的業務處理, 通過 read 讀取數據後,會分發給後面的 worker 線程池的某個線程處理業務
- worker 線程池會分配獨立線程完成真正的業務,並將結果返回給 handler
- handler 收到響應後,通過 send 將結果返回給 client
5.5.3方案優缺點分析:
- 優點:可以充分的利用多核 cpu 的處理能力
- 缺點:多線程數據共享和訪問比較複雜, reactor 處理所有的事件的監聽和響應,在單線程運行, 在高併發場景容易出現性能瓶頸.
5.6 主從 Reactor 多線程
5.6.1 工作原理圖
針對單 Reactor 多線程模型中,Reactor 在單線程中運行,高併發場景下容易成爲性能瓶頸,可以讓 Reactor 在多線程中運行
注意:這裏的Reactor子線程以下部分是有多個的,即多個從 SubReactor ,開發要麼加緩存,要麼架構分層;
5.6.2上圖的方案說明
- Reactor 主線程 MainReactor 對象通過 select 監聽連接事件, 收到事件後,通過 Acceptor 處理連接事件
- 當 Acceptor 處理連接事件後,MainReactor 將連接分配給 SubReactor(多個)
- subreactor 將連接加入到連接隊列進行監聽,並創建 handler 進行各種事件處理
- 當有新事件發生時, subreactor 就會調用對應的 handler 處理
- handler 通過 read 讀取數據,分發給後面的 worker 線程處理
- worker 線程池分配獨立的 worker 線程進行業務處理,並返回結果
- handler 收到響應的結果後,再通過 send 將結果返回給 client
- Reactor 主線程可以對應多個 Reactor 子線程, 即 MainRecator 可以關聯多個 SubReactor
5.6.3 Scalable IO in Java 對 Multiple Reactors 的原理圖解:
5.6.4方案優缺點說明:
- 優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成後續的業務處理。
- 優點:父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
- 缺點:編程複雜度較高
結合實例:
這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持
5.7 Reactor 模式小結
5.7.13 種模式用生活案例來理解
- 單 Reactor 單線程,前臺接待員和服務員是同一個人,全程爲顧客服
- 單 Reactor 多線程,1 個前臺接待員,多個服務員,接待員只負責接待
- 主從 Reactor 多線程,多個前臺接待員,多個服務生
5.7.2 Reactor 模式具有如下的優點:
- 響應快,不必爲單個同步時間所阻塞,雖然 Reactor 本身依然是同步的
- 可以最大程度的避免複雜的多線程及同步問題,並且避免了多線程/進程的切換開銷
- 擴展性好,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源
- 複用性好,Reactor 模型本身與具體事件處理邏輯無關,具有很高的複用性
5.8 Netty 模型
5.8.1 工作原理示意圖 1-簡單版
Netty 主要基於主從 Reactors 多線程模型(如上圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor
5.8.2 對上圖說明
- BossGroup 線程維護 Selector , 只關注 Accecpt事件
- 當接收到 Accept 事件,獲取到對應的 SocketChannel, 封裝成 NIOScoketChannel 並註冊到 Worker 線程(事件循環), 並進行維護
- 當 Worker 線程監聽到 selector 中通道發生自己感興趣的事件後,就進行處理(就由handler), 注意 handler 之前就已經加入到通道
5.8.3工作原理示意圖 2-進階版
Netty 主要基於主從 Reactors 多線程模型(如圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor
5.8.4工作原理示意圖-詳細版
5.8.5對上圖的說明小結
- Netty 抽象出兩組線程池 BossGroup 專門負責接收客戶端的連接, WorkerGroup 專門負責網絡的讀寫
- BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup,圖中NioEventGroup應該是NioEventLoopGroup
- NioEventLoopGroup 相當於一個事件循環組, 這個組中含有多個事件循環 ,每一個事件循環是 NioEventLoop
- NioEventLoop 表示一個不斷循環的執行處理任務的線程, 每個 NioEventLoop 都有一個 selector , 用於監聽綁定在其上的 socket 的網絡通訊
- NioEventLoopGroup 可以有多個線程, 即可以含有多個 NioEventLoop
- 每個 Boss NioEventLoop 循環執行的步驟有 3 步
1 輪詢 accept 事件
2 處理 accept 事件 , 與 client 建立連接 , 生成 NioScocketChannel , 並將其註冊到某個 worker NIOEventLoop 上的 selector,至於是註冊到哪個selector上這個看算法的實現
3 處理任務隊列的任務 , 即 runAllTasks - 每個 Worker NIOEventLoop 循環執行的步驟
1 輪詢 read, write 事件
2 處理 i/o 事件, 即 read , write 事件,在對應 NioScocketChannel 處理
3 處理任務隊列的任務 , 即 runAllTasks - 每個Worker NIOEventLoop 處理業務時,會使用pipeline(管道), pipeline 中包含了 channel , 即通過pipeline可以獲取到對應通道, 管道中維護了很多的 處理器
5.8.6 Netty 快速入門實例-TCP 服務
實例要求:使用 IDEA 創建 Netty 項目
- Netty 服務器在 6668 端口監聽,客戶端能發送消息給服務器 “hello, 服務器~”
- 服務器可以回覆消息給客戶端 “hello, 客戶端~”,接收消息的工作是由 channel 關聯的Pipeline 的 ChannelHandler(現有+自定義) 來處理,Channel 和 Pipeline 是互相包含的
- 目的:對 Netty 線程模型 有一個初步認識, 便於理解 Netty 模型理論
…略…
5.8.7任務隊列中的 Task 有 3 種典型使用場景
1) 用戶程序自定義的普通任務
//比如這裏我們有一個非常耗時長的業務-> 異步執行 -> 提交該channel 對應的
//NIOEventLoop 的 taskQueue中, taskQueue自定義任務第49節視頻,debug可以看到是提交到taskQueue中
//解決方案1 用戶程序自定義的普通任務
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵2", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵3", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
}
});
//上面如果啓動2個任務,會順序執行,要花10秒才執行完,因爲執行的是同一個線程的,2個任務都會提交到taskQueue中;
2) 用戶自定義定時任務
//解決方案2 : 用戶自定義定時任務 ---》 該任務是提交到 scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵4", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
3) 非當前 Reactor 線程調用 Channel 的各種方法
例如在推送系統的業務線程裏面,根據用戶的標識,找到對應的 Channel 引用,然後調用 Write 類方法向該用戶推送消息,就會進入到這種場景。最終的 Write 會提交到任務隊列中後被異步消費
思路:用集合存放SocketChannel,根據用戶標識存放;
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("客戶socketChannel hashcode=" + ch.hashCode());
//可以使用一個集合管理 SocketChannel, 再推送消息時,
// 可以將業務加入到各個channel 對應的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
});
5.9 異步模型
5.9.1 基本介紹
-
異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的組件在完成後,通過狀態、通知和回調來通知調用者。
-
Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture。
-
調用者並不能立刻獲得結果,而是通過 Future-Listener 機制,用戶可以方便的主動獲取或者通過通知機制獲得IO 操作結果
-
Netty 的異步模型是建立在 future 和 callback 的之上的。callback 就是回調。重點說 Future,它的核心思想是:假設一個方法 fun,計算過程可能非常耗時,等待 fun 返回顯然不合適。那麼可以在調用 fun 的時候,立馬返回一個 Future,後續可以通過 Future 去監控方法 fun 的處理過程(即 : Future-Listener 機制)
5.9.2 Future 說明
- 表示 異步的執行結果, 可以通過它提供的方法來檢測執行是否完成,比如檢索計算等等.
- ChannelFuture 是一個接口 :
public interface ChannelFuture extends Future<Void>
我們可以添 加監聽器,當監聽的事件發生時,就會通知到監聽器. 案例說明
//綁定一個端口並且同步, 生成了一個 ChannelFuture 對象
//啓動服務器(並綁定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//給cf 註冊監聽器,監控我們關心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("監聽端口 6668 成功");
} else {
System.out.println("監聽端口 6668 失敗");
}
}
});
5.9.3 工作原理示意圖
說明:
- 在使用 Netty 進行編程時,攔截操作和轉換出入站數據只需要您提供 callback 或利用 future 即可。這使得鏈式操作簡單、高效, 並有利於編寫可重用的、通用的代碼。
- Netty 框架的目標就是讓你的業務邏輯從網絡基礎應用編碼中分離出來、解脫出來
5.9.4 Future-Listener 機制
- 當 Future 對象剛剛創建時,處於非完成狀態,調用者可以通過返回的 ChannelFuture 來獲取操作執行的狀態,註冊監聽函數來執行完成後的操作。
- 常見有如下操作
通過 isDone 方法來判斷當前操作是否完成;
通過 isSuccess 方法來判斷已完成的當前操作是否成功;
通過 getCause 方法來獲取已完成的當前操作失敗的原因;
通過 isCancelled 方法來判斷已完成的當前操作是否被取消;
通過 addListener 方法來註冊監聽器,當操作已完成(isDone 方法返回完成),將會通知指定的監聽器;如果Future 對象已完成,則通知指定的監聽器
5.10 快速入門實例-HTTP 服務
- 實例要求:使用 IDEA 創建 Netty 項目
- Netty 服務器在 6668 端口監聽,瀏覽器發出請求 "http://localhost:6668/ "
- 服務器可以回覆消息給客戶端 "Hello! 我是服務器 5 " , 並對特定請求資源進行過濾.
- 目的:Netty 可以做 Http 服務開發,並且理解 Handler 實例和客戶端及其請求的關係.
- 代碼演示
public class TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(6668).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向管道加入處理器
//得到管道
ChannelPipeline pipeline = ch.pipeline();
//加入一個netty 提供的httpServerCodec codec =>[coder - decoder] 編解碼器
//HttpServerCodec 說明
//1. HttpServerCodec 是netty 提供的處理http的 編-解碼器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//2. 增加一個自定義的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
System.out.println("ok~~~~");
}
}
/*
說明
1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
2. HttpObject 客戶端和服務器端相互通訊的數據被封裝成 HttpObject
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//channelRead0 讀取客戶端數據,當有讀取事件發生的時候就會調用此方法
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//每個瀏覽器都有單獨的 pipeline 和 handler 進行處理,每次刷新瀏覽器又會產生新的 pipeline 和 handler,因爲http請求是一次性的
System.out.println("對應的channel=" + ctx.channel() + " pipeline=" + ctx.pipeline() + " 通過pipeline獲取channel" + ctx.pipeline().channel());
System.out.println("當前ctx的handler=" + ctx.handler());//TestHttpServerHandler
//判斷 msg 是不是 httpRequest請求
if(msg instanceof HttpRequest) {
System.out.println("ctx 類型="+ctx.getClass());//DefaultChannelHandlerContext
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());
System.out.println("msg 類型=" + msg.getClass());//DefaultHttpRequest
System.out.println("客戶端地址" + ctx.channel().remoteAddress());
//獲取到
HttpRequest httpRequest = (HttpRequest) msg;
//獲取uri, 過濾指定的資源
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())) {
System.out.println("請求了 favicon.ico, 不做響應");
return;
}
//回覆信息給瀏覽器 [http協議] 回覆需要符合http協議
ByteBuf content = Unpooled.copiedBuffer("hello, 我是服務器", CharsetUtil.UTF_8);
//構造一個http的相應,即 httpResponse
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//將構建好 response返回
ctx.writeAndFlush(response);
}
}
}