萬字長文帶你深入理解netty!為你的春招做好準備!

Hi丶ImViper發表於2020-12-06

Table of Contents generated with DocToc

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,但是存在一系列問題,主要如下:

  1. NIO 的類庫和 API 繁雜,使用麻煩:你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  2. 需要具備其他的額外技能做鋪墊:例如熟悉 Java 多執行緒程式設計,因為 NIO 程式設計涉及到 Reactor 模式,你必須對多執行緒和網路程式設計非常熟悉,才能編寫出高質量的 NIO 程式。
  3. 可靠效能力補齊,開發工作量和難度都非常大:例如客戶端面臨斷連重連、網路閃斷、半包讀寫、失敗快取、網路擁塞和異常碼流的處理等等。NIO 程式設計的特點是功能開發相對容易,但是可靠效能力補齊工作量和難度都非常大。
  4. 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、高效能

  1. 採用非同步非阻塞的IO類庫,基於Reactor模式實現,解決了傳統同步阻塞IO模式
  2. TCP接收和傳送緩衝區使用直接記憶體代替堆記憶體,避免了記憶體複製,提升了IO讀取和寫入的效能
  3. 支援記憶體池的方式迴圈利用ByteBuf,避免了頻繁外掛和銷燬ByteBuf帶來的效能消耗
  4. 可配置的IO執行緒數、TCP引數等,為不同的使用者場景提供定製化的調優引數,滿足不同的效能場景
  5. 採用環形陣列緩衝區實現無鎖化併發程式設計,代替傳統的執行緒安全或鎖。
  6. 合理使用執行緒安全容器,原子類,提升系統的併發處理能力
  7. 關鍵資源的處理使用單執行緒序列化的方式,避免多執行緒併發訪問帶來的鎖競爭和cpu資源消耗
  8. 通過引用計數法及時地申請釋放不再被引用的物件,細粒度的記憶體管理降低了GC的頻率,減少了頻繁GC帶來的時延增大和CPU損耗

2、可靠性

1、 鏈路有效監測(心跳和空閒檢測)

  • 讀空閒超時機制
  • 寫空閒超時機制

2、記憶體保護機制

  • 通過物件引用計數法對Netty的ByteBuf等內建物件進行細粒度的記憶體申請和釋放,對非法的物件引用進行檢測和保護
  • 通過記憶體池來重用ByteBuf,節省記憶體
  • 可設定的記憶體容量上限,包括ByteBuf、執行緒池執行緒數
    3、優雅停機
  • 優雅停機需要設定最大超時時間,如果達到該時間系統還沒退出,則通過Kill -9 pid強殺當前執行緒。
  • JVM通過註冊的Shutdown Hook攔截到退出訊號量,然後執行退出操作

3、可定製性

  1. 責任鏈模式:channelPipeline基於責任鏈模式開發,便於業務邏輯的攔截、定製和擴充套件
  2. 基於介面的開發:關鍵的類庫都提供了介面或者抽象類,使用者可以自定義實現相關介面
  3. 提供了大量工廠類,通過過載這些工廠類可以按需建立出使用者實現的物件
  4. 提供大量的系統引數供使用者按需設定,增強系統的場景定製

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步:

  1. 輪詢accept事件;
  2. 處理io任務,即accept事件,與client建立連線,生成NioSocketChannel,並將NioSocketChannel註冊到某個worker NioEventLoop的selector上;
  3. 處理任務佇列中的任務,runAllTasks。任務佇列中的任務包括使用者呼叫eventloop.execute或schedule執行的任務,或者其它執行緒提交到該eventloop的任務。

每個worker NioEventLoop迴圈執行的任務包含3步:

  1. 輪詢read、write事件;
  2. 處理io任務,即read、write事件,在NioSocketChannel可讀、可寫事件發生時進行處理;
  3. 處理任務佇列中的任務,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步:

  1. 輪詢connect、read、write事件;
  2. 處理io任務,即connect、read、write事件,在NioSocketChannel連線建立、可讀、可寫事件發生時進行處理;
  3. 處理非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);
    }
}
  1. 首先建立了兩個NioEventLoopGroup,這兩個物件可以看做是傳統IO程式設計模型的兩大執行緒組,bossGroup表示監聽埠,accept 新連線的執行緒組,workerGroup表示處理每一條連線的資料讀寫的執行緒組。
  2. 接下來建立了一個引導類 ServerBootstrap,這個類將引導我們進行服務端的啟動工作,直接new出來開搞。
  3. 通過.group(bossGroup, workerGroup)給引導類配置兩大執行緒組,這個引導類的執行緒模型也就定型了。
  4. 然後指定服務端的 IO 模型為NIO,我們通過.channel(NioServerSocketChannel.class)來指定 IO 模型。
  5. 最後我們呼叫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("連線失敗!");
            }

        });
    }
}
  1. 首先,與服務端的啟動一樣,需要給它指定執行緒模型,驅動著連線的資料讀寫
  2. 然後指定 IO 模型為 NioSocketChannel,表示 IO 模型為 NIO
  3. 接著給引導類指定一個 handler,這裡主要就是定義連線的業務處理邏輯
  4. 配置完執行緒模型、IO 模型、業務處理邏輯之後,呼叫 connect 方法進行連線,可以看到 connect 方法有兩個引數,第一個引數可以填寫 IP 或者域名,第二個引數填寫的是埠號,由於 connect 方法返回的是一個 Future,也就是說這個方是非同步的,我們通過 addListener 方法可以監聽到連線是否成功,進而列印出連線資訊

總結:建立一個引導類,然後給他指定執行緒模型,IO 模型,連線讀寫處理邏輯,連線上特定主機和埠,客戶端就啟動起來了

7、ByteBuf

ByteBuf是一個節點容器,裡面資料包括三部分:

  1. 已經丟棄的資料,這部分資料是無效的
  2. 可讀位元組,這部分資料是ByteBuf的主體
  3. 可寫位元組
    這三段資料被兩個指標給劃分出來,讀指標、寫指標。

ByteBuf 本質上就是,它引用了一段記憶體,這段記憶體可以是堆內也可以是堆外的,然後用引用計數來控制這段記憶體是否需要被釋放,使用讀寫指標來控制對 ByteBuf 的讀寫,可以理解為是外觀模式的一種使用

基於讀寫指標和容量、最大可擴容容量,衍生出一系列的讀寫方法,要注意 read/write 與 get/set 的區別

多個 ByteBuf 可以引用同一段記憶體,通過引用計數來控制記憶體的釋放,遵循誰 retain() 誰 release() 的原則

ByteBuf和ByteBuffer的區別

  1. 可擴充套件到使用者定義的buffer型別中
  2. 通過內建的複合buffer型別實現透明的零拷貝(zero-copy)
  3. 容量可以根據需要擴充套件
  4. 切換讀寫模式不需要呼叫ByteBuffer.flip()方法
  5. 讀寫採用不同的索引
  6. 支援方法連結呼叫
  7. 支援引用計數
  8. 支援池技術(比如:執行緒池、資料庫連線池)

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事件,還有主要:

  1. 非IO操作的系統Task
  2. 定時任務
    非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、通訊協議編解碼

通訊協議是為了服務端與客戶端互動,雙方協商出來的滿足一定規則的二進位制格式

  1. 客戶端把一個Java物件按照通訊協議轉換成二進位制資料包
  2. 把二進位制資料包傳送到服務端,資料的傳輸油TCP/IP協議負責
  3. 服務端收到二進位制資料包後,按照通訊協議,包裝成Java物件。

通訊協議的設計

  1. 魔數,作用:能夠在第一時間識別出這個資料包是不是遵循自定義協議的,也就是無效資料包,為了安全考慮可以直接關閉連線以節省資源。
  2. 版本號
  3. 序列化演算法
  4. 指令
  5. 資料長度
  6. 資料

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)]
整個設計上核心的幾點:

  1. Stack相當於是一級快取,同一個執行緒內的使用和回收都將使用一個Stack
  2. 每個執行緒都會有一個自己對應的Stack,如果回收的執行緒不是Stack的執行緒,將元素放入到Queue中
  3. 所有的Queue組合成一個連結串列,Stack可以從這些連結串列中回收元素(實現了多執行緒之間共享回收的例項)

13、心跳與空閒檢測

連線假死的現象是:在某一端(服務端或者客戶端)看來,底層的 TCP 連線已經斷開了,但是應用程式並沒有捕獲到,因此會認為這條連線仍然是存在的,從 TCP 層面來說,只有收到四次握手資料包或者一個 RST 資料包,連線的狀態才表示已斷開。

假死導致兩個問題

  1. 對於服務端,每條連線都會耗費cpu和記憶體資源,大量假死的連線會耗光伺服器的資源
  2. 對於客戶端,假死會造成傳送資料超時,影響使用者體驗

通常,連線假死由以下幾個原因造成的

  1. 應用程式出現執行緒堵塞,無法進行資料的讀寫。
  2. 客戶端或者服務端網路相關的裝置出現故障,比如網路卡,機房故障。
  3. 公網丟包。公網環境相對內網而言,非常容易出現丟包,網路抖動等現象,如果在一段時間內使用者接入的網路連續出現丟包現象,那麼對客戶端來說資料一直髮送不出去,而服務端也是一直收不到客戶端來的資料,連線就一直耗著

服務端空閒檢測

  1. 如果能一直收到客戶端發來的資料,那麼可以說明這條連線還是活的,因此,服務端對於連線假死的應對策略就是空閒檢測。
  2. 簡化一下,我們的服務端只需要檢測一段時間內,是否收到過客戶端發來的資料即可,Netty 自帶的 IdleStateHandler 就可以實現這個功能。

IdleStateHandler 的建構函式有四個引數,其中第一個表示讀空閒時間,指的是在這段時間內如果沒有資料讀到,就表示連線假死;第二個是寫空閒時間,指的是 在這段時間如果沒有寫資料,就表示連線假死;第三個引數是讀寫空閒時間,表示在這段時間內如果沒有產生資料讀或者寫,就表示連線假死。寫空閒和讀寫空閒為0,表示我們不關心者兩類條件;最後一個參數列示時間單位。在我們的例子中,表示的是:如果 15 秒內沒有讀到資料,就表示連線假死。

在一段時間之內沒有讀到客戶端的資料,是否一定能判斷連線假死呢?並不能為了防止服務端誤判,我們還需要在客戶端做點什麼。

客戶端定時心跳

服務端在一段時間內沒有收到客戶端的資料有兩種情況

  1. 連線假死
  2. 非假死確實沒資料發

所以我們要排除第二種情況就能保證連線自然就是假死的,定期傳送心跳到服務端

實現了每隔 5 秒,向服務端傳送一個心跳資料包,這個時間段通常要比服務端的空閒檢測時間的一半要短一些,我們這裡直接定義為空閒檢測時間的三分之一,主要是為了排除公網偶發的秒級抖動。

為了排除是否是因為服務端在非假死狀態下確實沒有傳送資料,服務端也要定期傳送心跳給客戶端。

14、拆包粘包理論與解決

TCP是個“流”協議,所謂流,就是沒有界限的一串資料。TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是所謂的TCP粘包和拆包的問題。

解決方法

  1. 解決思路是在封裝自己的包協議:包=包內容長度(4byte)+包內容
  2. 對於粘包問題先讀出包頭即包體長度n,然後再讀取長度為n的包內容,這樣資料包之間的邊界就清楚了。
  3. 對於斷包問題先讀出包頭即包體長度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記憶體洩漏問題怎麼解決?

相關文章