Netty架構設計
Netty簡介
- 一個異步的、基於事件驅動的網絡應用框架,用以快速開發高性能、高可靠性的網路IO程序;
- 主要針對TCP協議下,面向clients端的高併發應用,或者大量數據傳輸的應用;
- 本質是一個NIO框架,適用於服務器通信相關的多種應用場景;
線程模型
名詞解釋 :
黃色的框表示對象, 藍色的框表示線程,白色的框表示方法(API);
傳統阻塞IO模型
特點:
- 採用阻塞IO模式獲取輸入的數據;
- 每個連接都需要獨立的線程完成數據的輸入、業務處理、數據返回等工作;
優缺點分析:
- 當併發數很大,就會創建大量的線程,佔用很大系統資源;
- 連接創建後,如果當前線程暫時沒有數據可讀,該線程會阻塞在read,造成線程資源浪費;
Reactor模型
特點:
- 基於 I/O 複用模型:多個連接(Client)共用一個阻塞對象(ServiceHandler),應用程序只需要在一個阻塞對象等待,無需阻塞等待的所有連接。
- 當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理;
- 服務器端程序處理傳入的多個請求,並將它們同步分派到相應的處理線程, 基於線程池複用線程原則,不必再爲每個連接創建線程,將連接完成後的業務處理任務分配給線程進行處理,一個線程就可以複用處理多個連接的業務,因此Reactor模式也叫 Dispatcher模式;
- Reactor 模式使用IO複用監聽事件, 收到事件後,分發給某個線程(進程), 這點就是網絡服務器高併發處理關鍵;
- 響應快,不必爲單個同步時間所阻塞,雖然 Reactor 本身依然是同步的,多個SubReactor有效的避免了阻塞;
- 可以最大程度的避免複雜的多線程及同步問題,並且避免了多線程/進程的切換開銷;
- 擴展性好,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源;
- 複用性好,Reactor 模型本身與具體事件處理邏輯無關,具有很高的複用性;
單Reactor單線程模型
- 上圖Select可以實現應用程序通過一個阻塞對象監聽多路連接請求;
- Reactor對象通過Select監控客戶端請求事件,收到事件後通過Dispatch 進行分發;
- 如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然後創建一個 Handler 對象處理連接完成後的後續業務處理;
- 如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應;
- Handler 會完成Read→業務處理→Send的完整業務流程
優缺點:
- 模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成;
- 性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸;
- 可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障;
- 使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis在業務處理的時間複雜度 O(1) 的情況;
單Reactor多線程
- Reactor 對象通過select 監控客戶端請求事件, 收到事件後,通過dispatch進行分發;
- 如果建立連接請求, 則右Acceptor 通過accept 處理連接請求, 然後創建一個Handler對象處理完成連接後的各種事件;
- 如果不是連接請求,則由reactor分發調用連接對應的handler 來處理;
- handler 只負責響應事件,不做具體的業務處理,通過read 讀取數據後,會分發給後面的worker線程池的某個線程處理業務;
- worker 線程池會分配獨立線程完成真正的業務,並將結果返回handler;
- handler收到響應後,通過send將結果返回給client;
優缺點:
- 可以充分的利用多核cpu 的處理能力;
- 多線程數據共享和訪問比較複雜, reactor 處理所有的事件的監聽和響應,在單線程運行, 在高併發場景容易出現性能瓶頸;
主從Reactor多線程
- Reactor主線程 MainReactor 對象通過select 監聽連接事件, 收到事件後,通過Acceptor 處理連接事件,Reactor 主線程可以對應多個Reactor 子線程, 即MainRecator 可以關聯多個SubReactor;
- 當 Acceptor 處理連接事件後,MainReactor 將連接分配給SubReactor ;
- Subreactor 將連接加入到連接隊列進行監聽,並創建handler進行各種事件處理;
- 當有新事件發生時, subreactor 就會調用對應的handler處理;
- handler 通過read 讀取數據,分發給後面的worker 線程處理;
- worker 線程池分配獨立的worker 線程進行業務處理,並返回結果;
- handler 收到響應的結果後,再通過send 將結果返回給client;
優缺點:
- 父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成後續的業務處理;
- 父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據;
- 編程複雜度較高;
Netty模型
- Netty抽象出兩組線程池 BossGroup 專門負責接收客戶端的連接, WorkerGroup 專門負責網絡的讀寫;
- BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup;
- NioEventLoopGroup 相當於一個事件循環組, 這個組中含有多個事件循環 ,每一個事件循環是 NioEventLoop;
- NioEventLoop 表示一個不斷循環的執行處理任務的線程, 每個NioEventLoop 都有一個selector , 用於監聽綁定在其上的socket的網絡通訊;
- NioEventLoopGroup 可以有多個線程, 即可以含有多個NioEventLoop;
- 每個Boss下的NioEventLoop 循環執行的步驟有3步:輪詢accept 事件、處理accept 事件,與client建立連接,生成NioScocketChannel,並將其註冊到某個worker中NIOEventLoop上的selector中、處理任務隊列的任務 , 即 runAllTasks;
- 每個 Worker下的NIOEventLoop 循環執行的步驟:輪詢read,write 事件、處理i/o事件,即read , write 事件,在對應NioScocketChannel 處理、處理任務隊列的任務 , 即 runAllTasks;
- 每個Worker下的NIOEventLoop 處理業務時,會使用pipeline(管道), pipeline 中包含了 channel , 即通過pipeline 可以獲取到對應通道, 管道中維護了很多的 處理器;
- Worker下的NIOEventLoop 處理業務採用輪詢機制;
Netty-Demo
服務器端:
package com.neei.netty.simple;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @param
* @Author: AaNeei
* @Date: 2019/12/4 22:24
* @Description: 遊學網
* @throws:
*/
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//創建BossGroup線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();
//創建WorkerGroup線程組
EventLoopGroup workerGroup = new NioEventLoopGroup();
//創建服務器端啓動對象
ServerBootstrap bootstrap = new ServerBootstrap();
try {
//設置參數
bootstrap.group(bossGroup, workerGroup)//綁定線程組
.channel(NioServerSocketChannel.class)//使用NioServerSocketChannel作爲服務器的通道
.option(ChannelOption.SO_BACKLOG, 128)//設置線程隊列連接數
.childOption(ChannelOption.SO_KEEPALIVE, true)//設置保持活動連接狀態
.childHandler(new ChannelInitializer<SocketChannel>() {
//爲pipeline設置處理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//註冊處理器handler
socketChannel.pipeline().addLast(new NettyServerHandler());
}
});//設置WorkerGroup的 EventLoop 對應的管道設置處理器
//服務準備完畢
System.out.println("------------- NettyServer is ready --------------");
//綁定端口並同步,啓動服務
ChannelFuture channelFuture = bootstrap.bind(6666).sync();
//監聽關閉的通道
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服務器Handler:
package com.neei.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @param
* @Author: AaNeei
* @Date: 2019/12/5 22:13
* @Description: 遊學網
* @throws:
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 讀取數據方法
* 當通道有讀取事件時觸發該方法
* @param ctx 上下文對象 包含:pipeline、channel等
* @param msg 數據內容
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//如果存在耗時操作,需要將其放到task隊列中去異步執行
//1.自定義線程
ctx.channel().eventLoop().execute(() -> {
try {
Thread.sleep(10000L);
ctx.writeAndFlush(Unpooled.copiedBuffer("這是耗時操作", CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//2.定時執行
ctx.channel().eventLoop().schedule(() -> {
ctx.writeAndFlush(Unpooled.copiedBuffer("這是定時執行操作", CharsetUtil.UTF_8));
}, 5, TimeUnit.SECONDS);
System.out.println("server ctx=" + ctx);
ByteBuf buf = (ByteBuf) msg;
System.out.println("客戶端發送的消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客戶端地址:" + ctx.channel().localAddress());
}
/**
* 讀取數據完成之後 觸發該方法
* 可以執行的操作
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,NettyClient", CharsetUtil.UTF_8));
}
/**
* 異常觸發該方法
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
客戶端
package com.neei.netty.simple;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @param
* @Author: AaNeei
* @Date: 2019/12/5 22:29
* @Description: 遊學網
* @throws:
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("----------client is ok---------");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客戶端handler:
package com.neei.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @param
* @Author: AaNeei
* @Date: 2019/12/5 22:37
* @Description: 遊學網
* @throws:
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就緒觸發該方法
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Client ctx=" + ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,NettyServer", CharsetUtil.UTF_8));
}
/**
* 通道有讀事件時觸發該方法
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("服務器回覆消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服務器地址:" + ctx.channel().localAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
cause.printStackTrace();
}
}
注:學習資源連接穀粒學院