Netty 原始碼分析系列(二)Netty 架構設計

初念初戀發表於2021-08-06

前言

上一篇文章,我們對 Netty做了一個基本的概述,知道什麼是Netty以及Netty的簡單應用。

Netty 原始碼分析系列(一)Netty 概述

本篇文章我們就來說說Netty的架構設計,解密高併發之道。學習一個框架之前,我們首先要弄懂它的設計原理,然後再進行深層次的分析。

接下來我們從三個方面來分析 Netty 的架構設計。

Selector 模型

Java NIO 是基於 Selector 模型來實現非阻塞的 I/O。Netty 底層是基於 Java NIO 實現的,因此也使用了 Selector 模型。

Selector 模型解決了傳統的阻塞 I/O 程式設計一個客戶端一個執行緒的問題。Selector 提供了一種機制,用於監視一個或多個 NIO 通道,並識別何時可以使用一個或多個 NIO 通道進行資料傳輸。這樣,一個執行緒就可以管理多個通道,從而管理多個網路連線。

image-20210804231340262

Selector 提供了選擇執行已經就緒的任務的能力。從底層來看,Selector 會輪詢 Channel 是否已經準備好執行每個 I/O 操作。Selector 允許單執行緒處理多個 Channel 。Selector 是一種多路複用的技術。

SelectableChannel

並不是所有的 Channel 都是可以被 Selector 複用的,只有抽象類 SelectableChannel的子類才能被 Selector 複用。

例如,FileChannel 就不能被選擇器複用,因為 FileChannel 不是SelectableChannel的子類。

為了與 Selector 一起使用,SelectableChannel必須首先通過register方法來註冊此類的例項。此方法返回一個新的SelectionKey物件,該物件表示Channel已經在Selector進行了註冊。向Selector註冊後,Channel將保持註冊狀態,直到登出為止。

一個 Channel 最多可以使用任何一個特定的 Selector 註冊一次,但是相同的 Channel 可以註冊到多個 Selector 上。可以通過呼叫 isRegistered方法來確定是否向一個或多個 Selector 註冊了 Channel。

SelectableChannel可以安全的供多個併發執行緒使用。

Channel 註冊到 Selector

使用 SelectableChannelregister方法,可將Channel註冊到Selector。方法介面原始碼如下:

    public final SelectionKey register(Selector sel, int ops)
        throws ClosedChannelException
    {
        return register(sel, ops, null);
    }
    
    public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;

其中各選項說明如下:

  • sel:指定 Channel 要註冊的 Selector
  • ops : 指定 Selector需要查詢的通道的操作。

一個Channel在Selector註冊其代表的是一個SelectionKey事件,SelectionKey的型別包括:

  • OP_READ:可讀事件;值為:1<<0
  • OP_WRITE:可寫事件;值為:1<<2
  • OP_CONNECT:客戶端連線服務端的事件(tcp連線),一般為建立SocketChannel客戶端channel;值為:1<<3
  • OP_ACCEPT:服務端接收客戶端連線的事件,一般為建立ServerSocketChannel服務端channel;值為:1<<4

具體的註冊程式碼如下:

 // 1.建立通道管理器(Selector)
 Selector selector = Selector.open();
 
 // 2.建立通道ServerSocketChannel
 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 
 // 3.channel要註冊到Selector上就必須是非阻塞的,所以FileChannel是不可以使用Selector的,因為FileChannel是阻塞的
 serverSocketChannel.configureBlocking(false);
 
 // 4.第二個引數指定了我們對 Channel 的什麼型別的事件感興趣
 SelectionKey key = serverSocketChannel.register(selector , SelectionKey.OP_READ);
 
 // 也可以使用或運算|來組合多個事件,例如
 SelectionKey key = serverSocketChannel.register(selector , SelectionKey.OP_READ | SelectionKey.OP_WRITE);

值得注意的是:一個 Channel 僅僅可以被註冊到一個 Selector 一次, 如果將 Channel 註冊到 Selector 多次, 那麼其實就是相當於更新 SelectionKey interest set

SelectionKey

ChannelSelector 關係確定後之後,並且一旦 Channel 處於某種就緒狀態,就可以被選擇器查詢到。這個工作再呼叫 Selectorselect 方法完成。select 方法的作用,就是對感興趣的通道操作進行就緒狀態的查詢。

// 當註冊事件到達時,方法返回,否則該方法會一直阻塞
selector.select();

SelectionKey 包含了 interest 集合,代表了所選擇的感興趣的事件集合。可以通過 SelectionKey 讀寫 interest 集合,例如:

// 返回當前感興趣的事件列表
int interestSet = key.interestOps();

// 也可通過interestSet判斷其中包含的事件
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;    

// 可以通過interestOps(int ops)方法修改事件列表
key.interestOps(interestSet | SelectionKey.OP_WRITE);

可以看到,用位與操作 interest 集合和給定的 SelectionKey 常量,可以確定某個確定的事件是否在 interest 集合中。

SelectionKey 包含了ready集合。ready 集合是通道已經準備就緒的操作的集合。在一次選擇之後,會首先訪問這個 ready 集合。可以這樣訪問 ready 集合:

int readySet = key.readyOps();

// 也可通過四個方法來分別判斷不同事件是否就緒
key.isReadable();    //讀事件是否就緒
key.isWritable();    //寫事件是否就緒
key.isConnectable(); //客戶端連線事件是否就緒
key.isAcceptable();  //服務端連線事件是否就緒

我們可以通過SelectionKey來獲取當前的channelselector

//返回當前事件關聯的通道,可轉換的選項包括:`ServerSocketChannel`和`SocketChannel`
Channel channel = key.channel();

//返回當前事件所關聯的Selector物件
Selector selector = key.selector();

可以將一個物件或者其他資訊附著到 SelectionKey 上,這樣就能方便地識別某個特定的通道。

key.attach(theObject);
Object attachedObj = key.attachment();

還可以在用 register() 方法向 Selector 註冊 Channel 的時候附加物件。

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

遍歷 SelectionKey

一旦呼叫了 select 方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過呼叫 selector selectedKey()方法,訪問 SelectionKey 集合中的就緒通道,如下所示:

Set<SelectionKey> selectionKeys = selector.selectedKeys();

可以遍歷這個已選擇的鍵集合來訪問就緒的通道,程式碼如下:

// 獲取監聽事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代處理
while (iterator.hasNext()) {
    // 獲取事件
    SelectionKey key = iterator.next();
    // 移除事件,避免重複處理
    iterator.remove();
    // 可連線
    if (key.isAcceptable()) {
        ...
    } 
    // 可讀
    if (key.isReadable()) {
        ...
    }
    //可寫
    if(key.isWritable()){
    	...                
    }
}

事件驅動

Netty是一款非同步的事件驅動的網路應用程式框架。在 Netty 中,事件是指對某些操作感興趣的事。例如,在某個Channel註冊了 OP_READ,說明該 Channel 對讀感興趣,當 Channel 中有可讀的資料時,它會得到一個事件的通知。

Netty 事件驅動模型中包括以下核心元件。

Channel

Channel(管道)是 Java NIO 的一個基本抽象,代表了一個連線到如硬體裝置、檔案、網路 socket 等實體的開放連線,或者是一個能夠完成一種或多種不同的I/O 操作的程式。

回撥

回撥 就是一個方法,一個指向已經被提供給另外一個方法的方法的引用。這使得後者可以在適當的時候呼叫前者,Netty 在內部使用了回撥來處理事件;當一個回撥被觸發時,相關的事件可以被一個ChannelHandler介面處理。

例如:在上一篇文章中,Netty 開發的服務端的管道處理器程式碼中,當Channel中有可讀的訊息時,NettyServerHandler的回撥方法channelRead就會被呼叫。

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    //讀取資料實際(這裡我們可以讀取客戶端傳送的訊息)
    @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 exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

Future

Future 可以看作是一個非同步操作的結果的佔位符;它將在未來的某個時刻完成,並提供對其結果的訪問,Netty 提供了 ChannelFuture 用於在非同步操作的時候使用,每個 Netty 的出站 I/O 操作都將返回一個 ChannelFuture(完全是非同步和事件驅動的)。

以下是一個 ChannelFutureListener使用的示例。

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ChannelFuture future = ctx.channel().close();
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                //..
            }
        });
    }

事件及處理器

在 Netty 中事件按照出/入站資料流進行分類:

入站資料或相關狀態更改觸發的事件包括:

  • 連線已被啟用或者失活。
  • 資料讀取。
  • 使用者事件。
  • 錯誤事件,

出站事件是未來將會出發的某個動作的操作結果:

  • 開啟或者關閉到遠端節點的連線。
  • 將資料寫或者沖刷到套接字。

每個事件都可以被分發給ChannelHandler類中的某個使用者實現的方法。如下圖展示了一個事件是如何被一個這樣的ChannelHandler鏈所處理的。

image-20210805153230027

ChannelHandler 為處理器提供了基本的抽象,可理解為一種為了響應特定事件而被執行的回撥。

責任鏈模式

責任鏈模式(Chain of Responsibility Pattern)是一種行為型設計模式,它為請求建立了一個處理物件的鏈。其鏈中每一個節點都看作是一個物件,每個節點處理的請求均不同,且內部自動維護一個下一節點物件。當一個請求從鏈式的首端發出時,會沿著鏈的路徑依次傳遞給每一個節點物件,直至有物件處理這個請求為止。

責任鏈模式的重點在這個 "鏈"上,由一條鏈去處理相似的請求,在鏈中決定誰來處理這個請求,並返回相應的結果。在Netty中,定義了ChannelPipeline介面用於對責任鏈的抽象。

責任鏈模式會定義一個抽象處理器(Handler)角色,該角色對請求進行抽象,並定義一個方法來設定和返回對下一個處理器的引用。在Netty中,定義了ChannelHandler介面承擔該角色。

責任鏈模式的優缺點

優點:

  • 傳送者不需要知道自己傳送的這個請求到底會被哪個物件處理掉,實現了傳送者和接受者的解耦。
  • 簡化了傳送者物件的設計。
  • 可以動態的新增節點和刪除節點。

缺點:

  • 所有的請求都從鏈的頭部開始遍歷,對效能有損耗。
  • 不方便除錯。由於該模式採用了類似遞迴的方式,除錯的時候邏輯比較複雜。

使用場景:

  • 一個請求需要一系列的處理工作。
  • 業務流的處理,例如檔案審批。
  • 對系統進行擴充套件補充。

ChannelPipeline

Netty 的ChannelPipeline設計,就採用了責任鏈設計模式, 底層採用雙向連結串列的資料結構,,將鏈上的各個處理器串聯起來。

客戶端每一個請求的到來,Netty都認為,ChannelPipeline中的所有的處理器都有機會處理它,因此,對於入棧的請求,全部從頭節點開始往後傳播,一直傳播到尾節點(來到尾節點的msg會被釋放掉)。

入站事件:通常指 IO 執行緒生成了入站資料(通俗理解:從 socket 底層自己往上冒上來的事件都是入站)。
比如EventLoop收到selectorOP_READ事件,入站處理器呼叫socketChannel.read(ByteBuffer)接受到資料後,這將導致通道的ChannelPipeline中包含的下一個中的channelRead方法被呼叫。

出站事件:通常指 IO 執行緒執行實際的輸出操作(通俗理解:想主動往 socket 底層操作的事件的都是出站)。
比如bind方法用意時請求server socket繫結到給定的SocketAddress,這將導致通道的ChannelPipeline中包含的下一個出站處理器中的bind方法被呼叫。

將事件傳遞給下一個處理器

處理器必須呼叫ChannelHandlerContext中的事件傳播方法,將事件傳遞給下一個處理器。

入站事件和出站事件的傳播方法如下圖所示:

以下示例說明了事件傳播通常是如何完成的:

public class MyInboundHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Connected!");
        ctx.fireChannelActive();
    }
}

public class MyOutboundHandler extends ChannelOutboundHandlerAdapter {

    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        System.out.println("Closing...");
        ctx.close(promise);
    }
}

總結

正是由於 Netty 的分層架構設計非常合理,基於 Netty 的各種應用伺服器和協議棧開發才能夠如雨後春筍般得到快速發展。

結尾

我是一個正在被打擊還在努力前進的碼農。如果文章對你有幫助,記得點贊、關注喲,謝謝!

相關文章