Netty核心組件介紹
在瞭解新知識之前,先來對前面的知識回顧下:
- Netty抽象出兩組線程池,BossGroup專門負責接收客戶端連接,WorkerGroup專門負責網絡讀寫
- NioEventLoop表示一個不斷循環執行處理任務的線程,每個NioEventLoop都有一個selector,用於監聽綁定在其上的socket網絡通道
- NioEventLoop內部採用串行化設計,從消息的讀取->解碼->編碼->發送,始終由IO線程NioEventLoop負責
- NioEventLoopGroup下包含多個NioEventLoop
- 每個NioEventLoop中包含一個Selector,一個taskQueue
- 每個NioEventLoop的Selector上可以註冊監聽多個NioChannel
- 每個NioChannel只會綁定在唯一的NioEventLoop上
- 每個NioChannel都綁定有一個自己的ChannelPipeline
任務隊列中Task的使用
關於示例代碼以NettyServerHandler
舉例
1. 用戶程序自定義的普通任務
public class NettyServerCustomTaskHandler extends NettyServerHandler {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//比如有個很耗時的業務->異步執行->提交到該channel對應的NioEventLoop下的taskQueue中
ctx.channel().eventLoop().execute(() -> {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,這是一個耗時操作", CharsetUtil.UTF_8));
System.out.println("channel code=" + ctx.channel().hashCode());
} catch (Exception ex) {
System.out.println("發生異常" + ex.getMessage());
}
});
}
}
2.用戶自定義定時任務
public class NettyServerCustomTaskHandler extends NettyServerHandler {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//用戶自定義定時任務 ->該任務是提交到 scheduledTaskQueue 中
ctx.channel().eventLoop().schedule(() -> {
try {
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,這是一個耗時操作", 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會提交到任務隊列中後被異步消費
異步模型
基本介紹
- 異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的組件在完成後,通過狀態通知和回調來通知調用者。
- Netty中的IO操作時異步的。包括Bind/Write/Connect等操作會簡單的返回一個ChannelFuture。
- 調用者並不能立刻獲得結果,而是通過Future-Listener機制,用戶可以方便主動地獲取或者通過通知機制獲取IO操作結果
- Netty的異步模型是建立在future和callback(回調)之上的。Future的核心思想是:假設一個方法fun,計算過程可能非常耗時,等待fun返回顯然不太合適。那可以在調用fun的時候立馬返回一個future,後續可以通過Future去監控方法fun的處理過程(Future-Listener機制)
Future說明
- 表示異步的執行結果,可以通過它提供的方法來檢測執行是否完成,比如檢索計算等
- ChannelFuture是一個接口,我們可以添加監聽器,當監聽的事件發生時就會通過監聽器
Future-Listener機制
- 當Future對象剛剛創建時,處於非完成狀態,調用者可以通過返回的ChannelFuture來獲取操作執行的狀態,註冊監聽函數來執行完成後的操作
- 常見有如下操作
- 通過
isDone
方法來判斷當前操作是否完成 - 通過
isSuccess
方法來判斷已完成的當前操作是成功 - 通過
getCause
方法來獲取已完成的當前操作失敗的原因 - 通過
isCancelled
方法來判斷已完成的當前操作是否被取消 - 通過
addListener
方法來註冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器,如果Future對象已完成,則通知指定的監聽器
- 通過
示例
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.option(ChannelOption.SO_BACKLOG, 128)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerCustomTaskHandler());
}
});
ChannelFuture channelFuture = bootstrap.bind(6668).sync();
//給 channelFuture 註冊監聽器,監控我們關心的事件
channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if(channelFuture.isDone()){
System.out.println("已完成");
}
if(channelFuture.isSuccess()){
System.out.println("已成功");
}
}
});
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
示例:基於Netty實現一個簡單的HTTP服務
- Netty服務器在6668端口監聽,瀏覽器發出請求"http://localhost:16668/"
- 服務器回覆消息給瀏覽器或客戶端消息,並對指定請求資源進行過濾
//服務器端
public class NettyBrowserServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("serverCodec", new HttpServerCodec());
ch.pipeline().addLast(new NettyBrowserHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(16668).sync();
channelFuture.addListener(future -> {
if (channelFuture.isSuccess()) {
System.out.println("運行成功");
}
});
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
//handler
/**
* Created by myk
* 2020/1/21 下午2:28
* 1、SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子類
* 2、客戶端和服務器端通訊的數據放在HttpObject中
*/
public class NettyBrowserHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判斷是否爲Http請求
if (msg instanceof HttpRequest) {
System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + ",NettyBrowserHandler.hash=" + this.hashCode());
System.out.println("消息類型" + msg.getClass());
System.out.println("客戶端地址:" + ctx.channel().remoteAddress());
HttpRequest httpRequest = (HttpRequest) msg;
//瀏覽器請求時會請求圖標favicon.ico 這裏沒有這個圖標因此將這個給過濾掉
if (Objects.equals(httpRequest.uri(), "/favicon.ico")) {
return;
}
//回覆信息給瀏覽器
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,這是瀏覽器", CharsetUtil.UTF_16);
//給瀏覽器回覆信息 要符合http協議規範'
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
//設置content-type=text-plain
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
//設置content-length
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
//將構建好 response 返回
ctx.writeAndFlush(response);
}
}
}
瀏覽器的輸出結果:
Bootstrap、ServerBootstrap介紹
-
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 <T> B option(ChannelOption<T> option,T value);//用來給ServerChannel添加配置 public <T> ServerBootstrap childOption(ChannelOption<T> 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和ChannelFutures,它們可以註冊一個監聽,當操作執行成功或失敗時監聽會自動觸發註冊的監聽事件
常見的方法有:
Channel channel();//返回當前正在執行IO操作的通道
ChannelFuture sync();//等待異步操作執行完畢
Channel介紹
- Netty網絡通信的組件,能夠用於執行網絡IO操作
- 通過Channel可獲得當前網絡連接的通道的狀態
- 通過Channel可獲得網絡連接的配置參數(例如接收緩衝區的大小)
- Channe提供異步的網絡IO操作(如建立連接、讀寫、綁定端口),異步調用意味着任何IO調用都將立即返回,並且不保證在調用結束時所請求的IO操作已完成
- 調用立即返回一個ChannelFuture實例,通過註冊監聽器到ChannelFuture上,可以IO操作成功、失敗或取消時回調通知對方
- 支持關聯IO操作與對應的處理程序
- 不同協議、不同阻塞類型的鏈接都有不同的Channel類型與之對應,常用的Channel類型:
- NioSocketchannel 異步客戶端TCP Scoket連接
- NioServerSocketChannel,異步的服務器端TCP Socket連接
- NioSctpChannel,異步的UDP連接
- NioDatagramChannel,異步的UDP連接
- NioSctpServerChannel,異步的Sctp服務器端連接,這些通道涵蓋了UDP和TCP網絡IO以及文件IO
Selector
- Netty基於Selector對象實現IO多路複用,通過Selector一個線程可以監聽多個連接的Channel事件。
- 當向一個Selector中註冊Channel後,Selector內部的機制就可以自動不斷地查詢(Select)這些註冊的Channel是否有已就緒的IO事件(例如可讀、可寫、網絡連接完成等),這些程序就可以很簡單地使用一個線程高效地管理多個Channel
ChannelHandler及其實現類
- ChannelHandler是一個接口,處理IO事件或攔截IO操作,並將其轉發到其ChannelPipeline(業務處理鏈)中的下一個處理程序
- ChannelHandler本身並沒有提供很多方法,因爲這個接口有很多的方法需要實現,使用期間可以繼承它的子類
-
ChannelInboundHandler用於處理入站IO事件
-
ChannelOutboundHandler用於處理出站IO操作
//適配器
-
ChannelInboundHandlerAdapter用戶處理入站IO事件
-
ChannelOutboundHandlerAdapter用於處理出站IO操作
-
ChannelDuplexHandler用於處理入站和出站事件
對常用方法的介紹(部分未列出):
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
//通道就緒事件
void channelActive(ChannelHandlerContext ctx) throws Exception;
//通道不活躍事件
void channelInactive(ChannelHandlerContext ctx) throws Exception;
//通道讀取數據事件
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
//通道讀取完畢事件
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
//異常捕獲事件
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
執行順序:handlerAdded->channelRegistered->channelActive->channelReadComplete->channelInactive->channelUnregistered->handlerRemoved
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介紹
Netty在創建Channel實例後,一般都需要設置ChannelOption參數:
ChannelOption.SO_BACKLOG
對應TCP/IP協議listen函數中的backlog參數,用來初始化服務器可連接隊列大小。服務器端處理客戶端連接請求是順序處理的,所以同一時間只能處理一個客戶端連接。多個客戶端的時候,服務端將不能處理的客戶端連接請求放在隊列中等待處理,backlog參數指定了隊列的大小
ChannelOption.SO_KEEPALIVE
一直保持連接活動狀態
EventLoopGroup和實現類NioEventLoopGroup的介紹
-
EventLoopGroup是一組EventLoop的抽象,Netty爲了更好的利用多核CPU資源,一般會有多個EventLoop同時工作,每個EventLoop維護者一個Selector實例
-
EventLoopGroup提供next接口,可以從組裏面按照一定規則獲取其中一個EventLoop來處理任務。在服務端編程中,我們一般需要提供兩個EventLoopGroup,BossEventLoopGroup和WorkerEventLoopGroup
-
通常一個服務端口即一個ServerSocketChannel對應一個Selector和一個EventLoop線程。BossEventLoop負責接收客戶端的連接並將SocketChannel交給WorkerEventLoopGroup來進行IO處理,
說明:
BossEventLoopGroup通常是一個單線程的EventLoop,EventLoop維護着一個註冊了ServerSocketChannel的Selector實例,BossEventLoop不斷輪詢Selector將連接事件分離出來。然後將接收到的SocketChannel交給WorkerEventLoopGroup。WorkerEventLoopGroup會由next選擇其中一個EventLoop來將SocketChannel註冊到其維護的Selector,並對其後續的IO事件進行處理。
常用方法:
public NioEventLoopGroup();//構造方法
public Future<?> shutdownGracefully();//斷開連接,關閉線程