思維導圖
前言
本文主要講述Netty框架的一些特性以及重要組件,希望看完之後能對Netty框架有一個比較直觀的感受,希望能幫助讀者快速入門Netty,減少一些彎路。
一、Netty概述
官方的介紹:
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。
二、爲什麼使用Netty
從官網上介紹,Netty是一個網絡應用程序框架,開發服務器和客戶端。也就是用於網絡編程的一個框架。既然是網絡編程,Socket就不談了,爲什麼不用NIO呢?
2.1 NIO的缺點
對於這個問題,之前我寫了一篇文章《NIO入門》對NIO有比較詳細的介紹,NIO的主要問題是:
- NIO的類庫和API繁雜,學習成本高,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
- 需要熟悉Java多線程編程。這是因爲NIO編程涉及到Reactor模式,你必須對多線程和網絡編程非常熟悉,才能寫出高質量的NIO程序。
- 臭名昭著的epoll bug。它會導致Selector空輪詢,最終導致CPU 100%。直到JDK1.7版本依然沒得到根本性的解決。
2.2 Netty的優點
相對地,Netty的優點有很多:
- API使用簡單,學習成本低。
- 功能強大,內置了多種解碼編碼器,支持多種協議。
- 性能高,對比其他主流的NIO框架,Netty的性能最優。
- 社區活躍,發現BUG會及時修復,迭代版本週期短,不斷加入新的功能。
- Dubbo、Elasticsearch都採用了Netty,質量得到驗證。
三、架構圖
上面這張圖就是在官網首頁的架構圖,我們從上到下分析一下。
綠色的部分Core核心模塊,包括零拷貝、API庫、可擴展的事件模型。
橙色部分Protocol Support協議支持,包括Http協議、webSocket、SSL(安全套接字協議)、谷歌Protobuf協議、zlib/gzip壓縮與解壓縮、Large File Transfer大文件傳輸等等。
紅色的部分Transport Services傳輸服務,包括Socket、Datagram、Http Tunnel等等。
以上可看出Netty的功能、協議、傳輸方式都比較全,比較強大。
四、永遠的Hello Word
首先搭建一個HelloWord工程,先熟悉一下API,還有爲後面的學習做鋪墊。以下面這張圖爲依據:
4.1 引入Maven依賴
使用的版本是4.1.20,相對比較穩定的一個版本。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.20.Final</version>
</dependency>
4.2 創建服務端啓動類
public class MyServer {
public static void main(String[] args) throws Exception {
//創建兩個線程組 boosGroup、workerGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//創建服務端的啓動對象,設置參數
ServerBootstrap bootstrap = new ServerBootstrap();
//設置兩個線程組boosGroup和workerGroup
bootstrap.group(bossGroup, workerGroup)
//設置服務端通道實現類型
.channel(NioServerSocketChannel.class)
//設置線程隊列得到連接個數
.option(ChannelOption.SO_BACKLOG, 128)
//設置保持活動連接狀態
.childOption(ChannelOption.SO_KEEPALIVE, true)
//使用匿名內部類的形式初始化通道對象
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//給pipeline管道設置處理器
socketChannel.pipeline().addLast(new MyServerHandler());
}
});//給workerGroup的EventLoop對應的管道設置處理器
System.out.println("java技術愛好者的服務端已經準備就緒...");
//綁定端口號,啓動服務端
ChannelFuture channelFuture = bootstrap.bind(6666).sync();
//對關閉通道進行監聽
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
4.3 創建服務端處理器
/**
* 自定義的Handler需要繼承Netty規定好的HandlerAdapter
* 才能被Netty框架所關聯,有點類似SpringMVC的適配器模式
**/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//獲取客戶端發送過來的消息
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到客戶端" + ctx.channel().remoteAddress() + "發送的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//發送消息給客戶端
ctx.writeAndFlush(Unpooled.copiedBuffer("服務端已收到消息,並給你發送一個問號?", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//發生異常,關閉通道
ctx.close();
}
}
4.4 創建客戶端啓動類
public class MyClient {
public static void main(String[] args) throws Exception {
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
//創建bootstrap對象,配置參數
Bootstrap bootstrap = new Bootstrap();
//設置線程組
bootstrap.group(eventExecutors)
//設置客戶端的通道實現類型
.channel(NioSocketChannel.class)
//使用匿名內部類初始化通道
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//添加客戶端通道的處理器
ch.pipeline().addLast(new MyClientHandler());
}
});
System.out.println("客戶端準備就緒,隨時可以起飛~");
//連接服務端
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
//對通道關閉進行監聽
channelFuture.channel().closeFuture().sync();
} finally {
//關閉線程組
eventExecutors.shutdownGracefully();
}
}
}
4.5 創建客戶端處理器
public class MyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//發送消息到服務端
ctx.writeAndFlush(Unpooled.copiedBuffer("歪比巴卜~茉莉~Are you good~馬來西亞~", CharsetUtil.UTF_8));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//接收服務端發送過來的消息
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到服務端" + ctx.channel().remoteAddress() + "的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
}
}
4.6 測試
先啓動服務端,再啓動客戶端,就可以看到結果:
MyServer打印結果:
MyClient打印結果:
五、Netty的特性與重要組件
5.1 taskQueue任務隊列
如果Handler處理器有一些長時間的業務處理,可以交給taskQueue異步處理。怎麼用呢,請看代碼演示:
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//獲取到線程池eventLoop,添加線程,執行
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
//長時間操作,不至於長時間的業務操作導致Handler阻塞
Thread.sleep(1000);
System.out.println("長時間的業務處理");
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
我們打一個debug調試,是可以看到添加進去的taskQueue有一個任務。
5.2 scheduleTaskQueue延時任務隊列
延時任務隊列和上面介紹的任務隊列非常相似,只是多了一個可延遲一定時間再執行的設置,請看代碼演示:
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
//長時間操作,不至於長時間的業務操作導致Handler阻塞
Thread.sleep(1000);
System.out.println("長時間的業務處理");
} catch (Exception e) {
e.printStackTrace();
}
}
},5, TimeUnit.SECONDS);//5秒後執行
依然打開debug進行調試查看,我們可以有一個scheduleTaskQueue任務待執行中
5.3 Future異步機制
在搭建HelloWord工程的時候,我們看到有一行這樣的代碼:
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666);
很多操作都返回這個ChannelFuture對象,究竟這個ChannelFuture對象是用來做什麼的呢?
ChannelFuture提供操作完成時一種異步通知的方式。一般在Socket編程中,等待響應結果都是同步阻塞的,而Netty則不會造成阻塞,因爲ChannelFuture是採取類似觀察者模式的形式進行獲取結果。請看一段代碼演示:
//添加監聽器
channelFuture.addListener(new ChannelFutureListener() {
//使用匿名內部類,ChannelFutureListener接口
//重寫operationComplete方法
@Override
public void operationComplete(ChannelFuture future) throws Exception {
//判斷是否操作成功
if (future.isSuccess()) {
System.out.println("連接成功");
} else {
System.out.println("連接失敗");
}
}
});
5.4 Bootstrap與ServerBootStrap
Bootstrap和ServerBootStrap是Netty提供的一個創建客戶端和服務端啓動器的工廠類,使用這個工廠類非常便利地創建啓動類,根據上面的一些例子,其實也看得出來能大大地減少了開發的難度。首先看一個類圖:
可以看出都是繼承於AbstractBootStrap抽象類,所以大致上的配置方法都相同。
一般來說,使用Bootstrap創建啓動器的步驟可分爲以下幾步:
5.4.1 group()
在上一篇文章《Reactor模式》中,我們就講過服務端要使用兩個線程組:
- bossGroup 用於監聽客戶端連接,專門負責與客戶端創建連接,並把連接註冊到workerGroup的Selector中。
- workerGroup用於處理每一個連接發生的讀寫事件。
一般創建線程組直接使用以下new就完事了:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
有點好奇的是,既然是線程組,那線程數默認是多少呢?深入源碼:
//使用一個常量保存
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
//NettyRuntime.availableProcessors() * 2,cpu核數的兩倍賦值給常量
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
//如果不傳入,則使用常量的值,也就是cpu核數的兩倍
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
通過源碼可以看到,默認的線程數是cpu核數的兩倍。假設想自定義線程數,可以使用有參構造器:
//設置bossGroup線程數爲1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//設置workerGroup線程數爲16
EventLoopGroup workerGroup = new NioEventLoopGroup(16);
5.4.2 channel()
這個方法用於設置通道類型,當建立連接後,會根據這個設置創建對應的Channel實例。
使用debug模式可以看到
通道類型有以下:
NioSocketChannel: 異步非阻塞的客戶端 TCP Socket 連接。
NioServerSocketChannel: 異步非阻塞的服務器端 TCP Socket 連接。
常用的就是這兩個通道類型,因爲是異步非阻塞的。所以是首選。
OioSocketChannel: 同步阻塞的客戶端 TCP Socket 連接。
OioServerSocketChannel: 同步阻塞的服務器端 TCP Socket 連接。
稍微在本地調試過,用起來和Nio有一些不同,是阻塞的,所以API調用也不一樣。因爲是阻塞的IO,幾乎沒什麼人會選擇使用Oio,所以也很難找到例子。我稍微琢磨了一下,經過幾次報錯之後,總算調通了。代碼如下:
//server端代碼,跟上面幾乎一樣,只需改三個地方
//這個地方使用的是OioEventLoopGroup
EventLoopGroup bossGroup = new OioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup)//只需要設置一個線程組boosGroup
.channel(OioServerSocketChannel.class)//設置服務端通道實現類型
//client端代碼,只需改兩個地方
//使用的是OioEventLoopGroup
EventLoopGroup eventExecutors = new OioEventLoopGroup();
//通道類型設置爲OioSocketChannel
bootstrap.group(eventExecutors)//設置線程組
.channel(OioSocketChannel.class)//設置客戶端的通道實現類型
NioSctpChannel: 異步的客戶端 Sctp(Stream Control Transmission Protocol,流控制傳輸協議)連接。
NioSctpServerChannel: 異步的 Sctp 服務器端連接。
本地沒啓動成功,網上看了一些網友的評論,說是隻能在linux環境下才可以啓動。從報錯信息看:SCTP not supported on this platform,不支持這個平臺。因爲我電腦是window系統,所以網友說的有點道理。
5.4.3 option()與childOption()
首先說一下這兩個的區別。
option()設置的是服務端用於接收進來的連接,也就是boosGroup線程。
childOption()是提供給父管道接收到的連接,也就是workerGroup線程。
搞清楚了之後,我們看一下常用的一些設置有哪些:
SocketChannel參數,也就是childOption()常用的參數:
SO_RCVBUF Socket參數,TCP數據接收緩衝區大小。
TCP_NODELAY TCP參數,立即發送數據,默認值爲Ture。
SO_KEEPALIVE Socket參數,連接保活,默認值爲False。啓用該功能時,TCP會主動探測空閒連接的有效性。
ServerSocketChannel參數,也就是option()常用參數:
SO_BACKLOG Socket參數,服務端接受連接的隊列長度,如果隊列已滿,客戶端連接將被拒絕。默認值,Windows爲200,其他爲128。
由於篇幅限制,其他就不列舉了,大家可以去網上找資料看看,瞭解一下。
5.4.4 設置流水線(重點)
ChannelPipeline是Netty處理請求的責任鏈,ChannelHandler則是具體處理請求的處理器。實際上每一個channel都有一個處理器的流水線。
在Bootstrap中childHandler()方法需要初始化通道,實例化一個ChannelInitializer,這時候需要重寫initChannel()初始化通道的方法,裝配流水線就是在這個地方進行。代碼演示如下:
//使用匿名內部類的形式初始化通道對象
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//給pipeline管道設置自定義的處理器
socketChannel.pipeline().addLast(new MyServerHandler());
}
});
處理器Handler主要分爲兩種:
ChannelInboundHandlerAdapter(入站處理器)、ChannelOutboundHandler(出站處理器)
入站指的是數據從底層java NIO Channel到Netty的Channel。
出站指的是通過Netty的Channel來操作底層的java NIO Channel。
ChannelInboundHandlerAdapter處理器常用的事件有:
- 註冊事件 fireChannelRegistered。
- 連接建立事件 fireChannelActive。
- 讀事件和讀完成事件 fireChannelRead、fireChannelReadComplete。
- 異常通知事件 fireExceptionCaught。
- 用戶自定義事件 fireUserEventTriggered。
- Channel 可寫狀態變化事件 fireChannelWritabilityChanged。
- 連接關閉事件 fireChannelInactive。
ChannelOutboundHandler處理器常用的事件有:
- 端口綁定 bind。
- 連接服務端 connect。
- 寫事件 write。
- 刷新時間 flush。
- 讀事件 read。
- 主動斷開連接 disconnect。
- 關閉 channel 事件 close。
還有一個類似的handler(),主要用於裝配parent通道,也就是bossGroup線程。一般情況下,都用不上這個方法。
5.4.5 bind()
提供用於服務端或者客戶端綁定服務器地址和端口號,默認是異步啓動。如果加上sync()方法則是同步。
有五個同名的重載方法,作用都是用於綁定地址端口號。不一一介紹了。
5.4.6 優雅地關閉EventLoopGroup
//釋放掉所有的資源,包括創建的線程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
會關閉所有的child Channel。關閉之後,釋放掉底層的資源。
5.5 Channel
Channel是什麼?不妨看一下官方文檔的說明:
A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind
翻譯大意:一種連接到網絡套接字或能進行讀、寫、連接和綁定等I/O操作的組件。
如果上面這段說明比較抽象,下面還有一段說明:
A channel provides a user:
the current state of the channel (e.g. is it open? is it connected?),
the configuration parameters of the channel (e.g. receive buffer size),
the I/O operations that the channel supports (e.g. read, write, connect, and bind), and
the ChannelPipeline which handles all I/O events and requests associated with the channel.
翻譯大意:
channel爲用戶提供:
- 通道當前的狀態(例如它是打開?還是已連接?)
- channel的配置參數(例如接收緩衝區的大小)
- channel支持的IO操作(例如讀、寫、連接和綁定),以及處理與channel相關聯的所有IO事件和請求的ChannelPipeline。
5.5.1 獲取channel的狀態
boolean isOpen(); //如果通道打開,則返回true
boolean isRegistered();//如果通道註冊到EventLoop,則返回true
boolean isActive();//如果通道處於活動狀態並且已連接,則返回true
boolean isWritable();//當且僅當I/O線程將立即執行請求的寫入操作時,返回true。
以上就是獲取channel的四種狀態的方法。
5.5.2 獲取channel的配置參數
獲取單條配置信息,使用getOption(),代碼演示:
ChannelConfig config = channel.config();//獲取配置參數
//獲取ChannelOption.SO_BACKLOG參數,
Integer soBackLogConfig = config.getOption(ChannelOption.SO_BACKLOG);
//因爲我啓動器配置的是128,所以我這裏獲取的soBackLogConfig=128
獲取多條配置信息,使用getOptions(),代碼演示:
ChannelConfig config = channel.config();
Map<ChannelOption<?>, Object> options = config.getOptions();
for (Map.Entry<ChannelOption<?>, Object> entry : options.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
/**
SO_REUSEADDR : false
WRITE_BUFFER_LOW_WATER_MARK : 32768
WRITE_BUFFER_WATER_MARK : WriteBufferWaterMark(low: 32768, high: 65536)
SO_BACKLOG : 128
以下省略...
*/
5.5.3 channel支持的IO操作
寫操作,這裏演示從服務端寫消息發送到客戶端:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("這波啊,這波是肉蛋蔥雞~", CharsetUtil.UTF_8));
}
客戶端控制檯:
//收到服務端/127.0.0.1:6666的消息:這波啊,這波是肉蛋蔥雞~
連接操作,代碼演示:
ChannelFuture connect = channelFuture.channel().connect(new InetSocketAddress("127.0.0.1", 6666));//一般使用啓動器,這種方式不常用
通過channel獲取ChannelPipeline,並做相關的處理:
//獲取ChannelPipeline對象
ChannelPipeline pipeline = ctx.channel().pipeline();
//往pipeline中添加ChannelHandler處理器,裝配流水線
pipeline.addLast(new MyServerHandler());
5.6 Selector
在NioEventLoop中,有一個成員變量selector,這是nio包的Selector,在之前《NIO入門》中,我已經講過Selector了。
Netty中的Selector也和NIO的Selector是一樣的,就是用於監聽事件,管理註冊到Selector中的channel,實現多路複用器。
5.7 PiPeline與ChannelPipeline
在前面介紹Channel時,我們知道可以在channel中裝配ChannelHandler流水線處理器,那一個channel不可能只有一個channelHandler處理器,肯定是有很多的,既然是很多channelHandler在一個流水線工作,肯定是有順序的。
於是pipeline就出現了,pipeline相當於處理器的容器。初始化channel時,把channelHandler按順序裝在pipeline中,就可以實現按序執行channelHandler了。
在一個Channel中,只有一個ChannelPipeline。該pipeline在Channel被創建的時候創建。ChannelPipeline包含了一個ChannelHander形成的列表,且所有ChannelHandler都會註冊到ChannelPipeline中。
5.8 ChannelHandlerContext
在Netty中,Handler處理器是有我們定義的,上面講過通過集成入站處理器或者出站處理器實現。這時如果我們想在Handler中獲取pipeline對象,或者channel對象,怎麼獲取呢。
於是Netty設計了這個ChannelHandlerContext上下文對象,就可以拿到channel、pipeline等對象,就可以進行讀寫等操作。
通過類圖,ChannelHandlerContext是一個接口,下面有三個實現類。
實際上ChannelHandlerContext在pipeline中是一個鏈表的形式。看一段源碼就明白了:
//ChannelPipeline實現類DefaultChannelPipeline的構造器方法
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
//設置頭結點head,尾結點tail
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
下面我用一張圖來表示,會更加清晰一點:
5.9 EventLoopGroup
我們先看一下EventLoopGroup的類圖:
其中包括了常用的實現類NioEventLoopGroup。OioEventLoopGroup在前面的例子中也有使用過。
從Netty的架構圖中,可以知道服務器是需要兩個線程組進行配合工作的,而這個線程組的接口就是EventLoopGroup。
每個EventLoopGroup裏包括一個或多個EventLoop,每個EventLoop中維護一個Selector實例。
5.9.1 輪詢機制的實現原理
我們不妨看一段DefaultEventExecutorChooserFactory的源碼:
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
@Override
public EventExecutor next() {
//idx.getAndIncrement()相當於idx++,然後對任務長度取模
return executors[idx.getAndIncrement() & executors.length - 1];
}
這段代碼可以確定執行的方式是輪詢機制,接下來debug調試一下:
它這裏還有一個判斷,如果線程數不是2的N次方,則採用取模算法實現。
@Override
public EventExecutor next() {
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}