萬字長文帶你深入理解netty!為你的春招做好準備!
Table of Contents generated with DocToc
- 1、IO和NIO
- 2、JDK原生NIO程式的問題
- 3、Netty的介紹
- 4、Netty的執行緒模型
- 5、Netty工作原理
- 6、netty的啟動
- 7、ByteBuf
- 9、channelHandler
- 10、NioEventLoop
- 11、通訊協議編解碼
- 12、Netty記憶體池和物件池
- 13、心跳與空閒檢測
- 14、拆包粘包理論與解決
- 15、Netty 自帶的拆包器
- 16、預留問題
1、IO和NIO
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-kOkpcBhq-1607192867690)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-1.png)]
面向流和麵向Buffer
傳統IO和Java NIO最大的區別是傳統的IO是面向流,NIO是面向Buffer
Java IO面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被快取在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先將它快取到一個緩衝區。
Java NIO的緩衝導向方法略有不同。資料讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的資料。而且,需確保當更多的資料讀入緩衝區時,不要覆蓋緩衝區裡尚未處理的資料。
選擇器
Java NIO的選擇器允許一個單獨的執行緒來 監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的執行緒來“選擇”通道,這些通道里已經有可以處理的輸入,或者選擇已經準備寫入的通道,這種選擇機制,使得一個單獨的執行緒很容易來管理多個通道
區別
傳統的IO
- socketServer的accept方法是阻塞的;
- 獲得連線的順序是和客戶端請求到達伺服器的先後順序相關;
- 適用於一個執行緒管理一個通道的情況;因為其中的流資料的讀取是阻塞的;
- 適合需要管理同時開啟不太多的連線,這些連線會傳送大量的資料
NIO
- 基於事件驅動,當有連線請求,會將此連線註冊到多路複用器上(selector);
- 在多路複用器上可以註冊監聽事件,比如監聽accept、read;
- 通過監聽,當真正有請求資料時,才來處理資料;
- 會不停的輪詢是否有就緒的事件,所以處理順序和連線請求先後順序無關,與請求資料到來的先後順序有關;
- 優勢在於一個執行緒管理多個通道;但是資料的處理將會變得複雜;
- 適合需要管理同時開啟的成千上萬個連線,這些連線每次只是傳送少量的資料
2、JDK原生NIO程式的問題
JDK 原生也有一套網路應用程式 API,但是存在一系列問題,主要如下:
- NIO 的類庫和 API 繁雜,使用麻煩:你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
- 需要具備其他的額外技能做鋪墊:例如熟悉 Java 多執行緒程式設計,因為 NIO 程式設計涉及到 Reactor 模式,你必須對多執行緒和網路程式設計非常熟悉,才能編寫出高質量的 NIO 程式。
- 可靠效能力補齊,開發工作量和難度都非常大:例如客戶端面臨斷連重連、網路閃斷、半包讀寫、失敗快取、網路擁塞和異常碼流的處理等等。NIO 程式設計的特點是功能開發相對容易,但是可靠效能力補齊工作量和難度都非常大。
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。官方聲稱在 JDK 1.6 版本的 update 18 修復了該問題,但是直到 JDK 1.7 版本該問題仍舊存在,只不過該 Bug 發生概率降低了一些而已,它並沒有被根本解決。
3、Netty的介紹
Netty 是一個非同步事件驅動的網路應用框架,用於快速開發可維護的高效能伺服器和客戶端。
Netty 對 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題。
Netty的主要特點
1、高效能
- 採用非同步非阻塞的IO類庫,基於Reactor模式實現,解決了傳統同步阻塞IO模式
- TCP接收和傳送緩衝區使用直接記憶體代替堆記憶體,避免了記憶體複製,提升了IO讀取和寫入的效能
- 支援記憶體池的方式迴圈利用ByteBuf,避免了頻繁外掛和銷燬ByteBuf帶來的效能消耗
- 可配置的IO執行緒數、TCP引數等,為不同的使用者場景提供定製化的調優引數,滿足不同的效能場景
- 採用環形陣列緩衝區實現無鎖化併發程式設計,代替傳統的執行緒安全或鎖。
- 合理使用執行緒安全容器,原子類,提升系統的併發處理能力
- 關鍵資源的處理使用單執行緒序列化的方式,避免多執行緒併發訪問帶來的鎖競爭和cpu資源消耗
- 通過引用計數法及時地申請釋放不再被引用的物件,細粒度的記憶體管理降低了GC的頻率,減少了頻繁GC帶來的時延增大和CPU損耗
2、可靠性
1、 鏈路有效監測(心跳和空閒檢測)
- 讀空閒超時機制
- 寫空閒超時機制
2、記憶體保護機制
- 通過物件引用計數法對Netty的ByteBuf等內建物件進行細粒度的記憶體申請和釋放,對非法的物件引用進行檢測和保護
- 通過記憶體池來重用ByteBuf,節省記憶體
- 可設定的記憶體容量上限,包括ByteBuf、執行緒池執行緒數
3、優雅停機 - 優雅停機需要設定最大超時時間,如果達到該時間系統還沒退出,則通過Kill -9 pid強殺當前執行緒。
- JVM通過註冊的Shutdown Hook攔截到退出訊號量,然後執行退出操作
3、可定製性
- 責任鏈模式:channelPipeline基於責任鏈模式開發,便於業務邏輯的攔截、定製和擴充套件
- 基於介面的開發:關鍵的類庫都提供了介面或者抽象類,使用者可以自定義實現相關介面
- 提供了大量工廠類,通過過載這些工廠類可以按需建立出使用者實現的物件
- 提供大量的系統引數供使用者按需設定,增強系統的場景定製
4、可擴充套件性
可以方便進行應用層協議定製,比如Dubbo、RocketMQ
4、Netty的執行緒模型
對於網路請求一般可以分為兩個處理階段,一是接收請求任務,二是處理網路請求。根據不同階段處理方式分為以下幾種執行緒模型:
1、序列化處理模型
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-CfkOmNe1-1607192867691)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-2.png)]
這個模型中用一個執行緒來處理網路請求連線和任務處理,當worker接受到一個任務之後,就立刻進行處理,也就是說任務接受和任務處理是在同一個worker執行緒中進行的,沒有進行區分。這樣做存在一個很大的問題是,必須要等待某個task處理完成之後,才能接受處理下一個task。
因此可以把接收任務和處理任務兩個階段分開處理,一個執行緒接收任務,放入任務佇列,另外的執行緒非同步處理任務佇列中的任務。
2、並行化處理模型
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Gu1lkOww-1607192867692)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-3.png)]
由於任務處理一般比較緩慢,會導致任務佇列中任務積壓長時間得不到處理,這時可以使用執行緒池來處理。可以通過為每個執行緒維護一個任務佇列來改進這種模型。
3、Netty具體執行緒模型
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-s6XuIn9A-1607192867694)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-4.png)]
1、如何理解NioEventLoop和NioEventLoopGroup
- NioEventLoop實際上就是工作執行緒,可以直接理解為一個執行緒。NioEventLoopGroup是一個執行緒池,執行緒池中的執行緒就是NioEventLoop。
- 實際上bossGroup中有多個NioEventLoop執行緒,每個NioEventLoop繫結一個埠,也就是說,如果程式只需要監聽1個埠的話,bossGroup裡面只需要有一個NioEventLoop執行緒就行了。
2、每個NioEventLoop都繫結了一個Selector,所以在Netty的執行緒模型中,是由多個Selecotr在監聽IO就緒事件。而Channel註冊到Selector。
3、一個Channel繫結一個NioEventLoop,相當於一個連線繫結一個執行緒,這個連線所有的ChannelHandler都是在一個執行緒中執行的,避免了多執行緒干擾。更重要的是ChannelPipline連結串列必須嚴格按照順序執行的。單執行緒的設計能夠保證ChannelHandler的順序執行。
4、一個NioEventLoop的selector可以被多個Channel註冊,也就是說多個Channel共享一個EventLoop。EventLoop的Selecctor對這些Channel進行檢查。
5、Netty工作原理
1、server端工作原理
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-APVNgNVr-1607192867696)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-5.png)]
server端啟動時繫結本地某個埠,將自己NioServerSocketChannel註冊到某個boss NioEventLoop的selector上。
server端包含1個boss NioEventLoopGroup和1個worker NioEventLoopGroup,NioEventLoopGroup相當於1個事件迴圈組,這個組裡包含多個事件迴圈NioEventLoop,每個NioEventLoop包含1個selector和1個事件迴圈執行緒。
每個boss NioEventLoop迴圈執行的任務包含3步:
- 輪詢accept事件;
- 處理io任務,即accept事件,與client建立連線,生成NioSocketChannel,並將NioSocketChannel註冊到某個worker NioEventLoop的selector上;
- 處理任務佇列中的任務,runAllTasks。任務佇列中的任務包括使用者呼叫eventloop.execute或schedule執行的任務,或者其它執行緒提交到該eventloop的任務。
每個worker NioEventLoop迴圈執行的任務包含3步:
- 輪詢read、write事件;
- 處理io任務,即read、write事件,在NioSocketChannel可讀、可寫事件發生時進行處理;
- 處理任務佇列中的任務,runAllTasks。
2、client端工作原理
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-4erUiXTY-1607192867696)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-6.png)]
client端啟動時connect到server,建立NioSocketChannel,並註冊到某個NioEventLoop的selector上。
client端只包含1個NioEventLoopGroup,每個NioEventLoop迴圈執行的任務包含3步:
- 輪詢connect、read、write事件;
- 處理io任務,即connect、read、write事件,在NioSocketChannel連線建立、可讀、可寫事件發生時進行處理;
- 處理非io任務,runAllTasks。
6、netty的啟動
1、服務端啟動流程
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
}
});
serverBootstrap.bind(8000);
}
}
- 首先建立了兩個NioEventLoopGroup,這兩個物件可以看做是傳統IO程式設計模型的兩大執行緒組,bossGroup表示監聽埠,accept 新連線的執行緒組,workerGroup表示處理每一條連線的資料讀寫的執行緒組。
- 接下來建立了一個引導類 ServerBootstrap,這個類將引導我們進行服務端的啟動工作,直接new出來開搞。
- 通過.group(bossGroup, workerGroup)給引導類配置兩大執行緒組,這個引導類的執行緒模型也就定型了。
- 然後指定服務端的 IO 模型為NIO,我們通過.channel(NioServerSocketChannel.class)來指定 IO 模型。
- 最後我們呼叫childHandler()方法,給這個引導類建立一個ChannelInitializer,這裡主要就是定義後續每條連線的資料讀寫,業務處理邏輯。ChannelInitializer這個類中,我們注意到有一個泛型引數NioSocketChannel,這個類是 Netty 對 NIO 型別的連線的抽象,而我們前面NioServerSocketChannel也是對 NIO 型別的連線的抽象,NioServerSocketChannel和NioSocketChannel的概念可以和 BIO 程式設計模型中的ServerSocket以及Socket兩個概念對應上
總結:建立一個引導類,然後給他指定執行緒模型,IO模型,連線讀寫處理邏輯,繫結埠之後,服務端就啟動起來了。
2、客戶端啟動流程
對於客戶端的啟動來說,和服務端的啟動類似,依然需要執行緒模型、IO 模型,以及 IO 業務處理邏輯三大引數
public class NettyClient {
public static void main(String[] args) {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
// 1.指定執行緒模型
.group(workerGroup)
// 2.指定 IO 型別為 NIO
.channel(NioSocketChannel.class)
// 3.IO 處理邏輯
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
}
});
// 4.建立連線
bootstrap.connect("juejin.im", 80).addListener(future -> {
if (future.isSuccess()) {
System.out.println("連線成功!");
} else {
System.err.println("連線失敗!");
}
});
}
}
- 首先,與服務端的啟動一樣,需要給它指定執行緒模型,驅動著連線的資料讀寫
- 然後指定 IO 模型為 NioSocketChannel,表示 IO 模型為 NIO
- 接著給引導類指定一個 handler,這裡主要就是定義連線的業務處理邏輯
- 配置完執行緒模型、IO 模型、業務處理邏輯之後,呼叫 connect 方法進行連線,可以看到 connect 方法有兩個引數,第一個引數可以填寫 IP 或者域名,第二個引數填寫的是埠號,由於 connect 方法返回的是一個 Future,也就是說這個方是非同步的,我們通過 addListener 方法可以監聽到連線是否成功,進而列印出連線資訊
總結:建立一個引導類,然後給他指定執行緒模型,IO 模型,連線讀寫處理邏輯,連線上特定主機和埠,客戶端就啟動起來了
7、ByteBuf
ByteBuf是一個節點容器,裡面資料包括三部分:
- 已經丟棄的資料,這部分資料是無效的
- 可讀位元組,這部分資料是ByteBuf的主體
- 可寫位元組
這三段資料被兩個指標給劃分出來,讀指標、寫指標。
ByteBuf 本質上就是,它引用了一段記憶體,這段記憶體可以是堆內也可以是堆外的,然後用引用計數來控制這段記憶體是否需要被釋放,使用讀寫指標來控制對 ByteBuf 的讀寫,可以理解為是外觀模式的一種使用
基於讀寫指標和容量、最大可擴容容量,衍生出一系列的讀寫方法,要注意 read/write 與 get/set 的區別
多個 ByteBuf 可以引用同一段記憶體,通過引用計數來控制記憶體的釋放,遵循誰 retain() 誰 release() 的原則
ByteBuf和ByteBuffer的區別
- 可擴充套件到使用者定義的buffer型別中
- 通過內建的複合buffer型別實現透明的零拷貝(zero-copy)
- 容量可以根據需要擴充套件
- 切換讀寫模式不需要呼叫ByteBuffer.flip()方法
- 讀寫採用不同的索引
- 支援方法連結呼叫
- 支援引用計數
- 支援池技術(比如:執行緒池、資料庫連線池)
ByteBuf和設計模式
1、ByteBufAllocator - 抽象工廠模式
在Netty的世界裡,ByteBuf例項通常應該由ByteBufAllocator來建立。
2、CompositeByteBuf - 組合模式
CompositeByteBuf可以讓我們把多個ByteBuf當成一個大Buf來處理,ByteBufAllocator提供了compositeBuffer()工廠方法來建立CompositeByteBuf。CompositeByteBuf的實現使用了組合模式
3、ByteBufInputStream - 介面卡模式
ByteBufInputStream使用介面卡模式,使我們可以把ByteBuf當做Java的InputStream來使用。同理,ByteBufOutputStream允許我們把ByteBuf當做OutputStream來使用。
4、ReadOnlyByteBuf - 裝飾器模式
ReadOnlyByteBuf用介面卡模式把一個ByteBuf變為只讀,ReadOnlyByteBuf通過呼叫Unpooled.unmodifiableBuffer(ByteBuf)方法獲得:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Fb6hVjH3-1607192867697)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-9.png)]
5、ByteBuf - 工廠方法模式
我們很少需要直接通過建構函式來建立ByteBuf例項,而是通過Allocator來建立。從裝飾器模式可以看出另外一種獲得ByteBuf的方式是呼叫ByteBuf的工廠方法,比如:
- ByteBuf#duplicate()
- ByteBuf#slice()
9、channelHandler
channelHandler在只會對感興趣的事件進行攔截和處理,Servlet的Filter過濾器,負責對IO事件或者IO操作進行攔截和處理,它可以選擇性地攔截和處理自己感興趣的事件,也可以透傳和終止事件的傳遞。
pipeline與channelHandler它們通過責任鏈設計模式來組織程式碼邏輯,並且支援邏輯的動態新增和刪除。
ChannelHandler 有兩大子介面:
- 第一個子介面是 ChannelInboundHandler,從字面意思也可以猜到,他是處理讀資料的邏輯
- 第二個子介面 ChannelOutBoundHandler 是處理寫資料的邏輯
這兩個子介面分別有對應的預設實現,ChannelInboundHandlerAdapter,和 ChanneloutBoundHandlerAdapter,它們分別實現了兩大介面的所有功能,預設情況下會把讀寫事件傳播到下一個 handler。
事件的傳播
AbstractChannel直接呼叫了Pipeline的write()方法,因為write是個outbound事件,所以DefaultChannelPipeline直接找到tail部分的context,呼叫其write()方法:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-wncOQImu-1607192867697)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-10.png)]
context的write()方法沿著context鏈往前找,直至找到一個outbound型別的context為止,然後呼叫其invokeWrite()方法:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-pWrVG2Nk-1607192867698)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-11.png)]
10、NioEventLoop
NioEventLoop除了要處理IO事件,還有主要:
- 非IO操作的系統Task
- 定時任務
非IO操作和IO操作各佔預設值50%,底層使用Selector(多路複用器)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-t5VGCg8V-1607192867698)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-8.png)]
Selector BUG出現的原因
若Selector的輪詢結果為空,也沒有wakeup或新訊息處理,則發生空輪詢,CPU使用率100%,
Netty的解決辦法
- 對Selector的select操作週期進行統計,每完成一次空的select操作進行一次計數,
- 若在某個週期內連續發生N次空輪詢,則觸發了epoll死迴圈bug。
- 重建Selector,判斷是否是其他執行緒發起的重建請求,若不是則將原SocketChannel從舊的Selector上去除註冊,重新註冊到新的Selector上,並將原來的Selector關閉。
11、通訊協議編解碼
通訊協議是為了服務端與客戶端互動,雙方協商出來的滿足一定規則的二進位制格式
- 客戶端把一個Java物件按照通訊協議轉換成二進位制資料包
- 把二進位制資料包傳送到服務端,資料的傳輸油TCP/IP協議負責
- 服務端收到二進位制資料包後,按照通訊協議,包裝成Java物件。
通訊協議的設計
- 魔數,作用:能夠在第一時間識別出這個資料包是不是遵循自定義協議的,也就是無效資料包,為了安全考慮可以直接關閉連線以節省資源。
- 版本號
- 序列化演算法
- 指令
- 資料長度
- 資料
12、Netty記憶體池和物件池
1、記憶體池是指為了實現記憶體池的功能,設計一個記憶體結構Chunk,其內部管理著一個大塊的連續記憶體區域,將這個記憶體區域切分成均等的大小,每一個大小稱之為一個Page。將從記憶體池中申請記憶體的動作對映為從Chunk中申請一定數量Page。為了方便計算和申請Page,Chunk內部採用完全二叉樹的方式對Page進行管理。
2、物件池是指Recycler整個物件池的核心實現由ThreadLocal和Stack及WrakOrderQueue構成,接著來看Stack和WrakOrderQueue的具體實現,最後概括整體實現。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-uPUgxNdN-1607192867699)(E:\學習\寫作\別人的文章\最近可抄\Java-Summarize-master\image/netty-7.png)]
整個設計上核心的幾點:
- Stack相當於是一級快取,同一個執行緒內的使用和回收都將使用一個Stack
- 每個執行緒都會有一個自己對應的Stack,如果回收的執行緒不是Stack的執行緒,將元素放入到Queue中
- 所有的Queue組合成一個連結串列,Stack可以從這些連結串列中回收元素(實現了多執行緒之間共享回收的例項)
13、心跳與空閒檢測
連線假死的現象是:在某一端(服務端或者客戶端)看來,底層的 TCP 連線已經斷開了,但是應用程式並沒有捕獲到,因此會認為這條連線仍然是存在的,從 TCP 層面來說,只有收到四次握手資料包或者一個 RST 資料包,連線的狀態才表示已斷開。
假死導致兩個問題
- 對於服務端,每條連線都會耗費cpu和記憶體資源,大量假死的連線會耗光伺服器的資源
- 對於客戶端,假死會造成傳送資料超時,影響使用者體驗
通常,連線假死由以下幾個原因造成的
- 應用程式出現執行緒堵塞,無法進行資料的讀寫。
- 客戶端或者服務端網路相關的裝置出現故障,比如網路卡,機房故障。
- 公網丟包。公網環境相對內網而言,非常容易出現丟包,網路抖動等現象,如果在一段時間內使用者接入的網路連續出現丟包現象,那麼對客戶端來說資料一直髮送不出去,而服務端也是一直收不到客戶端來的資料,連線就一直耗著
服務端空閒檢測
- 如果能一直收到客戶端發來的資料,那麼可以說明這條連線還是活的,因此,服務端對於連線假死的應對策略就是空閒檢測。
- 簡化一下,我們的服務端只需要檢測一段時間內,是否收到過客戶端發來的資料即可,Netty 自帶的 IdleStateHandler 就可以實現這個功能。
IdleStateHandler 的建構函式有四個引數,其中第一個表示讀空閒時間,指的是在這段時間內如果沒有資料讀到,就表示連線假死;第二個是寫空閒時間,指的是 在這段時間如果沒有寫資料,就表示連線假死;第三個引數是讀寫空閒時間,表示在這段時間內如果沒有產生資料讀或者寫,就表示連線假死。寫空閒和讀寫空閒為0,表示我們不關心者兩類條件;最後一個參數列示時間單位。在我們的例子中,表示的是:如果 15 秒內沒有讀到資料,就表示連線假死。
在一段時間之內沒有讀到客戶端的資料,是否一定能判斷連線假死呢?並不能為了防止服務端誤判,我們還需要在客戶端做點什麼。
客戶端定時心跳
服務端在一段時間內沒有收到客戶端的資料有兩種情況
- 連線假死
- 非假死確實沒資料發
所以我們要排除第二種情況就能保證連線自然就是假死的,定期傳送心跳到服務端
實現了每隔 5 秒,向服務端傳送一個心跳資料包,這個時間段通常要比服務端的空閒檢測時間的一半要短一些,我們這裡直接定義為空閒檢測時間的三分之一,主要是為了排除公網偶發的秒級抖動。
為了排除是否是因為服務端在非假死狀態下確實沒有傳送資料,服務端也要定期傳送心跳給客戶端。
14、拆包粘包理論與解決
TCP是個“流”協議,所謂流,就是沒有界限的一串資料。TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是所謂的TCP粘包和拆包的問題。
解決方法
- 解決思路是在封裝自己的包協議:包=包內容長度(4byte)+包內容
- 對於粘包問題先讀出包頭即包體長度n,然後再讀取長度為n的包內容,這樣資料包之間的邊界就清楚了。
- 對於斷包問題先讀出包頭即包體長度n,由於此次讀取的快取區長度小於n,這時候就需要先快取這部分的內容,等待下次read事件來時拼接起來形成完整的資料包。
15、Netty 自帶的拆包器
1、固定長度的拆包器 FixedLengthFrameDecoder
如果你的應用層協議非常簡單,每個資料包的長度都是固定的,比如 100,那麼只需要把這個拆包器加到 pipeline 中,Netty 會把一個個長度為 100 的資料包 (ByteBuf) 傳遞到下一個 channelHandler。
2、行拆包器 LineBasedFrameDecoder
從字面意思來看,傳送端傳送資料包的時候,每個資料包之間以換行符作為分隔,接收端通過 LineBasedFrameDecoder 將粘過的 ByteBuf 拆分成一個個完整的應用層資料包。
3、分隔符拆包器 DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不過我們可以自定義分隔符。
4、基於長度域拆包器 LengthFieldBasedFrameDecoder
這種拆包器是最通用的一種拆包器,只要你的自定義協議中包含長度域欄位,均可以使用這個拆包器來實現應用層拆包。
16、預留問題
預設情況下,Netty服務端啟動多少個執行緒?何時啟動?
Netty是如何解決jdk空輪詢bug?
Netty如何保證非同步序列無鎖?
Netty是在哪裡檢測有新連線接入的?
答:Boss執行緒通過服務端Channel繫結的Selector輪詢OP_ACCEPT事件,通過JDK底層Channel的accept()方法獲取JDK底層SocketChannel建立新連線
新連線是怎樣註冊到NioEventLoop執行緒的?
答:Worker執行緒呼叫Chooser的next()方法選擇獲取NioEventLoop繫結到客戶端Channel,使用doRegister()方法將新連線註冊到NioEventLoop的Selector上面
Netty是如何判斷ChannelHandler型別的?
對於ChannelHandler的新增應遵循什麼順序?
使用者手動觸發事件傳播,不同觸發方式的區別?
Netty記憶體類別有哪些?
如何減少多執行緒記憶體分配之間的競爭?
不同大小的記憶體是如何進行分配的?
解碼器抽象的解碼過程是什麼樣的?
Netty裡面有哪些拆箱即用的解碼器?
如何把物件變成位元組流,最終寫到Socket底層?
Netty記憶體洩漏問題怎麼解決?
相關文章
- 萬字長文帶你深入理解Kafka!為春招面試做好準備!Kafka面試
- 萬字常人帶你深入理解Zookeeper!為你的春招做好準備!
- 一文理解Tomcat!為你的春招做好準備!Tomcat
- 一文了解Solr框架!為你的春招做好準備!Solr框架
- 帶你深入理解和解剖 synchronizedsynchronized
- 【長文】帶你搞明白RedisRedis
- 帶你深入理解傳遞引數
- 大飛帶你深入理解Tomcat(七)Tomcat
- 大飛帶你深入理解Tomcat(二)Tomcat
- 深入理解JVM垃圾收集機制,下次面試你準備好了嗎JVM面試
- 帶你深入理解 Flutter 中的字型“冷”知識Flutter
- 帶你深入理解Android中的自定義屬性!!!Android
- 新手程式設計師該如何準備面試?【備戰春招/秋招系列】程式設計師面試
- 【備戰春招/秋招系列】初出茅廬的程式設計師該如何準備面試?程式設計師面試
- 為三角學做好準備
- 為限制和連續做好準備
- 五萬字長文帶你學會SpringSpring
- 一文帶你理解透MyBatis原始碼MyBatis原始碼
- 老司機帶你深入理解 Laravel 之 FacadeLaravel
- 帶你深入理解圖靈機--什麼是圖靈機、圖靈完備圖靈
- 數字化“長征”---CIO你準備好了嗎?
- 一文帶你徹底理解 JavaScript 原型物件JavaScript原型物件
- 擁抱數字化積極轉型,你做好準備了嗎?
- 帶你通俗理解httpsHTTP
- 用例項帶你深入理解Java記憶體模型Java記憶體模型
- 阿里大佬帶你,深入理解執行緒池底層原理阿里執行緒
- 萬字長文,帶你輕鬆學習 SparkSpark
- 一文帶你深入瞭解 Redis 的持久化方式及其原理Redis持久化
- 帶你通透Netty原理之架構解析Netty架構
- 秋招還有 1 個月到達戰場,請做好準備 !
- 帶你理解Lock鎖原理
- [譯]帶你理解 Async/awaitAI
- 大學生想進入IT行業,這7項準備你做好了嗎?行業
- 老當機帶你深入理解 Laravel 之驗證器下Laravel
- 玩轉Redis-老闆帶你深入理解分散式鎖Redis分散式
- 從JAVA記憶體到垃圾回收,帶你深入理解JVMJava記憶體JVM
- 重溫Java泛型,帶你更深入地理解它,更好的使用它!Java泛型
- 專為Python初學者準備的IDE你用過嗎?PythonIDE