1. 程式人生 > >Dubbo教程-03-netty框架

Dubbo教程-03-netty框架

寫在前面

hello 大家好
歡迎大家收看御風大世界
那麼這次課呢使我們Dubbo系列教程的第三課
在本次課我將為大家介紹
dubbo的底層RPC通訊框架 netty
並且我將為大家演示一個 netty的服務端 客戶端通訊程式

什麼是netty?

1)本質:JBoss做的一個Jar包

2)目的:快速開發高效能、高可靠性的網路伺服器和客戶端程式

3)優點:提供非同步的、事件驅動的網路應用程式框架和工具

通俗的說:一個好使的處理Socket的東東

如果沒有Netty?

遠古:java.net + java.io

近代:java.nio

其他:Mina,Grizzly

imagepng

netty為什麼效能高?

高效能的三大要素

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

2) 協議:採用什麼樣的通訊協議,HTTP或者內部私有協議。協議的選擇不同,效能模型也不同。相比於公有協議,內部私有協議的效能通常可以被設計的更優。

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

非同步非阻塞通訊

在IO程式設計過程中,當需要同時處理多個客戶端接入請求時,可以利用多執行緒或者IO多路複用技術進行處理。
IO多路複用技術通過把多個IO的阻塞複用到同一個select的阻塞上,從而使得系統在單執行緒的情況下可以同時處理多個客戶端請求。
與傳統的多執行緒/多程序模型比,I/O多路複用的最大優勢是系統開銷小,
系統不需要建立新的額外程序或者執行緒,也不需要維護這些程序和執行緒的執行,降低了系統的維護工作量,節省了系統資源。

NIO的多路複用模型圖

與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現。
這兩種新增的通道都支援阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是效能和可靠性都不好,非阻塞模式正好相反。
開發人員一般可以根據自己的需要來選擇合適的模式,一般來說,低負載、低併發的應用程式可以選擇同步阻塞IO以降低程式設計複雜度。
但是對於高負載、高併發的網路應用,需要使用NIO的非阻塞模式進行開發。

零拷貝

零拷貝是Netty的重要特性之一,而究竟什麼是零拷貝呢?

“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

從WIKI的定義中,我們看到“零拷貝”是指計算機操作的過程中,CPU不需要為資料在記憶體之間的拷貝消耗資源。
而它通常是指計算機在網路上傳送檔案時,不需要將檔案內容拷貝到使用者空間(User Space)而直接在核心空間(Kernel Space)中傳輸到網路的方式。

Non-Zero Copy方式:

Non-Zero Copy方式

Zero Copy方式:

從上圖中可以清楚的看到,Zero Copy的模式中,避免了資料在使用者空間和記憶體空間之間的拷貝,從而提高了系統的整體效能。
Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都實現了零拷貝的功能,
而在Netty中也通過在FileRegion中包裝了NIO的FileChannel.transferTo()方法實現了零拷貝。

而在Netty中還有另一種形式的零拷貝,即Netty允許我們將多段資料合併為一整段虛擬資料供使用者使用,
而過程中不需要對資料進行拷貝操作,這也是我們今天要講的重點。我們都知道在stream-based transport(如TCP/IP)的傳輸過程中,
資料包有可能會被重新封裝在不同的資料包中,例如當你傳送如下資料時:

有可能實際收到的資料如下:

因此在實際應用中,很有可能一條完整的訊息被分割為多個數據包進行網路傳輸,而單個的資料包對你而言是沒有意義的,
只有當這些資料包組成一條完整的訊息時你才能做出正確的處理,而Netty可以通過零拷貝的方式將這些資料包組合成一條完整的訊息供你來使用。
而此時,零拷貝的作用範圍僅在使用者空間中。

記憶體池

為什麼要使用記憶體池?

隨著JVM虛擬機器和JIT即時編譯技術的發展,物件的分配和回收是個非常輕量級的工作。
但是對於緩衝區Buffer,情況卻稍有不同,特別是對於堆外直接記憶體的分配和回收,是一件耗時的操作。
而且這些例項隨著訊息的處理朝生夕滅,這就會給伺服器帶來沉重的GC壓力,同時消耗大量的記憶體。
為了儘量重用緩衝區,Netty提供了基於記憶體池的緩衝區重用機制。效能測試表明,採用記憶體池的ByteBuf相比於朝生夕滅的ByteBuf,效能高23倍左右(效能資料與使用場景強相關)。

如何啟動並初始化記憶體池?

在Netty4或Netty5中實現了一個新的ByteBuf記憶體池,它是一個純Java版本的 jemalloc (Facebook也在用)。
現在,Netty不會再因為用零填充緩衝區而浪費記憶體帶寬了。 不過,由於它不依賴於GC,開發人員需要小心記憶體洩漏。
如果忘記在處理程式中釋放緩衝區,那麼記憶體使用率會無限地增長。 Netty預設不使用記憶體池,需要在建立客戶端或者服務端的時候在引導輔助類中進行配置:

work執行緒配置

如何在自己的業務程式碼中使用記憶體池?

首先,介紹一下Netty的ByteBuf緩衝區的種類:ByteBuf支援堆緩衝區和堆外直接緩衝區,根據經驗來說,
底層IO處理執行緒的緩衝區使用堆外直接緩衝區,減少一次IO複製。業務訊息的編解碼使用堆緩衝區,分配效率更高,而且不涉及到核心緩衝區的複製問題。

ByteBuf的堆緩衝區又分為記憶體池緩衝區PooledByteBuf和普通記憶體緩衝區UnpooledHeapByteBuf。
PooledByteBuf採用二叉樹來實現一個記憶體池,集中管理記憶體的分配和釋放,不用每次使用都新建一個緩衝區物件。
UnpooledHeapByteBuf每次都會新建一個緩衝區物件。在高併發的情況下推薦使用PooledByteBuf,可以節約記憶體的分配。
在效能能夠保證的情況下,可以使用UnpooledHeapByteBuf,實現比較簡單。

在此說明這是當我們在業務程式碼中要使用池化的ByteBuf時的方法:

第一種情況:若我們的業務程式碼只是為了將資料寫入ByteBuf中併發送出去,那麼我們應該使用堆外直接緩衝區DirectBuffer.使用方式如下:

高效的Reactor執行緒模型

Reactor模式是事件驅動的,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers;
這個Service Handler會同步的將輸入的請求(Event)多路複用的分發給相應的Request Handler

從結構上,這有點類似生產者消費者模式,即有一個或多個生產者將事件放入一個Queue中,而一個或多個消費者主動的從這個Queue中Poll事件來處理;
而Reactor模式則並沒有Queue來做緩衝,每當一個Event輸入到Service Handler之後,該Service Handler會立刻的根據不同的Event型別將其分發給對應的Request Handler來處理。

這個做的好處有很多,首先我們可以將處理event的Request handler實現一個單獨的執行緒,即:

Request handler執行緒

這樣Service Handler 和request Handler實現了非同步,加快了service Handler處理event的速度,
那麼每一個request同樣也可以以多執行緒的形式來處理自己的event,即Thread1 擴充套件成Thread pool 1,

Netty的Reactor執行緒模型1 Reactor單執行緒模型 Reactor機制中保證每次讀寫能非阻塞讀寫

Reactor單執行緒模型

一個執行緒(單執行緒)來處理CONNECT事件(Acceptor),一個執行緒池(多執行緒)來處理read,一個執行緒池(多執行緒)來處理write,那麼從Reactor Thread到handler都是非同步的,從而IO操作也多執行緒化。

到這裡跟BIO對比已經提升了很大的效能,但是還可以繼續提升,由於Reactor Thread依然為單執行緒,從效能上考慮依然有所限制

Reactor多執行緒模型

Reactor多執行緒模型

這樣通過Reactor Thread Pool來提高event的分發能力

3 Reactor主從模型

Netty的高效併發程式設計主要體現在如下幾點:

1) volatile的大量、正確使用;

2) CAS和原子類的廣泛使用;

3) 執行緒安全容器的使用;

4) 通過讀寫鎖提升併發效能。

Netty除了使用reactor來提升效能,當然還有

1、零拷貝,IO效能優化

2、通訊上的粘包拆包

2、同步的設計

3、高效能的序列

dubbo為什麼選擇netty?

Dubbo通訊層(利用Netty**)的實現過程**

dubbo 的 provider 和 consumer 的通訊 互動
實際就是 RPC呼叫
而也就是 JAVA的代理
只不過 我們需要把資料通過網路相互傳遞
因此一個 網路 服務端執行緒模型 或者說 事件驅動的 設計模式
顯得 十分對路子
我想這就是Dubbo為什麼 選擇用 netty的 原因吧

netty怎麼用?

netty 的程式設計模型其實並不難
我們來演示一下

netty的hello world

首先我們需要匯入的maven依賴

<dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>5.0.0.Alpha2</version>
</dependency>

接下來就是快樂的編碼過程
首先給大家 說明一下 基於網路程式設計 離不開連個角色
server 服務端 client 客戶端
我們的第一個 hello world 程式主要給大家演示
如何使用 netty寫一個 server
並用我們的 CMD telnet 實現 client的一些操作

package cn.bywind;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * 丟棄任何進入的資料 啟動服務端的DiscardServerHandler
 */
public class DiscardServer {
    private int port;

    public DiscardServer(int port) {
        super();
        this.port = port;
    }

    public void run() throws Exception {

        /***
         * NioEventLoopGroup 是用來處理I/O操作的多執行緒事件迴圈器,
         * Netty提供了許多不同的EventLoopGroup的實現用來處理不同傳輸協議。 在這個例子中我們實現了一個服務端的應用,
         * 因此會有2個NioEventLoopGroup會被使用。 第一個經常被叫做‘boss’,用來接收進來的連線。
         * 第二個經常被叫做‘worker’,用來處理已經被接收的連線, 一旦‘boss’接收到連線,就會把連線資訊註冊到‘worker’上。
         * 如何知道多少個執行緒已經被使用,如何對映到已經建立的Channels上都需要依賴於EventLoopGroup的實現,
         * 並且可以通過建構函式來配置他們的關係。
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        System.out.println("準備執行埠:" + port);
        try {
            /**
             * ServerBootstrap 是一個啟動NIO服務的輔助啟動類 你可以在這個服務中直接使用Channel
             */
            ServerBootstrap b = new ServerBootstrap();
            /**
             * 這一步是必須的,如果沒有設定group將會報java.lang.IllegalStateException: group not
             * set異常
             */
            b = b.group(bossGroup, workerGroup);
            /***
             * ServerSocketChannel以NIO的selector為基礎進行實現的,用來接收新的連線
             * 這裡告訴Channel如何獲取新的連線.
             */
            b = b.channel(NioServerSocketChannel.class);
            /***
             * 這裡的事件處理類經常會被用來處理一個最近的已經接收的Channel。 ChannelInitializer是一個特殊的處理類,
             * 他的目的是幫助使用者配置一個新的Channel。
             * 也許你想通過增加一些處理類比如NettyServerHandler來配置一個新的Channel
             * 或者其對應的ChannelPipeline來實現你的網路程式。 當你的程式變的複雜時,可能你會增加更多的處理類到pipline上,
             * 然後提取這些匿名類到最頂層的類上。
             */
            b = b.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new DiscardServerHandler());// demo1.discard
                    // ch.pipeline().addLast(new
                    // ResponseServerHandler());//demo2.echo
                    // ch.pipeline().addLast(new
                    // TimeServerHandler());//demo3.time
                }
            });
            /***
             * 你可以設定這裡指定的通道實現的配置引數。 我們正在寫一個TCP/IP的服務端,
             * 因此我們被允許設定socket的引數選項比如tcpNoDelay和keepAlive。
             * 請參考ChannelOption和詳細的ChannelConfig實現的介面文件以此可以對ChannelOptions的有一個大概的認識。
             */
            b = b.option(ChannelOption.SO_BACKLOG, 128);
            /***
             * option()是提供給NioServerSocketChannel用來接收進來的連線。
             * childOption()是提供給由父管道ServerChannel接收到的連線,
             * 在這個例子中也是NioServerSocketChannel。
             */
            b = b.childOption(ChannelOption.SO_KEEPALIVE, true);
            /***
             * 繫結埠並啟動去接收進來的連線
             */
            ChannelFuture f = b.bind(port).sync();
            /**
             * 這裡會一直等待,直到socket被關閉
             */
            f.channel().closeFuture().sync();
        } finally {
            /***
             * 關閉
             */
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    //將規則跑起來
    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
        System.out.println("server:run()");
    }
}

接著我們需要自己寫一個 handler

package cn.bywind;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;

/**
 * 服務端處理通道.這裡只是列印一下請求的內容,並不對請求進行任何的響應 DiscardServerHandler 繼承自
 * ChannelHandlerAdapter, 這個類實現了ChannelHandler介面, ChannelHandler提供了許多事件處理的介面方法,
 * 然後你可以覆蓋這些方法。 現在僅僅只需要繼承ChannelHandlerAdapter類而不是你自己去實現介面方法。
 *
 */
public class DiscardServerHandler extends ChannelHandlerAdapter {
    /**
     * 這裡我們覆蓋了chanelRead()事件處理方法。 每當從客戶端收到新的資料時, 這個方法會在收到訊息時被呼叫,
     * 這個例子中,收到的訊息的型別是ByteBuf
     * 
     * @param ctx
     *            通道處理的上下文資訊
     * @param msg
     *            接收的訊息
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {

        try {
            ByteBuf in = (ByteBuf) msg;
            // 列印客戶端輸入,傳輸過來的的字元
            System.out.print(in.toString(CharsetUtil.UTF_8));
        } finally {
            /**
             * ByteBuf是一個引用計數物件,這個物件必須顯示地呼叫release()方法來釋放。
             * 請記住處理器的職責是釋放所有傳遞到處理器的引用計數物件。
             */
            // 拋棄收到的資料
            ReferenceCountUtil.release(msg);
        }

    }

    /***
     * 這個方法會在發生異常時觸發
     * 
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        /**
         * exceptionCaught() 事件處理方法是當出現 Throwable 物件才會被呼叫,即當 Netty 由於 IO
         * 錯誤或者處理器在處理事件時丟擲的異常時。在大部分情況下,捕獲的異常應該被記錄下來 並且把關聯的 channel
         * 給關閉掉。然而這個方法的處理方式會在遇到不同異常的情況下有不 同的實現,比如你可能想在關閉連線之前傳送一個錯誤碼的響應訊息。
         */
        // 出現異常就關閉
        cause.printStackTrace();
        ctx.close();
    }

}

接下來 我們執行 server端 啟動它的 main方法
我們開啟 CMD telnet localhost 8080
然後我們後續的任何輸入
都會被 server 接收到

imagepng

我們開啟CMD
然後 輸入

telnet 127.0.0.1 8080

然後就可以輸入字元
而這個時候就會實時的在服務端 顯示你輸入的字元
比如我們 輸入 java
你可以看到 server的控制檯 就會 出現了

imagepng

總結

netty 作為高效能的 網路通訊框架

擁有良好的設計模式

高效的讀寫效能

通過對netty的學習和實踐

我們知道 dubbo的 底層實現就是基於netty

也更加清楚的明白 遠端過程呼叫 RPC 基於 代理 + 網路通訊

是如何實現的

本次課的演示程式碼我上傳到了我的

github : https://github.com/ibywind/dubbo-learn