1. 程式人生 > >Netty學習筆記(一):基礎理論+核心元件

Netty學習筆記(一):基礎理論+核心元件

前置知識

I/O模型

用什麼樣的通道將資料傳送給對方,BIO、NIO或者AIO,I/O模型在很大程度上決定了框架的效能

阻塞I/O

傳統阻塞型I/O(BIO)可以用下圖表示:

特點

  • 每個請求都需要獨立的執行緒完成資料read,業務處理,資料write的完整操作

問題

  • 當併發數較大時,需要建立大量執行緒來處理連線,系統資源佔用較大
  • 連線建立後,如果當前執行緒暫時沒有資料可讀,則執行緒就阻塞在read操作上,造成執行緒資源浪費

I/O複用模型

在I/O複用模型中,會用到select,這個函式也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函式可以同時阻塞多個I/O操作,而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時,才真正呼叫I/O操作函式

Netty的非阻塞I/O的實現關鍵是基於I/O複用模型,這裡用Selector物件表示:

Netty的IO執行緒NioEventLoop由於聚合了多路複用器Selector,可以同時併發處理成百上千個客戶端連線。當執行緒從某客戶端Socket通道進行讀寫資料時,若沒有資料可用時,該執行緒可以進行其他任務。執行緒通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的執行緒可以管理多個輸入和輸出通道。

由於讀寫操作都是非阻塞的,這就可以充分提升IO執行緒的執行效率,避免由於頻繁I/O阻塞導致的執行緒掛起,一個I/O執行緒可以併發處理N個客戶端連線和讀寫操作,這從根本上解決了傳統同步阻塞I/O一連線一執行緒模型,架構的效能、彈性伸縮能力和可靠性都得到了極大的提升。

基於buffer

傳統的I/O是面向位元組流或字元流的,以流式的方式順序地從一個Stream 中讀取一個或多個位元組, 因此也就不能隨意改變讀取指標的位置。

在NIO中, 拋棄了傳統的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能從Channel中讀取資料到Buffer中或將資料 Buffer 中寫入到 Channel。

基於buffer操作不像傳統IO的順序操作, NIO 中可以隨意地讀取任意位置的資料

執行緒模型

資料報如何讀取?讀取之後的編解碼在哪個執行緒進行,編解碼後的訊息如何派發,執行緒模型的不同,對效能的影響也非常大。

事件驅動模型

通常,我們設計一個事件處理模型的程式有兩種思路

  • 輪詢方式 執行緒不斷輪詢訪問相關事件發生源有沒有發生事件,有發生事件就呼叫事件處理邏輯。
  • 事件驅動方式 發生事件,主執行緒把事件放入事件佇列,在另外執行緒不斷迴圈消費事件列表中的事件,呼叫事件對應的處理邏輯處理事件。事件驅動方式也被稱為訊息通知方式,其實是設計模式中觀察者模式的思路。

以GUI的邏輯處理為例,說明兩種邏輯的不同:

  • 輪詢方式 執行緒不斷輪詢是否發生按鈕點選事件,如果發生,呼叫處理邏輯
  • 事件驅動方式 發生點選事件把事件放入事件佇列,在另外執行緒消費的事件列表中的事件,根據事件型別呼叫相關事件處理邏輯

這裡借用O'Reilly 大神關於事件驅動模型解釋圖

主要包括4個基本元件:

  • 事件佇列(event queue):接收事件的入口,儲存待處理事件
  • 分發器(event mediator):將不同的事件分發到不同的業務邏輯單元
  • 事件通道(event channel):分發器與處理器之間的聯絡渠道
  • 事件處理器(event processor):實現業務邏輯,處理完成後會發出事件,觸發下一步操作

可以看出,相對傳統輪詢模式,事件驅動有如下優點:

  • 可擴充套件性好,分散式的非同步架構,事件處理器之間高度解耦,可以方便擴充套件事件處理邏輯
  • 高效能,基於佇列暫存事件,能方便並行非同步處理事件

Reactor執行緒模型

Reactor是反應堆的意思,Reactor模型,是指通過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。 服務端程式處理傳入多路請求,並將它們同步分派給請求對應的處理執行緒,Reactor模式也叫Dispatcher模式,即I/O多了複用統一監聽事件,收到事件後分發(Dispatch給某程序),是編寫高效能網路伺服器的必備技術之一。

Reactor模型中有2個關鍵組成:

  • Reactor Reactor在一個單獨的執行緒中執行,負責監聽和分發事件,分發給適當的處理程式來對IO事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯絡人
  • Handlers 處理程式執行I/O事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor通過排程適當的處理程式來響應I/O事件,處理程式執行非阻塞操作

取決於Reactor的數量和Hanndler執行緒數量的不同,Reactor模型有3個變種

  • 單Reactor單執行緒
  • 單Reactor多執行緒
  • 主從Reactor多執行緒

可以這樣理解,Reactor就是一個執行while (true) { selector.select(); ...}迴圈的執行緒,會源源不斷的產生新的事件,稱作反應堆很貼切。

篇幅關係,這裡不再具體展開Reactor特性、優缺點比較,有興趣的讀者可以參考我之前另外一篇文章:《理解高效能網路模型》

Netty執行緒模型

Netty主要基於主從Reactors多執行緒模型(如下圖)做了一定的修改,其中主從Reactor多執行緒模型有多個Reactor:MainReactor和SubReactor:

  • MainReactor負責客戶端的連線請求,並將請求轉交給SubReactor
  • SubReactor負責相應通道的IO讀寫請求
  • 非IO請求(具體邏輯處理)的任務則會直接寫入佇列,等待worker threads進行處理

這裡引用Doug Lee大神的Reactor介紹:Scalable IO in Java裡面關於主從Reactor多執行緒模型的圖

特別說明的是: 雖然Netty的執行緒模型基於主從Reactor多執行緒,借用了MainReactor和SubReactor的結構,但是實際實現上,SubReactor和Worker執行緒在同一個執行緒池中:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)

上面程式碼中的bossGroup 和workerGroup是Bootstrap構造方法中傳入的兩個物件,這兩個group均是執行緒池

  • bossGroup執行緒池則只是在bind某個埠後,獲得其中一個執行緒作為MainReactor,專門處理埠的accept事件,每個埠對應一個boss執行緒
  • workerGroup執行緒池會被各個SubReactor和worker執行緒充分利用

非同步處理

非同步的概念和同步相對。當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果。實際處理這個呼叫的部件在完成後,通過狀態、通知和回撥來通知呼叫者。

Netty中的I/O操作是非同步的,包括bind、write、connect等操作會簡單的返回一個ChannelFuture,呼叫者並不能立刻獲得結果,通過Future-Listener機制,使用者可以方便的主動獲取或者通過通知機制獲得IO操作結果。

當future物件剛剛建立時,處於非完成狀態,呼叫者可以通過返回的ChannelFuture來獲取操作執行的狀態,註冊監聽函式來執行完成後的操,常見有如下操作:

  • 通過isDone方法來判斷當前操作是否完成
  • 通過isSuccess方法來判斷已完成的當前操作是否成功
  • 通過getCause方法來獲取已完成的當前操作失敗的原因
  • 通過isCancelled方法來判斷已完成的當前操作是否被取消
  • 通過addListener方法來註冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;如果future物件已完成,則理解通知指定的監聽器

例如下面的的程式碼中繫結埠是非同步操作,當繫結操作處理完,將會呼叫相應的監聽器處理邏輯

serverBootstrap.bind(port).addListener(future -> {
    if (future.isSuccess()) {
        System.out.println(new Date() + ": 埠[" + port + "]繫結成功!");
    } else {
        System.err.println("埠[" + port + "]繫結失敗!");
    }
});

相比傳統阻塞I/O,執行I/O操作後執行緒會被阻塞住, 直到操作完成;非同步處理的好處是不會造成執行緒阻塞,執行緒在I/O操作期間可以執行別的程式,在高併發情形下會更穩定和更高的吞吐量。

什麼是 Netty?

Netty 是一款提供非同步的、事件驅動的網路應用程式框架和工具,用以快速開發高效能、高可靠性的網路伺服器和客戶端程式

也就是說,Netty 是一個基於 NIO 的客戶、伺服器端程式設計框架,使用 Netty 可以確保你快速和簡單地開發出一個網路應用,例如實現了某種協議的客戶,服務端應用。Netty 相當簡化和流線化了網路應用的程式設計開發過程,例如,TCP 和 UDP 的 socket 服務開發。(以上摘自百度百科)。

Netty具有如下特性(摘自《Netty in Action》)

分類

Netty的特性

設計

統一的API,支援多種傳輸型別,阻塞和非阻塞的 
簡單而強大的執行緒模型
真正的無連線資料報套接字支援
連結邏輯元件以支援複用

易於使用

詳實的 Javadoc 和大量的示例集
不需要超過JdK 1.6+的依賴

效能

擁有比 Java 的核心 API 更高的吞吐量以及更低的延遲
得益於池化和複用,擁有更低的資源消耗
最少的記憶體複製

健壯性

不會因為慢速、快速或者超載的連線而導致 OutOfMemoryError 
消除在高速網路中 NIO 應用程式常見的不公平讀/寫比率

安全性

完整的 SSL/TLS 以及 StartTLs 支援
可用於受限環境下,如 Applet 和 OSGI

社群驅動

釋出快速而且頻繁

概述

在我們開始之前,如果你瞭解Netty程式的一般結構和大致用法(客戶端和伺服器都有一個類似的結構)會更好。

一個Netty程式開始於Bootstrap類,Bootstrap類是Netty提供的一個可以通過簡單配置來設定或"引導"程式的一個很重要的類。Netty中設計了Handlers來處理特定的"event"和設定Netty中的事件從而來處理多個協議和資料事件可以描述成一個非常通用的方法,因為你可以自定義一個handler,用來將Object轉成byte[]或將byte[]轉成Object;也可以定義個handler處理丟擲的異常。

你會經常編寫一個實現ChannelInboundHandler的類,ChannelInboundHandler是用來接收訊息,當有訊息過來時,你可以決定如何處理。當程式需要返回訊息時可以在ChannelInboundHandler裡write/flush資料。可以認為應用程式的業務邏輯都是在ChannelInboundHandler中來處理的,業務羅的生命週期在ChannelInboundHandler中。

Netty連線客戶端端或繫結伺服器需要知道如何傳送或接收訊息,這是通過不同型別的handlers來做的,多個Handlers是怎麼配置的?Netty提供了ChannelInitializer類用來配置Handlers。ChannelInitializer是通過ChannelPipeline來新增ChannelHandler的,如傳送和接收訊息,這些Handlers將確定發的是什麼訊息。ChannelInitializer自身也是一個ChannelHandler,在新增完其他的handlers之後會自動從ChannelPipeline中刪除自己。

所有的Netty程式都是基於ChannelPipeline。ChannelPipeline和EventLoop和EventLoopGroup密切相關,因為它們三個都和事件處理相關,所以這就是為什麼它們處理IO的工作由EventLoop管理的原因。

Netty中所有的IO操作都是非同步執行的,例如你連線一個主機預設是非同步完成的;寫入/傳送訊息也是同樣是非同步。也就是說操作不會直接執行,而是會等一會執行,因為你不知道返回的操作結果是成功還是失敗,但是需要有檢查是否成功的方法或者是註冊監聽來通知;Netty使用Futures和ChannelFutures來達到這種目的。Future註冊一個監聽,當操作成功或失敗時會通知ChannelFuture封裝的是一個操作的相關資訊,操作被執行時會立刻返回ChannelFuture

先上程式碼

先把程式跑起來再說,整體結構

Server.java

public class Server {
    public static void main(String[] args) throws InterruptedException {
        // 建立 ServerBootstrap 物件
        ServerBootstrap bootstrap = new ServerBootstrap();
        // 建立兩個EventLoopGroup物件
        // 建立boss執行緒組 用於服務端接收客戶端的連線
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 建立 worker執行緒組,用於 SocketChannel 的資料讀寫
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 設定使用的EventLoopGroup
            bootstrap.group(bossGroup, workerGroup)
                    // 設定要被例項化的為 NioServerSocketChannel 類
                    .channel(NioServerSocketChannel.class)
                    // 設定 NioServerSocketChannel 的處理器
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 設定連入服務端 Client 的SocketChannel處理器
                    .childHandler(new ServerInitializer());
            // 繫結埠,並同步等待成功,即啟動服務端
            ChannelFuture future = bootstrap.bind(8888);
            future.channel().closeFuture().sync();
        } finally {
            // 優雅關閉兩個 EventLoopGroup 物件
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

ServerInitializer.java

public class ServerInitializer extends ChannelInitializer<SocketChannel> {
    private static final StringDecoder DECODER = new StringDecoder();
    private static final StringEncoder ENCODER = new StringEncoder();

    private static final ServerHandler SERVER_HANDLER = new ServerHandler();

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        // 新增幀限定符來防止粘包現象
        pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        // 解碼和編碼,要和客戶端一致
        pipeline.addLast(DECODER);
        pipeline.addLast(ENCODER);

        // 業務邏輯實現類
        pipeline.addLast(SERVER_HANDLER);
    }
}

ServerHandler.java

@ChannelHandler.Sharable
public class ServerHandler extends SimpleChannelInboundHandler<String> {

    /**
     * 建立連線時,傳送一條慶祝訊息
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 為新連線傳送慶祝
        ctx.write("Welcome to " + InetAddress.getLocalHost().getHostName() + "!\r\n");
        ctx.write("It is " + new Date() + " now.\r\n");
        ctx.flush();
    }


    /**
     * 業務邏輯處理
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        // Generate and write a response.
        String response;
        boolean close = false;
        if (msg.isEmpty()) {
            response = "Please type something.\r\n";
        } else if ("bye".equals(msg.toLowerCase())) {
            response = "Have a good day.\r\n";
            close = true;
        } else {
            System.out.println("接收到訊息:" + msg);
            response = "Did you say '" + msg + "'\r\n";
        }

        ChannelFuture future = ctx.write(response);

        if (close) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    /**
     * 異常處理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Client.java

public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ClientInitializer());
            Channel ch = bootstrap.connect("127.0.0.1", 8888).sync().channel();

            ChannelFuture lastWriteFuture = null;
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            for (; ; ) {
                String line = in.readLine();
                if (line == null) {
                    break;
                }

                // Sends the received line to the server.
                lastWriteFuture = ch.writeAndFlush(line + "\r\n");

                // If user typed the 'bye' command, wait until the server closes
                // the connection.
                if ("bye".equals(line.toLowerCase())) {
                    ch.closeFuture().sync();
                    break;
                }
            }

            // Wait until all message are flushed before closing the channel.
            if (lastWriteFuture != null) {
                lastWriteFuture.sync();
            }
        } finally {
            group.shutdownGracefully();
        }
    }
}

ClientInitializer.java

public class ClientInitializer extends ChannelInitializer<SocketChannel> {
    private static final StringDecoder DECODER = new StringDecoder();
    private static final StringEncoder ENCODER = new StringEncoder();

    private static final ClientHandler CLIENT_HANDLER = new ClientHandler();

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        pipeline.addLast(DECODER);
        pipeline.addLast(ENCODER);

        pipeline.addLast(CLIENT_HANDLER);
    }
}

ClientHandler.java

@ChannelHandler.Sharable
public class ClientHandler extends SimpleChannelInboundHandler<String> {
    /**
     * 列印讀取到的資料
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg);
    }

    /**
     * 異常資料捕獲
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

線執行Server再執行Client,在Client輸入訊息,會收Server回覆的重複的訊息。

Bootstrap

概述

  1. Bootstrap意思是引導,一個Netty應用由一個Bootstrap開始,主要作用是配置整個Netty程式,串聯各個元件。
  2. Netty中Bootstrap類是客戶端程式的啟動引導類,ServerBootstrap是服務端啟動引導類。

繼承關係

主要方法

名稱

描述

group

設定 EventLoopGroup 用於處理所有的 Channel 的事件

channel channelFactory

channel() 指定 Channel 的實現類。如果類沒有提供一個預設的建構函式,你可以呼叫 channelFactory() 來指定一個工廠類被 bind() 呼叫。

localAddress

指定應該繫結到本地地址 Channel。如果不提供,將由作業系統建立一個隨機的。或者,您可以使用 bind() 或 connect()指定localAddress

option

設定 ChannelOption 應用於 新建立 Channel 的 ChannelConfig。這些選項將被 bind 或 connect 設定在通道,這取決於哪個被首先呼叫。這個方法在建立管道後沒有影響。所支援 ChannelOption 取決於使用的管道型別。請參考9.6節和 ChannelConfig 的 API 文件 的 Channel 型別使用。

attr

這些選項將被 bind 或 connect 設定在通道,這取決於哪個被首先呼叫。這個方法在建立管道後沒有影響。請參考9.6節。

handler

設定新增到 ChannelPipeline 中的 ChannelHandler 接收事件通知。

clone

建立一個當前 Bootstrap的克隆擁有原來相同的設定。

remoteAddress

設定遠端地址。此外,您可以通過 connect() 指定

connect

連線到遠端,返回一個 ChannelFuture, 用於通知連線操作完成

bind

將通道繫結並返回一個 ChannelFuture,用於通知繫結操作完成後,必須呼叫 Channel.connect() 來建立連線。

ServerBootstrap

概述

Bootstrap只包含一個EventLoopGroup,而ServerBootstrap包含兩個EventLoopGroup。

一個是隻負責接收指令的bossGroup另一個是負責處理任務的workerGroup

繼承關係

主要方法

名稱

描述

group

設定 EventLoopGroup 用於 ServerBootstrap。這個 EventLoopGroup 提供 ServerChannel 的 I/O 並且接收 Channel

channel channelFactory

channel() 指定 Channel 的實現類。如果管道沒有提供一個預設的建構函式,你可以提供一個 ChannelFactory。

localAddress

指定 ServerChannel 例項化的類。如果不提供,將由作業系統建立一個隨機的。或者,您可以使用 bind() 或 connect()指定localAddress

option

指定一個 ChannelOption 來用於新建立的 ServerChannel 的 ChannelConfig 。這些選項將被設定在管道的 bind() 或 connect(),這取決於誰首先被呼叫。在此呼叫這些方法之後設定或更改 ChannelOption 是無效的。所支援 ChannelOption 取決於使用的管道型別。請參考9.6節和 ChannelConfig 的 API 文件 的 Channel 型別使用。

childOption

當管道已被接受,指定一個 ChannelOption 應用於 Channel 的 ChannelConfig。

attr

指定 ServerChannel 的屬性。這些屬性可以被 管道的 bind() 設定。當呼叫 bind() 之後,修改它們不會生效。

childAttr

應用屬性到接收到的管道上。後續呼叫沒有效果。

handler

設定新增到 ServerChannel 的 ChannelPipeline 中的 ChannelHandler。 具體詳見 childHandler() 描述

childHandler

設定新增到接收到的 Channel 的 ChannelPipeline 中的 ChannelHandler。handler() 和 childHandler()之間的區別是前者是接收和處理ServerChannel,同時 childHandler() 新增處理器用於處理和接收 Channel。後者代表一個套接字繫結到一個遠端。

clone

克隆 ServerBootstrap 用於連線到不同的遠端,通過設定相同的原始 ServerBoostrap。

bind

繫結 ServerChannel 並且返回一個 ChannelFuture,用於 通知連線操作完成了(結果可以是成功或者失敗)

Netty服務端基本過程如下:

  1. 初始化建立2個NioEventLoopGroup,其中boosGroup用於Accetpt連線建立事件並分發請求, workerGroup用於處理I/O讀寫事件和業務邏輯
  2. 基於ServerBootstrap(服務端啟動引導類),配置EventLoopGroup、Channel型別,連線引數、配置入站、出站事件handler
  3. 繫結埠,開始工作

結合上面的介紹的Netty Reactor模型,介紹服務端Netty的工作架構圖:

server端包含1個Boss NioEventLoopGroup和1個Worker NioEventLoopGroup,NioEventLoopGroup相當於1個事件迴圈組,這個組裡包含多個事件迴圈NioEventLoop,每個NioEventLoop包含1個selector和1個事件迴圈執行緒。

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

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

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

  1. 輪詢read、write事件;
  2. 處I/O事件,即read、write事件,在NioSocketChannel可讀、可寫事件發生時進行處理
  3. 處理任務佇列中的任務,runAllTasks。

Channel

概述

在傳統的網路程式設計中,作為核心類的 Socket ,它對程式設計師來說並不是那麼友好,直接使用其成本還是稍微高了點。而Netty 的 Channel 則提供的一系列的 API ,它大大降低了直接與 Socket 進行操作的複雜性。而相對於原生 NIO 的 Channel,Netty 的 Channel 具有如下優勢(摘自《Netty權威指南(第二版)》):

在 Channel 介面層,採用 Facade 模式進行統一封裝,將網路 I/O 操作、網路 I/O 相關聯的其他操作封裝起來,統一對外提供。

Channel 介面的定義儘量大而全,為 SocketChannel 和 ServerSocketChannel 提供統一的檢視,由不同子類實現不同的功能,公共功能在抽象父類中實現,最大程度地實現功能和介面的重用。

具體實現採用聚合而非包含的方式,將相關的功能類聚合在 Channel 中,有 Channel 統一負責和排程,功能實現更加靈活。

繼承關係

主要方法

Channel 生命週期

Channel 有個簡單但強大的狀態模型,與 ChannelInboundHandler API 密切相關。下面表格是 Channel 的四個狀態

狀態

描述

channelUnregistered

channel建立但未註冊到一個 EventLoop.

channelRegistered

channel 註冊到一個 EventLoop.

channelActive

channel 的活動的(連線到了它的 remote peer(遠端對等方)),現在可以接收和傳送資料了

channelInactive

channel 沒有連線到 remote peer(遠端對等方)

Channel 的正常的生命週期如下圖,當這些狀態變化出現,對應的事件將會生成,這樣與 ChannelPipeline 中的 ChannelHandler 的互動就能及時響應

ChannelInitializer

概述

Netty連線客戶端端或繫結伺服器需要知道如何傳送或接收訊息,這是通過不同型別的handlers來做的,多個Handlers是怎麼配置的?Netty提供了ChannelInitializer類用來配置Handlers。ChannelInitializer是通過ChannelPipeline來新增ChannelHandler的,如傳送和接收訊息,這些Handlers將確定發的是什麼訊息。ChannelInitializer自身也是一個ChannelHandler,在新增完其他的handlers之後會自動從ChannelPipeline中刪除自己。

繼承關係

主要方法

ChannelFuture

概述

Netty中所有的IO操作都是非同步執行的,例如你連線一個主機預設是非同步完成的;寫入/傳送訊息也是同樣是非同步。也就是說操作不會直接執行,而是會等一會執行,因為你不知道返回的操作結果是成功還是失敗,但是需要有檢查是否成功的方法或者是註冊監聽來通知;Netty使用Futures和ChannelFutures來達到這種目的。Future註冊一個監聽,當操作成功或失敗時會通知

繼承關係

主要方法

ChannelHandler

概述

  1. ChannelHandler是個介面
  2. ChannelHandler 主要用來處理各種事件,這裡的事件很廣泛,比如可以是連線、資料接收、異常、資料轉換等。
  3. ChannelHandler 有兩個核心子類 ChannelInboundHandler 和 ChannelOutboundHandler,其中 ChannelInboundHandler 用於接收、處理入站資料和事件,而 ChannelOutboundHandler 則相反。

主要方法

ChannelHandler本身並沒有提供很多方法,因為這個介面有許多的方法需要實現,方便使用期間,可以繼承它的子類:

  • ChannelInboundHandler用於處理入站I / O事件
  • ChannelOutboundHandler用於處理出站I / O操作

ChannelHandler生命週期

ChannelHandler 定義的生命週期操作如下表,當 ChannelHandler 新增到 ChannelPipeline,或者從 ChannelPipeline 移除後,這些將會呼叫。每個方法都會帶 ChannelHandlerContext 引數

ChannelHandler lifecycle methods

型別

描述

handlerAdded

當 ChannelHandler 新增到 ChannelPipeline 呼叫

handlerRemoved

當 ChannelHandler 從 ChannelPipeline 移除時呼叫

exceptionCaught

當 ChannelPipeline 執行發生錯誤時呼叫

ChannelHandler子介面

Netty 提供2個重要的 ChannelHandler 子介面:

  • ChannelInboundHandler - 處理進站資料,並且所有狀態都更改
  • ChannelOutboundHandler - 處理出站資料,允許攔截各種操作

ChannelInboundHandler

繼承關係與主要方法

ChannelInboundHandler生命週期

ChannelInboundHandler 的生命週期方法在下表中,當接收到資料或者與之關聯的 Channel 狀態改變時呼叫。之前已經注意到了,這些方法與 Channel 的生命週期接近

型別

描述

channelRegistered

Invoked when a Channel is registered to its EventLoop and is able to handle I/O.

channelUnregistered

Invoked when a Channel is deregistered from its EventLoop and cannot handle any I/O.

channelActive

Invoked when a Channel is active; the Channel is connected/bound and ready.

channelInactive

Invoked when a Channel leaves active state and is no longer connected to its remote peer.

channelReadComplete

Invoked when a read operation on the Channel has completed.

channelRead

Invoked if data are read from the Channel.

channelWritabilityChanged

Invoked when the writability state of the Channel changes. The user can ensure writes are not done too fast (with risk of an OutOfMemoryError) or can resume writes when the Channel becomes writable again.Channel.isWritable() can be used to detect the actual writability of the channel. The threshold for writability can be set via Channel.config().setWriteHighWaterMark() and Channel.config().setWriteLowWaterMark().

userEventTriggered(...)

Invoked when a user calls Channel.fireUserEventTriggered(...) to pass a pojo through the ChannelPipeline. This can be used to pass user specific events through the ChannelPipeline and so allow handling those events.

ChannelOutboundHandler

ChannelOutboundHandler 提供了出站操作時呼叫的方法。這些方法會被 Channel, ChannelPipeline, 和 ChannelHandlerContext 呼叫。

繼承關係與主要方法

方法的主要功能通過變數名就可以知道

幾乎所有的方法都將 ChannelPromise 作為引數,一旦請求結束要通過 ChannelPipeline 轉發的時候,必須通知此引數。

生命週期

型別

描述

bind

Invoked on request to bind the Channel to a local address

connect

Invoked on request to connect the Channel to the remote peer

disconnect

Invoked on request to disconnect the Channel from the remote peer

close

Invoked on request to close the Channel

deregister

Invoked on request to deregister the Channel from its EventLoop

read

Invoked on request to read more data from the Channel

flush

Invoked on request to flush queued data to the remote peer through the Channel

write

Invoked on request to write data through the Channel to the remote peer

ChannelHandlerContext

概述

ChannelHandlerContext 有許多方法,其中一些也出現在 Channel 和ChannelPipeline 本身。然而,如果您通過Channel 或ChannelPipeline 的例項來呼叫這些方法,他們就會在整個 pipeline中傳播 。相比之下,一樣的 的方法在 ChannelHandlerContext的例項上呼叫, 就只會從當前的 ChannelHandler 開始並傳播到相關管道中的下一個有處理事件能力的 ChannelHandler 。

主要方法

小結

  • 當 ChannelHandler 被新增到的 ChannelPipeline 它得到一個 ChannelHandlerContext,它代表一個 ChannelHandler 和 ChannelPipeline 之間的“繫結”。
  • ChannelHandlerContext是在 ChannelHandler 新增到 ChannelPipeline 時建立的
  • ChannelHandlerContext 儲存了ChannelHandler中相關的所有上下文資訊,並關聯著一個ChannelHandler
  • 入站事件和出站事件在同一個ChannelPipeline中,而他們之間傳遞的關係儲存在ChannelHandlerContext中

一句話總結:ChannelPipeline中的ChannelHandler的事件需要傳遞,而傳遞的關係儲存在ChannelHandlerContext中。

ChannelPipeline

概述

在 pipeline 的介面文件上,作者寫了很多註釋並且畫了一幅圖:

通常一個 pipeline 有多個 handler,例如,一個典型的伺服器在每個通道的管道中都會有以下處理程式,但是您的里程可能會因協議和業務邏輯的複雜性和特徵而異:

  1. 協議解碼器 - 將二進位制資料(例如 ByteBuf 在io.netty.buffer中的類))轉換為Java物件。
  2. 協議編碼器 - 將Java物件轉換為二進位制資料。
  3. 業務邏輯處理程式 - 執行實際業務邏輯(例如資料庫訪問)。

繼承關係

主要方法

方法雖然多,但主要的方法還是addXXX()開頭的方法,最常用的也就是addLast();

該介面繼承了 inBound,outBound,Iterable 介面,表示他可以呼叫當資料出站的方法和入站的方法,同時也能遍歷內部的連結串列。

出站入站主要流程

為了使資料從一端到達另一端,一個或多個ChannelHandler將以某種方式操作資料。這些ChannelHandler會在程式的引導”階段被新增ChannelPipeline中,並且被新增的順序將決定處理資料的順序。ChannelPipeline的作用我們可以理解為用來管理ChannelHandler的一個容器,每個ChannelHandler處理各自的資料(例如入站資料只能由ChannelInboundHandler處理),處理完成後將轉換的資料放到ChannelPipeline中交給下一個ChannelHandler繼續處理,直到最後一個ChannelHandler處理完成。

當 ChannelHandler 被新增到 ChannelPipeline 時,它將會被分配一個 ChannelHandlerContext,它代表了 ChannelHandler 和 ChannelPipeline 之間的繫結。

當一個數據流進入 ChannlePipeline 時,它會從 ChannelPipeline 頭部開始傳給第一個 ChannelInboundHandler ,當第一個處理完後再傳給下一個,一直傳遞到管道的尾部。與之相對應的是,當資料被寫出時,它會從管道的尾部開始,先經過管道尾部的 “最後” 一個ChannelOutboundHandler,當它處理完成後會傳遞給前一個 ChannelOutboundHandler 。

出站和入站為什麼可以放在同一個ChannelPipeline工作?

Netty的設計是很巧妙的,入站和出站Handler有不同的實現,Netty能跳過一個不能處理的操作,所以在出站事件的情況下,ChannelInboundHandler將被跳過,Netty知道每個handler都必須實現ChannelInboundHandler或ChannelOutboundHandler

當一個ChannelHandler新增到ChannelPipeline中時獲得一個ChannelHandlerContext。通常是安全的獲得這個物件的引用,但是當一個數據報協議如UDP時這是不正確的,這個物件可以在之後用來獲取底層通道,因為要用它來read/write訊息,因此通道會保留。也就是說Netty中傳送訊息有兩種方法:直接寫入通道或寫入ChannelHandlerContext物件。這兩種方法的主要區別如下:

  • 直接寫入通道導致處理訊息從ChannelPipeline的尾部開始(通過context獲取對應的channel寫入channel也是從ChannelPipeline尾部開始)
  • 寫入ChannelHandlerContext物件導致處理訊息從ChannelPipeline的下一個handler開始

各種Channel之間的關係

一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向連結串列, 並且每個 ChannelHandlerContext 中又關聯著一個 ChannelHandler。入站事件和出站事件在同一個雙向連結串列中,入站事件會從連結串列head往後傳遞到最後一個入站的handler,出站事件會從連結串列tail往前傳遞到最前一個出站的handler,兩種型別的handler互不干擾。

在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應, 它們的組成關係如下:

小結

  • 一個Channel包含一個ChannelPipiline
  • 一個ChannelHandlerContext包含一個ChannelHandler
  • 一個ChannelPipiline包含多個ChannelHandlerContext
  • ChannelHandlerContext 儲存了ChannelHandler中相關的所有上下文資訊
  • ChannelPipeline 維護了一個由 ChannelHandlerContext 組成的雙向連結串列
  • 入站事件和出站事件在同一個ChannelPipeline中,而他們之間傳遞的關係儲存在ChannelHandlerContext中
  • 入站事件會從連結串列head往後傳遞到最後一個入站的handler,出站事件會從連結串列tail往前傳遞到最前一個出站的handler,兩種型別的handler互不干擾。

EventLoop

概述

  1. Channel 為Netty 網路操作抽象類,EventLoop 主要是為Channel 處理 I/O 操作,兩者配合參與 I/O 操作。
  2. 一個 EventLoop通常會處理多個 Channel 事件。
  3. EventLoop總是繫結一個單一的執行緒,在其生命週期內不會改變。
  4. 一個 EventLoopGroup 可以含有多於一個的 EventLoop 和 提供了一種迭代用於檢索清單中的下一個。

繼承關係

主要方法

EventLoopGroup

概述

就像他的名字一樣EventLoppGroup是EventLoop的容器。

繼承關係

主要方法