Netty 總算總結完了,小編 也是長舒了一口氣。有太多讀者私信我讓我總結 Netty 了,因爲經常會在面試中碰到 Netty 相關的問題。
全文采用大家喜歡的與面試官對話的形式展開。 如果大家覺得 小編 總結的不錯的話,不妨轉發分享鼓勵一下!
推薦大家看看這個Netty教程:全網最權威的Netty底層原理講解!阿里P8架構師用300分鐘讓你精通Netty。
概覽:
- Netty 是什麼?
- 爲什麼要用 Netty?
- Netty 應用場景瞭解麼?
- Netty 核心組件有哪些?分別有什麼作用?
- EventloopGroup 瞭解麼?和 EventLoop 啥關係?
- Bootstrap 和 ServerBootstrap 瞭解麼?
- NioEventLoopGroup 默認的構造函數會起多少線程?
- Netty 線程模型瞭解麼?
- Netty 服務端和客戶端的啓動過程瞭解麼?
- Netty 長連接、心跳機制瞭解麼?
- Netty 的零拷貝瞭解麼?
Netty 是什麼?
面試官 :介紹一下自己對 Netty 的認識吧!小夥子。
我 :好的!那我就簡單用 3 點來概括一下 Netty 吧!
- Netty 是一個 基於 NIO 的 client-server(客戶端服務器)框架,使用它可以快速簡單地開發網絡應用程序。
- 它極大地簡化並優化了 TCP 和 UDP 套接字服務器等網絡編程,並且性能以及安全性等很多方面甚至都要更好。
- 支持多種協議 如 FTP,SMTP,HTTP 以及各種二進制和基於文本的傳統協議。
用官方的總結就是:Netty 成功地找到了一種在不妥協可維護性和性能的情況下實現易於開發,性能,穩定性和靈活性的方法。
除了上面介紹的之外,很多開源項目比如我們常用的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
網絡編程我願意稱 Netty 爲王 。
爲什麼要用 Netty?
面試官 :爲什麼要用 Netty 呢?能不能說一下自己的看法。
我 :因爲 Netty 具有下面這些優點,並且相比於直接使用 JDK 自帶的 NIO 相關的 API 來說更加易用。
- 統一的 API,支持多種傳輸類型,阻塞和非阻塞的。
- 簡單而強大的線程模型。
- 自帶編解碼器解決 TCP 粘包/拆包問題。
- 自帶各種協議棧。
- 真正的無連接數據包套接字支持。
- 比直接使用 Java 核心 API 有更高的吞吐量、更低的延遲、更低的資源消耗和更少的內存複製。
- 安全性不錯,有完整的 SSL/TLS 以及 StartTLS 支持。
- 社區活躍
- 成熟穩定,經歷了大型項目的使用和考驗,而且很多開源項目都使用到了 Netty, 比如我們經常接觸的 Dubbo、RocketMQ 等等。
- ......
Netty 應用場景瞭解麼?
面試官 :能不能通俗地說一下使用 Netty 可以做什麼事情?
我 :憑藉自己的瞭解,簡單說一下吧!理論上來說,NIO 可以做的事情 ,使用 Netty 都可以做並且更好。Netty 主要用來做網絡通信 :
- 作爲 RPC 框架的網絡通信工具 :我們在分佈式系統中,不同服務節點之間經常需要相互調用,這個時候就需要 RPC 框架了。不同服務節點之間的通信是如何做的呢?可以使用 Netty 來做。比如我調用另外一個節點的方法的話,至少是要讓對方知道我調用的是哪個類中的哪個方法以及相關參數吧!
- 實現一個自己的 HTTP 服務器 :通過 Netty 我們可以自己實現一個簡單的 HTTP 服務器,這個大家應該不陌生。說到 HTTP 服務器的話,作爲 Java 後端開發,我們一般使用 Tomcat 比較多。一個最基本的 HTTP 服務器可要以處理常見的 HTTP Method 的請求,比如 POST 請求、GET 請求等等。
- 實現一個即時通訊系統 :使用 Netty 我們可以實現一個可以聊天類似微信的即時通訊系統,這方面的開源項目還蠻多的,可以自行去 Github 找一找。
- 實現消息推送系統 :市面上有很多消息推送系統都是基於 Netty 來做的。
- ......
Netty 核心組件有哪些?分別有什麼作用?
面試官 :Netty 核心組件有哪些?分別有什麼作用?
我 :表面上,嘴上開始說起 Netty 的核心組件有哪些,實則,內心已經開始 mmp 了,深度懷疑這面試官是存心搞我啊!
1.Channel
Channel 接口是 Netty 對網絡操作抽象類,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
比較常用的Channel接口實現類是NioServerSocketChannel(服務端)和NioSocketChannel(客戶端),這兩個 Channel 可以和 BIO 編程模型中的ServerSocket以及Socket兩個概念對應上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 類的複雜性。
2.EventLoop
這麼說吧!EventLoop(事件循環)接口可以說是 Netty 中最核心的概念了!
《Netty 實戰》這本書是這樣介紹它的:
“EventLoop 定義了 Netty 的核心抽象,用於處理連接的生命週期中所發生的事件。
是不是很難理解?說實話,我學習 Netty 的時候看到這句話是沒太能理解的。
說白了,EventLoop 的主要作用實際就是負責監聽網絡事件並調用事件處理器進行相關 I/O 操作的處理。
那 Channel 和 EventLoop 直接有啥聯繫呢?
Channel 爲 Netty 網絡操作(讀寫等操作)抽象類,EventLoop 負責處理註冊到其上的Channel 處理 I/O 操作,兩者配合參與 I/O 操作。
3.ChannelFuture
Netty 是異步非阻塞的,所有的 I/O 操作都爲異步的。
因此,我們不能立刻得到操作是否執行成功,但是,你可以通過 ChannelFuture 接口的 addListener() 方法註冊一個 ChannelFutureListener,當操作執行成功或者失敗時,監聽就會自動觸發返回結果。
並且,你還可以通過ChannelFuture 的 channel() 方法獲取關聯的Channel
public interface ChannelFuture extends Future<Void> {
Channel channel();
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
......
ChannelFuture sync() throws InterruptedException;
}
另外,我們還可以通過 ChannelFuture 接口的 sync()方法讓異步的操作變成同步的。
4.ChannelHandler 和 ChannelPipeline
下面這段代碼使用過 Netty 的小夥伴應該不會陌生,我們指定了序列化編解碼器以及自定義的 ChannelHandler 處理消息。
b.group(eventLoopGroup)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
ch.pipeline().addLast(new KryoClientHandler());
}
});
ChannelHandler 是消息的具體處理器。他負責處理讀寫操作、客戶端連接等事情。
ChannelPipeline 爲 ChannelHandler 的鏈,提供了一個容器並定義了用於沿着鏈傳播入站和出站事件流的 API 。當 Channel 被創建時,它會被自動地分配到它專屬的ChannelPipeline。
我們可以在 ChannelPipeline 上通過 addLast() 方法添加一個或者多個ChannelHandler ,因爲一個數據或者事件可能會被多個 Handler 處理。當一個 ChannelHandler 處理完之後就將數據交給下一個 ChannelHandler 。
EventloopGroup 瞭解麼?和 EventLoop 啥關係?
面試官 :剛剛你也介紹了 EventLoop。那你再說說 EventloopGroup 吧!和 EventLoop 啥關係?
我 :
EventLoopGroup 包含多個 EventLoop(每一個 EventLoop 通常內部包含一個線程),上面我們已經說了 EventLoop 的主要作用實際就是負責監聽網絡事件並調用事件處理器進行相關 I/O 操作的處理。
並且 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理,即 Thread 和 EventLoop 屬於 1 : 1 的關係,從而保證線程安全。
上圖是一個服務端對 EventLoopGroup 使用的大致模塊圖,其中 Boss EventloopGroup 用於接收連接,Worker EventloopGroup 用於具體的處理(消息的讀寫以及其他邏輯處理)。
從上圖可以看出:當客戶端通過 connect 方法連接服務端時,bossGroup 處理客戶端連接請求。當客戶端處理完成後,會將這個連接提交給 workerGroup 來處理,然後 workerGroup 負責處理其 IO 相關操作。
Bootstrap 和 ServerBootstrap 瞭解麼?
面試官 :你再說說自己對 Bootstrap 和 ServerBootstrap 的瞭解吧!
我 :
Bootstrap 是客戶端的啓動引導類/輔助類,具體使用方法如下:
EventLoopGroup group = new NioEventLoopGroup();
try {
//創建客戶端啓動引導/輔助類:Bootstrap
Bootstrap b = new Bootstrap();
//指定線程模型
b.group(group).
......
// 嘗試建立連接
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
// 優雅關閉相關線程組資源
group.shutdownGracefully();
}
ServerBootstrap 客戶端的啓動引導類/輔助類,具體使用方法如下:
// 1.bossGroup 用於接收連接,workerGroup 用於具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.創建服務端啓動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.給引導類配置兩大線程組,確定了線程模型
b.group(bossGroup, workerGroup).
......
// 6.綁定端口
ChannelFuture f = b.bind(port).sync();
// 等待連接關閉
f.channel().closeFuture().sync();
} finally {
//7.優雅關閉相關線程組資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
從上面的示例中,我們可以看出:
- Bootstrap 通常使用 connet() 方法連接到遠程的主機和端口,作爲一個 Netty TCP 協議通信中的客戶端。另外,Bootstrap 也可以通過 bind() 方法綁定本地的一個端口,作爲 UDP 協議通信中的一端。
- ServerBootstrap通常使用 bind() 方法綁定本地的端口上,然後等待客戶端的連接。
- Bootstrap 只需要配置一個線程組— EventLoopGroup ,而 ServerBootstrap需要配置兩個線程組— EventLoopGroup ,一個用於接收連接,一個用於具體的處理。
NioEventLoopGroup 默認的構造函數會起多少線程?
面試官 :看過 Netty 的源碼了麼?NioEventLoopGroup 默認的構造函數會起多少線程呢?
我 :嗯嗯!看過部分。
回顧我們在上面寫的服務器端的代碼:
// 1.bossGroup 用於接收連接,workerGroup 用於具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
爲了搞清楚NioEventLoopGroup 默認的構造函數 到底創建了多少個線程,我們來看一下它的源碼。
/**
* 無參構造函數。
* nThreads:0
*/
public NioEventLoopGroup() {
//調用下一個構造方法
this(0);
}
/**
* Executor:null
*/
public NioEventLoopGroup(int nThreads) {
//繼續調用下一個構造方法
this(nThreads, (Executor) null);
}
//中間省略部分構造函數
/**
* RejectedExecutionHandler():RejectedExecutionHandlers.reject()
*/
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {
//開始調用父類的構造函數
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
一直向下走下去的話,你會發現在 MultithreadEventLoopGroup 類中有相關的指定線程數的代碼,如下:
// 從1,系統屬性,CPU核心數*2 這三個值中取出一個最大的
//可以得出 DEFAULT_EVENT_LOOP_THREADS 的值爲CPU核心數*2
private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
// 被調用的父類構造函數,NioEventLoopGroup 默認的構造函數會起多少線程的祕密所在
// 當指定的線程數nThreads爲0時,使用默認的線程數DEFAULT_EVENT_LOOP_THREADS
protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
}
綜上,我們發現 NioEventLoopGroup 默認的構造函數實際會起的線程數爲 CPU核心數*2。
另外,如果你繼續深入下去看構造函數的話,你會發現每個NioEventLoopGroup對象內部都會分配一組NioEventLoop,其大小是 nThreads, 這樣就構成了一個線程池, 一個NIOEventLoop 和一個線程相對應,這和我們上面說的 EventloopGroup 和 EventLoop關係這部分內容相對應。
Netty 線程模型瞭解麼?
面試官 :說一下 Netty 線程模型吧!
我 :大部分網絡框架都是基於 Reactor 模式設計開發的。
“
Reactor 模式基於事件驅動,採用多路複用將事件分發給相應的 Handler 處理,非常適合處理海量 IO 的場景。
在 Netty 主要靠 NioEventLoopGroup 線程池來實現具體的線程模型的 。
我們實現服務端的時候,一般會初始化兩個線程組:
- bossGroup :接收連接。
- workerGroup :負責具體的處理,交由對應的 Handler 處理。
下面我們來詳細看一下 Netty 中的線程模型吧!
1.單線程模型 :
一個線程需要執行處理所有的 accept、read、decode、process、encode、send 事件。對於高負載、高併發,並且對性能要求比較高的場景不適用。
對應到 Netty 代碼是下面這樣的
“使用 NioEventLoopGroup 類的無參構造函數設置線程數量的默認值就是 CPU 核心數 *2 。
//1.eventGroup既用於處理客戶端連接,又負責具體的處理。
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
//2.創建服務端啓動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
boobtstrap.group(eventGroup, eventGroup)
//......
2.多線程模型
一個 Acceptor 線程只負責監聽客戶端的連接,一個 NIO 線程池負責具體處理:accept、read、decode、process、encode、send 事件。滿足絕大部分應用場景,併發連接量不大的時候沒啥問題,但是遇到併發連接大的時候就可能會出現問題,成爲性能瓶頸。
對應到 Netty 代碼是下面這樣的:
// 1.bossGroup 用於接收連接,workerGroup 用於具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.創建服務端啓動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.給引導類配置兩大線程組,確定了線程模型
b.group(bossGroup, workerGroup)
//......
3.主從多線程模型
從一個 主線程 NIO 線程池中選擇一個線程作爲 Acceptor 線程,綁定監聽端口,接收客戶端連接的連接,其他線程負責後續的接入認證等工作。連接建立完成後,Sub NIO 線程池負責具體處理 I/O 讀寫。如果多線程模型無法滿足你的需求的時候,可以考慮使用主從多線程模型 。
// 1.bossGroup 用於接收連接,workerGroup 用於具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.創建服務端啓動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.給引導類配置兩大線程組,確定了線程模型
b.group(bossGroup, workerGroup)
//......
Netty 服務端和客戶端的啓動過程瞭解麼?
服務端
// 1.bossGroup 用於接收連接,workerGroup 用於具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.創建服務端啓動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.給引導類配置兩大線程組,確定了線程模型
b.group(bossGroup, workerGroup)
// (非必備)打印日誌
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定義客戶端消息的業務處理邏輯
p.addLast(new HelloServerHandler());
}
});
// 6.綁定端口,調用 sync 方法阻塞知道綁定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服務器Channel關閉(closeFuture()方法獲取Channel 的CloseFuture對象,然後調用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.優雅關閉相關線程組資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
簡單解細一下服務端的創建過程具體是怎樣的:
1.首先你創建了兩個 NioEventLoopGroup 對象實例:bossGroup 和 workerGroup。
- bossGroup : 用於處理客戶端的 TCP 連接請求。
- workerGroup :負責每一條連接的具體讀寫數據的處理邏輯,真正負責 I/O 讀寫操作,交由對應的 Handler 處理。
舉個例子:我們把公司的老闆當做 bossGroup,員工當做 workerGroup,bossGroup 在外面接完活之後,扔給 workerGroup 去處理。一般情況下我們會指定 bossGroup 的 線程數爲 1(併發連接量不大的時候) ,workGroup 的線程數量爲 CPU 核心數 *2 。另外,根據源碼來看,使用 NioEventLoopGroup 類的無參構造函數設置線程數量的默認值就是 CPU 核心數 *2 。
2.接下來 我們創建了一個服務端啓動引導/輔助類:ServerBootstrap,這個類將引導我們進行服務端的啓動工作。
3.通過 .group() 方法給引導類 ServerBootstrap 配置兩大線程組,確定了線程模型。
通過下面的代碼,我們實際配置的是多線程模型,這個在上面提到過。
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
4.通過channel()方法給引導類 ServerBootstrap指定了 IO 模型爲NIO
- NioServerSocketChannel :指定服務端的 IO 模型爲 NIO,與 BIO 編程模型中的ServerSocket對應
- NioSocketChannel : 指定客戶端的 IO 模型爲 NIO, 與 BIO 編程模型中的Socket對應5.通過 .childHandler()給引導類創建一個ChannelInitializer ,然後制定了服務端消息的業務處理邏輯 HelloServerHandler 對象6.調用 ServerBootstrap 類的 bind()方法綁定端口
客戶端
//1.創建一個 NioEventLoopGroup 對象實例
EventLoopGroup group = new NioEventLoopGroup();
try {
//2.創建客戶端啓動引導/輔助類:Bootstrap
Bootstrap b = new Bootstrap();
//3.指定線程組
b.group(group)
//4.指定 IO 模型
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 5.這裏可以自定義消息的業務處理邏輯
p.addLast(new HelloClientHandler(message));
}
});
// 6.嘗試建立連接
ChannelFuture f = b.connect(host, port).sync();
// 7.等待連接關閉(阻塞,直到Channel關閉)
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
繼續分析一下客戶端的創建流程:
1.創建一個 NioEventLoopGroup 對象實例
2.創建客戶端啓動的引導類是 Bootstrap
3.通過 .group() 方法給引導類 Bootstrap 配置一個線程組
4.通過channel()方法給引導類 Bootstrap指定了 IO 模型爲NIO
5.通過 .childHandler()給引導類創建一個ChannelInitializer ,然後制定了客戶端消息的業務處理邏輯 HelloClientHandler 對象
6.調用 Bootstrap 類的 connect()方法進行連接,這個方法需要指定兩個參數:
- inetHost : ip 地址
- inetPort : 端口號
public ChannelFuture connect(String inetHost, int inetPort) {
return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
public ChannelFuture connect(SocketAddress remoteAddress) {
ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
this.validate();
return this.doResolveAndConnect(remoteAddress, this.config.localAddress());
}
connect 方法返回的是一個 Future 類型的對象
public interface ChannelFuture extends Future<Void> {
......
}
也就是說這個方是異步的,我們通過 addListener 方法可以監聽到連接是否成功,進而打印出連接信息。具體做法很簡單,只需要對代碼進行以下改動:
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("連接成功!");
} else {
System.err.println("連接失敗!");
}
}).sync();
什麼是 TCP 粘包/拆包?有什麼解決辦法呢?
面試官 :什麼是 TCP 粘包/拆包?
我 :TCP 粘包/拆包 就是你基於 TCP 發送數據的時候,出現了多個字符串“粘”在了一起或者一個字符串被“拆”開的問題。比如你多次發送:“你好,你真帥啊!哥哥!”,但是客戶端接收到的可能是下面這樣的:
面試官 :那有什麼解決辦法呢?
我 :
1.使用 Netty 自帶的解碼器
- LineBasedFrameDecoder : 發送端發送數據包的時候,每個數據包之間以換行符作爲分隔,LineBasedFrameDecoder 的工作原理是它依次遍歷 ByteBuf 中的可讀字節,判斷是否有換行符,然後進行相應的截取。
- DelimiterBasedFrameDecoder : 可以自定義分隔符解碼器,LineBasedFrameDecoder 實際上是一種特殊的 DelimiterBasedFrameDecoder 解碼器。
- FixedLengthFrameDecoder: 固定長度解碼器,它能夠按照指定的長度對消息進行相應的拆包。
- LengthFieldBasedFrameDecoder:
2.自定義序列化編解碼器
在 Java 中自帶的有實現 Serializable 接口來實現序列化,但由於它性能、安全性等原因一般情況下是不會被使用到的。
通常情況下,我們使用 Protostuff、Hessian2、json 序列方式比較多,另外還有一些序列化性能非常好的序列化方式也是很好的選擇:
- 專門針對 Java 語言的:Kryo,FST 等等
- 跨語言的:Protostuff(基於 protobuf 發展而來),ProtoBuf,Thrift,Avro,MsgPack 等等
“
由於篇幅問題,這部分內容會在後續的文章中詳細分析介紹~~~
Netty 長連接、心跳機制瞭解麼?
面試官 :TCP 長連接和短連接瞭解麼?
我 :我們知道 TCP 在進行讀寫之前,server 與 client 之間必須提前建立一個連接。建立連接的過程,需要我們常說的三次握手,釋放/關閉連接的話需要四次揮手。這個過程是比較消耗網絡資源並且有時間延遲的。
所謂,短連接說的就是 server 端 與 client 端建立連接之後,讀寫完成之後就關閉掉連接,如果下一次再要互相發送消息,就要重新連接。短連接的優點很明顯,就是管理和實現都比較簡單,缺點也很明顯,每一次的讀寫都要建立連接必然會帶來大量網絡資源的消耗,並且連接的建立也需要耗費時間。
長連接說的就是 client 向 server 雙方建立連接之後,即使 client 與 server 完成一次讀寫,它們之間的連接並不會主動關閉,後續的讀寫操作會繼續使用這個連接。長連接的可以省去較多的 TCP 建立和關閉的操作,降低對網絡資源的依賴,節約時間。對於頻繁請求資源的客戶來說,非常適用長連接。
面試官 :爲什麼需要心跳機制?Netty 中心跳機制瞭解麼?
我 :
在 TCP 保持長連接的過程中,可能會出現斷網等網絡異常出現,異常發生的時候, client 與 server 之間如果沒有交互的話,他們是無法發現對方已經掉線的。爲了解決這個問題, 我們就需要引入 心跳機制 。
心跳機制的工作原理是: 在 client 與 server 之間在一定時間內沒有數據交互時, 即處於 idle 狀態時, 客戶端或服務器就會發送一個特殊的數據包給對方, 當接收方收到這個數據報文後, 也立即發送一個特殊的數據報文, 迴應發送方, 此即一個 PING-PONG 交互。所以, 當某一端收到心跳消息後, 就知道了對方仍然在線, 這就確保 TCP 連接的有效性.
TCP 實際上自帶的就有長連接選項,本身是也有心跳包機制,也就是 TCP 的選項:SO_KEEPALIVE。但是,TCP 協議層面的長連接靈活性不夠。所以,一般情況下我們都是在應用層協議上實現自定義信跳機制的,也就是在 Netty 層面通過編碼實現。通過 Netty 實現心跳機制的話,核心類是 IdleStateHandler 。
Netty 的零拷貝瞭解麼?
面試官 :講講 Netty 的零拷貝?
我 :
維基百科是這樣介紹零拷貝的:
“
零複製(英語:Zero-copy;也譯零拷貝)技術是指計算機執行操作時,CPU 不需要先將數據從某處內存複製到另一個特定區域。這種技術通常用於通過網絡傳輸文件時節省 CPU 週期和內存帶寬。
在 OS 層面上的 Zero-copy 通常指避免在 用戶態(User-space) 與 內核態(Kernel-space) 之間來回拷貝數據。而在 Netty 層面 ,零拷貝主要體現在對於數據操作的優化。
Netty 中的零拷貝體現在以下幾個方面:
- 使用 Netty 提供的 CompositeByteBuf 類, 可以將多個ByteBuf 合併爲一個邏輯上的 ByteBuf, 避免了各個 ByteBuf 之間的拷貝。
- ByteBuf 支持 slice 操作, 因此可以將 ByteBuf 分解爲多個共享同一個存儲區域的 ByteBuf, 避免了內存的拷貝。
- 通過 FileRegion 包裝的FileChannel.tranferTo 實現文件傳輸, 可以直接將文件緩衝區的數據發送到目標 Channel, 避免了傳統通過循環 write 方式導致的內存拷貝問題.