相比於NIO
- NIO學習成本高,代碼複雜
- NIO存在Bug
Netty架構設計、線程模型
傳統的阻塞IO
一個請求一個線程。
缺點
- 併發數高時,就會創建大量的線程,佔用系統資源
- 當沒有數據可讀時,線程會阻塞read操作,浪費資源
Reactor模式
單Reactor單線程
Handler中處理了業務邏輯。
- Reactor通過select監聽客戶端請求事件,通過Dispatch分發
- 如果不是建立連接,則會分發調用Handler處理
- 一個阻塞對象監聽多路連接請求
優點:模型簡單,全部在一個線程中完成
缺點:只有一個線程,無法發揮多核cpu的性能。如果線程意外終止,會導致整個通信系統不可用。
單Reactor多線程
將業務處理從Handler剝離出來,分發給某個線程池
說明:
- Reactor接收請求,如果是連接請求,則創建連接,並創建Handler對象用來響應事件
- 如果不是連接請求,則由Reactor分發調用對應的Handler處理。
- Handler只負責響應事件,不負責業務邏輯。將業務邏輯分發給某個線程池處理
優點:充分利用多核CPU的性能
缺點:多線程數據共享和訪問比較複雜。Reactor處理所有的監聽和響應,仍是單線程,高併發的場景下仍然有瓶頸。
主從Reactor多線程
說明:
- Reactor分爲主Reactor和從Reactor,MainReactor可以關聯多個SubReacor
- 主線程Reactor只負責連接請求,創建請求連接完成後,將連接分配給SubReactor
- 從Reactor負責監聽事件和調用對應的handler響應事件
- handler完成read後,分發給線程池進行業務處理,線程池分配的線程完成處理後,將結果趕回給handler
- handler將結果send給client
優點:主從Reactor分工明確
缺點:編程複雜度高
Netty模型
- BossGroup只負責監聽連接事件,有連接請求時,建立連接,並將返回的NIOSocketChannel註冊到WorkGroup下的某個Selector上
- WorkGroup只負責監聽IO事件,當有讀寫請求時,處理IO事件。
- 每個Work NIOEventLoop處理業務時,會用到PipeLine(管道),PipeLine包含Channel,管道中維護了很多處理器。
代碼示例
NettyServer
/**
* BossGroup和WorkGroup都是無限循環的
* 含有的子線程數量(NioEventLoop)
* 默認是cpu核心數量的兩倍
*/
// 創建BossGroup , 只監聽連接事件
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 創建WorkGroup , 只監聽IO事件
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
// 鏈式編程設置參數
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup) //設置兩個線程組
.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 {
// 添加處理器
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}); //給WorkGroup的EventLoop設置處理器
System.out.println("服務器準備好了...");
// 綁定一個端口並同步,生成一個ChannelFuture對象
// 啓動服務器(並綁定端口)
ChannelFuture cf = bootstrap.bind(6666).sync();
// 對 “關閉通道” 事件監聽
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
NettyServerHandler
/**
* 自定義的handler,需要繼承netty規定好的某個HandlerAdapter
**/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 讀取數據
* 1. ctx 上下文對象,含有管道、通道、地址
* 2. msg 消息
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("----- ctx : " + ctx);
System.out.println("客戶端地址 : " + ctx.channel().remoteAddress());
ByteBuf buf = (ByteBuf) msg;
System.out.println("msg : " + buf.toString(CharsetUtil.UTF_8));
}
/**
* 讀取數據完成
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 寫數據並flush
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端", CharsetUtil.UTF_8));
}
/**
* 處理異常
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
NettyClient
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
// Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 加入自己的處理器
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客戶端準備好了....");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
// 監聽關閉事件
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
NettyClientHandler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("喵喵喵", CharsetUtil.UTF_8));
}
@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().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
說明
- BossGroup只負責監聽連接事件,WorkGroup只監聽IO事件。兩者都包含多個NIOEventLoop,無限循環執行
- BossGroup和WorkGroup默認含有的子線程(NIOEventLoop)數量爲cpu核心的兩倍
- NIOEventLoop有一個Selector,用於監聽綁定在其上的socket通道。NIOEventLoop包含一個taskQueue
- 一個NIOEventLoop可以監聽多個NIOChannel
- 處理器需要重寫,根據需要重寫方法
- 上下文對象ChannelHandlerContext包含了pipeline,從pipeline中可以獲取NIOChannel
- pipeline本質是一個雙向鏈表
- NIOChannel中也包含pipeline,兩者相互對應
異步執行
在Handler中,儘量不要執行耗時長的業務,避免阻塞,考慮使用異步執行耗時長的任務。
使用普通任務,提交到TaskQueue:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果要執行一個耗時長的業務,這裏可以考慮異步執行
// 1. 使用channel中eventLoop異步執行,任務提交到TaskQueue中
ctx.channel().eventLoop().execute(()->{
try {
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hahaha",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
使用定時任務,提交到scheduleTaskQueue
// 使用定時任務,任務提交到scheduleTaskQueue
ctx.channel().eventLoop().schedule(()->{
try {
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hahaha2",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
},5,TimeUnit.SECONDS);
非當前Reactor線程調用Channel中的方法
如推送系統中,根據用戶標識,找到對應的Channel應用,然後調用Write方法推送信息。最終Write會提交到任務隊列中被異步消費。
可以在ChannelInitializer中的initChannel方法中,將Channel的hashcode與用戶綁定,加入到集合中,需要推送消息時,從集合中獲取Channel,加入到相應的NIOEventLoop的TaskQueue或schdueleTaskQueue推送消息