前言
關於Netty
的學習,最近看了不少有關視訊和書籍,也收穫不少,希望把我知道的分享給你們,一起加油,一起成長。前面我們對 Java IO
、BIO
、NIO
、 AIO
進行了分析,相關文章連結如下:
本篇文章我們就開始對 Netty
來進行深入分析,首先我們來了解一下 JAVA NIO
、AIO
的不足之處。
Java原生API之痛
雖然JAVA NIO
和 JAVA AIO
框架提供了多路複用IO/非同步IO的支援,但是並沒有提供上層“資訊格式”的良好封裝。用這些API實現一款真正的網路應用則並非易事。
JAVA NIO
和 JAVA AIO
並沒有提供斷連重連、網路閃斷、半包讀寫、失敗快取、網路擁塞和異常碼流等的處理,這些都需要開發者自己來補齊相關的工作。
AIO
在實踐中,並沒有比NIO
更好。AIO
在不同的平臺有不同的實現,windows系統下使用的是一種非同步IO技術:IOCP
;Linux下由於沒有這種非同步 IO 技術,所以使用的是epoll
對非同步 IO 進行模擬。所以 AIO 在 Linux 下的效能並不理想。AIO 也沒有提供對 UDP 的支援。
綜上,在實際的大型網際網路專案中,Java 原生的 API 應用並不廣泛,取而代之的是一款第三方Java 框架,這就是Netty
。
Netty的優勢
Netty 提供 非同步的、事件驅動的網路應用程式框架和工具,用以快速開發高效能、高可靠性的網路伺服器和客戶端程式。
非阻塞 I/O
Netty 是基於 Java NIO
API 實現的網路應用框架,使用它可以快速簡單的開發網路應用程式,如伺服器和客戶端程式。Netty 大大簡化了網路程式開發的過程,如 TCP 和 UDP 的 Socket 服務的開發。
由於是基於 NIO 的 API,因此,Netty 可以提供非阻塞的 I/O
操作,極大的提升了效能。同時,Netty 內部封裝了 Java NIO API 的複雜性,並提供了執行緒池的處理,使得開發 NIO 的應用變得極其簡單。
豐富的協議
Netty 提供了簡單、易用的 API ,但這並不意味著應用程式會有難維護和效能低的問題。Netty 是一個精心設計的框架,它從許多協議的實現中吸收了很多的經驗,如 FTP 、SMTP、 HTTP、許多二進位制和基於文字的傳統協議。
Netty 支援豐富的網路協議,如TCP
、 UDP
、 HTTP
、 HTTP/2
、 WebSocket
、 SSL/TLS
等,這些協議實現開箱即用,因此,Netty 開發者能夠在不失靈活的前提下來實現開發的簡易性、高效能和穩定性。
非同步和事件驅動
Netty 是非同步事件驅動的框架,該框架體現為所有的I/O
操作都是非同步的,所有的I/O
呼叫會立即返回,並不保證呼叫成功與否,但是呼叫會返回ChannelFuture
。Netty 會通過 ChannelFuture
通知呼叫是成功了還是失敗了,抑或是取消了。
同時,Netty 是基於事件驅動的,呼叫者並不能立即獲得結果,而是通過事件監聽機制,使用者可以方便地主動獲取或者通過通知機制獲得I/O
操作的結果。
當Future
物件剛剛建立時,處於非完成狀態,呼叫者可以通過返回的ChannelFuture
來獲取操作執行的狀態,再通過註冊監聽函式來執行完成後的操作,常見有如下操作:
- 通過
isDone
方法來判斷當前操作是否完成。 - 通過
isSuccess
方法來判斷已完成的當前操作是否成功。 - 通過
getCause
方法來獲取已完成的當前操作失敗的原因。 - 通過
isCancelled
方法來判斷已完成的當前操作是否被取消。 - 通過
addListener
方法來註冊監聽器,當操作已完成(isDone
方法返回完成),將會通知指定的監聽器;如果future
物件已完成,則理解通知指定的監聽器。
例如:下面的程式碼中繫結埠是非同步操作,當繫結操作處理完,將會呼叫相應的監聽器處理邏輯。
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()){
System.out.println("埠繫結成功!");
}else {
System.out.println("埠繫結失敗!");
}
});
相比傳統的阻塞 I/O
,Netty 非同步處理的好處是不會造成執行緒阻塞,執行緒在 I/O
操作期間可以執行其他的程式,在高併發情形下會更穩定並擁有更高的吞吐量。
精心設計的API
Netty 從開始就為使用者提供了體驗最好的API及實現設計。
例如,在使用者數較小的時候可能會選擇傳統的阻塞API,畢竟與 Java NIO 相比使用阻塞 API 將會更加容易一些。然而,當業務量呈指數增長並且伺服器需要同時處理成千上萬的客戶連線,便會遇到問題。這種情況下可能會嘗試使用 Java NIO,但是複雜的 NIO Selector 程式設計介面又會耗費大量的時間並最終會阻礙快速開發。
Netty 提供了一個叫作 channel
的統一的非同步I/O
程式設計介面,這個程式設計介面抽象了所有點對點的通訊操作。也就是說,如果應用是基於Netty 的某一種傳輸實現,那麼同樣的,應用也可以執行在 Netty 的另一種傳輸實現上。Channel
常見的子介面有:
豐富的緩衝實現
Netty 使用自建的快取 API,而不是使用 Java NIO 的 ByteBuffer
來表示一個連續的位元組序列。與 ByteBuffer
相比,這種方式擁有明顯的優勢。
Netty 使用新的緩衝型別 ByteBuf
,並且被設計為可從底層解決 ByteBuffer
問題,同時還滿足日常網路應用開發需要的緩衝型別。
Netty 重要有以下特性:
- 允許使用自定義的緩衝型別。
- 複合緩衝型別中內建透明的零拷貝實現。
- 開箱即用動態緩衝型別,具有像
StringBuffer
一樣的動態緩衝能力。 - 不再需要呼叫
flip()
方法。 - 正常情況下具有比
ByteBuffer
更快的響應速度。
高效的網路傳輸
Java 原生的序列化主要存在以下幾個弊端:
-
無法跨語言。
-
序列化後碼流太大。
-
序列化後效能太低。
業界有非常多的框架用於解決上述問題,如 Google Protobuf
、JBoss Marshalling
、Facebook Thrift
等。針對這些框架,Netty 都提供了相應的包將這些框架整合到應用中。同時,Netty 本身也提供了眾多的編解碼工具,方便開發者使用。開發者可以基於 Netty 來開發高效的網路傳輸應用,例如:高效能的訊息中介軟體 Apache RocketMQ
、高效能RPC框架Apache Dubbo
等。
Netty 核心概念
從上述的架構圖可以看出,Netty 主要由三大塊組成:
- 核心元件
- 傳輸服務
- 協議
核心元件
核心元件包括:事件模型、位元組緩衝區和通訊API
事件模型
Netty 是基於非同步事件驅動的,該框架體現為所有的I/O
操作都是非同步的,呼叫者並不能立即獲得結果,而是通過事件監聽機制,使用者可以方便地主動獲取或者通過通知機制獲得I/O
操作的結果。
Netty 將所有的事件按照它們與入站或出站資料流的相關性進行了分類。
可能由入站資料或者相關的狀態更改而觸發的事件包括以下幾項:
- 連線已被啟用或者連線失活。
- 資料讀取。
- 使用者事件。
- 錯誤事件。
出站事件是未來將會觸發的某個動作的操作結果,包括以下動作:
- 開啟或者關閉到遠端節點的連線。
- 將資料寫到或者沖刷到套接字。
每個事件都可以被分發到ChannelHandler
類中的某個使用者實現的方法。
位元組緩衝區
Netty 使用了區別於Java ByteBuffer
的新的緩衝型別ByteBuf
,ByteBuf
提供了豐富的特性。
通訊API
Netty 的通訊API都被抽象到Channel
裡,以統一的非同步I/O
程式設計介面來滿足所有點對點的通訊操作。
傳輸服務
Netty 內建了一些開箱即用的傳輸服務。因為並不是它們所有的傳輸都支援每一種協議,所以必須選擇一個和應用程式所使用的協議相相容的傳輸。以下是Netty提供的所有的傳輸。
NIO
io.netty.channel.socket.nio
包用於支援NIO。該包下面的實現是使用java.nio.channels
包作為基礎(基於選擇器的方式)。
epoll
io.netty.channel.epoll
包用於支援由 JNI 驅動的 epoll 和 非阻塞 IO。
需要注意的是,這個epoll
傳輸只能在 Linux 上獲得支援。epoll
同時提供多種特性,如:SO_REUSEPORT 等,比 NIO傳輸更快,而且是完全非阻塞的。
OIO
io.netty.channel.socket.oio
包用於支援使用java.net
包作為基礎的阻塞I/O
。
本地
io.netty.channel.local
包用於支援在 VM 內部通過管道進行通訊的本地傳輸。
內嵌
io.netty.channel.embedded
包作為內嵌傳輸,允許使用ChannelHandler
而又不需要一個真正的基於網路的傳輸。
協議支援
Netty 支援豐富的網路協議,如TCP
、 UDP
、 HTTP
、 HTTP/2
、 WebSocket
、 SSL/TLS
等,這些協議實現開箱即用,因此,Netty 開發者能夠在不失靈活的前提下來實現開發的簡易性、高效能和穩定性。
Netty簡單應用
引入Maven依賴
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.49.Final</version>
</dependency>
服務端的管道處理器
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//讀取資料實際(這裡我們可以讀取客戶端傳送的訊息)
/*
1. ChannelHandlerContext ctx:上下文物件, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客戶端傳送的資料 預設Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx =" + ctx);
Channel channel = ctx.channel();
//將 msg 轉成一個 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客戶端傳送訊息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客戶端地址:" + channel.remoteAddress());
}
//資料讀取完畢
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是 write + flush
//將資料寫入到快取,並重新整理
//一般講,我們對這個傳送的資料進行編碼
ctx.writeAndFlush(Unpooled.copiedBuffer("公司最近賬戶沒啥錢,再等幾天吧!", CharsetUtil.UTF_8));
}
//處理異常, 一般是需要關閉通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
NettyServerHandler
繼承自ChannelInboundHandlerAdapter
,這個類實現了ChannelInboundHandler
介面。ChannelInboundHandler
提供了許多事件處理的介面方法。
這裡覆蓋了channelRead()
事件處理方法。每當從客戶端收到新的資料時,這個方法會在收到訊息時被呼叫。
channelReadComplete()
事件處理方法是資料讀取完畢時被呼叫,通過呼叫ChannelHandlerContext
的writeAndFlush()
方法,把訊息寫入管道,並最終傳送給客戶端。
exceptionCaught()
事件處理方法是,當出現Throwable
物件時才會被呼叫。
服務端主程式
public class NettyServer {
public static void main(String[] args) throws Exception {
//建立BossGroup 和 WorkerGroup
//說明
//1. 建立兩個執行緒組 bossGroup 和 workerGroup
//2. bossGroup 只是處理連線請求 , 真正的和客戶端業務處理,會交給 workerGroup完成
//3. 兩個都是無限迴圈
//4. bossGroup 和 workerGroup 含有的子執行緒(NioEventLoop)的個數
// 預設實際 cpu核數 * 2
//
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
try {
//建立伺服器端的啟動物件,配置引數
ServerBootstrap bootstrap = new ServerBootstrap();
//使用鏈式程式設計來進行設定
bootstrap.group(bossGroup, workerGroup) //設定兩個執行緒組
.channel(NioServerSocketChannel.class) //bossGroup使用NioSocketChannel 作為伺服器的通道實現
.option(ChannelOption.SO_BACKLOG, 128) // 設定執行緒佇列得到連線個數 option主要是針對boss執行緒組,
.childOption(ChannelOption.SO_KEEPALIVE, true) //設定保持活動連線狀態 child主要是針對worker執行緒組
.childHandler(new ChannelInitializer<SocketChannel>() {//workerGroup使用 SocketChannel建立一個通道初始化物件 (匿名物件)
//給pipeline 設定處理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//可以使用一個集合管理 SocketChannel, 再推送訊息時,可以將業務加入到各個channel 對應的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
}
}); // 給我們的workerGroup 的 EventLoop 對應的管道設定處理器
System.out.println(".....伺服器 is ready...");
//繫結一個埠並且同步, 生成了一個 ChannelFuture 物件
//啟動伺服器(並繫結埠)
ChannelFuture cf = bootstrap.bind(7788).sync();
//給cf 註冊監聽器,監控我們關心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("服務已啟動,埠號為7788...");
} else {
System.out.println("服務啟動失敗...");
}
}
});
//對關閉通道進行監聽
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NioEventLoopGroup
是用來處理I/O
操作的多執行緒事件迴圈器。Netty 提供了許多不同的EventLoopGroup
的實現來處理不同的傳輸。
上面的服務端應用中,有兩個NioEventLoopGroup
被使用。第一個叫作bossGroup
,用來接收進來的連線。第二個叫作workerGroup
,用來處理已經被接收的連線,一旦 bossGroup
接收連線,就會把連線的資訊註冊到workerGroup
上。
ServerBootstrap
是一個NIO服務的引導啟動類。可以在這個服務中直接使用Channel
。
group
方法用於 設定EventLoopGroup
。- 通過
Channel
方法,可以指定新連線進來的Channel
型別為NioServerSocketChannel
類。 childHandler
用於指定ChannelHandler
,也就是前面實現的NettyServerHandler
。- 可以通過
option
設定指定的Channel
來實現NioServerSocketChannel
的配置引數。 childOption
主要設定SocketChannel
的子Channel
的選項。bind
用於繫結埠啟動服務。
客戶端管道處理器
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//當通道就緒就會觸發該方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client ctx =" + ctx);
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 {
cause.printStackTrace();
ctx.close();
}
}
channelRead
方法中將接收到的訊息轉化為字串,方便在控制檯上列印出來。
channelRead
接收到的訊息型別為ByteBuf
,ByteBuf
提供了轉為字串的方便方法。
客戶端主程式
public class NettyClient {
public static void main(String[] args) throws Exception {
//客戶端需要一個事件迴圈組
EventLoopGroup group = new NioEventLoopGroup();
try {
//建立客戶端啟動物件
//注意客戶端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap();
//設定相關引數
bootstrap.group(group) //設定執行緒組
.channel(NioSocketChannel.class) // 設定客戶端通道的實現類(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler()); //加入自己的處理器
}
});
System.out.println("客戶端 ok..");
//啟動客戶端去連線伺服器端
//關於 ChannelFuture 要分析,涉及到netty的非同步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7788).sync();
//給關閉通道進行監聽
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
客戶端只需要一個NioEventLoopGroup
就可以了。
測試執行
分別啟動伺服器 NettyServer
和客戶端 NettyClient
程式
服務端控制檯輸出內容:
.....伺服器 is ready...
服務已啟動,埠號為7788...
server ctx =ChannelHandlerContext(NettyServerHandler#0, [id: 0xa1b2233c, L:/127.0.0.1:7788 - R:/127.0.0.1:63239])
客戶端傳送訊息是:老闆,工資什麼時候發給我啊?
客戶端地址:/127.0.0.1:63239
客戶端控制檯輸出內容:
客戶端 ok..
client ctx =ChannelHandlerContext(NettyClientHandler#0, [id: 0x21d6f98e, L:/127.0.0.1:63239 - R:/127.0.0.1:7788])
伺服器回覆的訊息:公司最近賬戶沒啥錢,再等幾天吧!
伺服器的地址: /127.0.0.1:7788
至此,一個簡單的基於Netty開發的服務端和客戶端就完成了。
總結
本篇文章主要講解了 Netty 產生的背景、特點、核心元件及如何快速開啟第一個 Netty 應用。
後面我們會分析Netty架構設計
、Channel
、ChannelHandler
、位元組緩衝區ByteBuf
、執行緒模型
、編解碼
、載入程式
等方面的知識。
結尾
我是一個正在被打擊還在努力前進的碼農。如果文章對你有幫助,記得點贊、關注喲,謝謝!