超詳細Netty入門,看這篇就夠了!

思維導圖

image.png

前言

本文主要講述Netty框架的一些特性以及重要組件,希望看完之後能對Netty框架有一個比較直觀的感受,希望能幫助讀者快速入門Netty,減少一些彎路。

一、Netty概述

官方的介紹:

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.

Netty是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端

二、爲什麼使用Netty

從官網上介紹,Netty是一個網絡應用程序框架,開發服務器和客戶端。也就是用於網絡編程的一個框架。既然是網絡編程,Socket就不談了,爲什麼不用NIO呢?

2.1 NIO的缺點

對於這個問題,之前我寫了一篇文章《NIO入門》對NIO有比較詳細的介紹,NIO的主要問題是:

  • NIO的類庫和API繁雜,學習成本高,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要熟悉Java多線程編程。這是因爲NIO編程涉及到Reactor模式,你必須對多線程和網絡編程非常熟悉,才能寫出高質量的NIO程序。
  • 臭名昭著的epoll bug。它會導致Selector空輪詢,最終導致CPU 100%。直到JDK1.7版本依然沒得到根本性的解決。

2.2 Netty的優點

相對地,Netty的優點有很多:

  • API使用簡單,學習成本低。
  • 功能強大,內置了多種解碼編碼器,支持多種協議。
  • 性能高,對比其他主流的NIO框架,Netty的性能最優。
  • 社區活躍,發現BUG會及時修復,迭代版本週期短,不斷加入新的功能。
  • Dubbo、Elasticsearch都採用了Netty,質量得到驗證。

三、架構圖

image.png

上面這張圖就是在官網首頁的架構圖,我們從上到下分析一下。

綠色的部分Core核心模塊,包括零拷貝、API庫、可擴展的事件模型。

橙色部分Protocol Support協議支持,包括Http協議、webSocket、SSL(安全套接字協議)、谷歌Protobuf協議、zlib/gzip壓縮與解壓縮、Large File Transfer大文件傳輸等等。

紅色的部分Transport Services傳輸服務,包括Socket、Datagram、Http Tunnel等等。

以上可看出Netty的功能、協議、傳輸方式都比較全,比較強大。

四、永遠的Hello Word

首先搭建一個HelloWord工程,先熟悉一下API,還有爲後面的學習做鋪墊。以下面這張圖爲依據:

image.png

4.1 引入Maven依賴

使用的版本是4.1.20,相對比較穩定的一個版本。

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.20.Final</version>
</dependency>

4.2 創建服務端啓動類

public class MyServer {
    public static void main(String[] args) throws Exception {
        //創建兩個線程組 boosGroup、workerGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //創建服務端的啓動對象,設置參數
            ServerBootstrap bootstrap = new ServerBootstrap();
            //設置兩個線程組boosGroup和workerGroup
            bootstrap.group(bossGroup, workerGroup)
                //設置服務端通道實現類型    
                .channel(NioServerSocketChannel.class)
                //設置線程隊列得到連接個數    
                .option(ChannelOption.SO_BACKLOG, 128)
                //設置保持活動連接狀態    
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                //使用匿名內部類的形式初始化通道對象    
                .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //給pipeline管道設置處理器
                            socketChannel.pipeline().addLast(new MyServerHandler());
                        }
                    });//給workerGroup的EventLoop對應的管道設置處理器
            System.out.println("java技術愛好者的服務端已經準備就緒...");
            //綁定端口號,啓動服務端
            ChannelFuture channelFuture = bootstrap.bind(6666).sync();
            //對關閉通道進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

4.3 創建服務端處理器

/**
 * 自定義的Handler需要繼承Netty規定好的HandlerAdapter
 * 才能被Netty框架所關聯,有點類似SpringMVC的適配器模式
 **/
public class MyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //獲取客戶端發送過來的消息
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到客戶端" + ctx.channel().remoteAddress() + "發送的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //發送消息給客戶端
        ctx.writeAndFlush(Unpooled.copiedBuffer("服務端已收到消息,並給你發送一個問號?", CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //發生異常,關閉通道
        ctx.close();
    }
}

4.4 創建客戶端啓動類

public class MyClient {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            //創建bootstrap對象,配置參數
            Bootstrap bootstrap = new Bootstrap();
            //設置線程組
            bootstrap.group(eventExecutors)
                //設置客戶端的通道實現類型    
                .channel(NioSocketChannel.class)
                //使用匿名內部類初始化通道
                .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //添加客戶端通道的處理器
                            ch.pipeline().addLast(new MyClientHandler());
                        }
                    });
            System.out.println("客戶端準備就緒,隨時可以起飛~");
            //連接服務端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            //對通道關閉進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            //關閉線程組
            eventExecutors.shutdownGracefully();
        }
    }
}

4.5 創建客戶端處理器

public class MyClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //發送消息到服務端
        ctx.writeAndFlush(Unpooled.copiedBuffer("歪比巴卜~茉莉~Are you good~馬來西亞~", CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //接收服務端發送過來的消息
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到服務端" + ctx.channel().remoteAddress() + "的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
}

4.6 測試

先啓動服務端,再啓動客戶端,就可以看到結果:

MyServer打印結果:

image.png

MyClient打印結果:
image.png

五、Netty的特性與重要組件

5.1 taskQueue任務隊列

如果Handler處理器有一些長時間的業務處理,可以交給taskQueue異步處理。怎麼用呢,請看代碼演示:

public class MyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //獲取到線程池eventLoop,添加線程,執行
        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    //長時間操作,不至於長時間的業務操作導致Handler阻塞
                    Thread.sleep(1000);
                    System.out.println("長時間的業務處理");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

我們打一個debug調試,是可以看到添加進去的taskQueue有一個任務。

image.png

5.2 scheduleTaskQueue延時任務隊列

延時任務隊列和上面介紹的任務隊列非常相似,只是多了一個可延遲一定時間再執行的設置,請看代碼演示:

ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run() {
        try {
            //長時間操作,不至於長時間的業務操作導致Handler阻塞
            Thread.sleep(1000);
            System.out.println("長時間的業務處理");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
},5, TimeUnit.SECONDS);//5秒後執行

依然打開debug進行調試查看,我們可以有一個scheduleTaskQueue任務待執行中

5.3 Future異步機制

在搭建HelloWord工程的時候,我們看到有一行這樣的代碼:

ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666);

很多操作都返回這個ChannelFuture對象,究竟這個ChannelFuture對象是用來做什麼的呢?

ChannelFuture提供操作完成時一種異步通知的方式。一般在Socket編程中,等待響應結果都是同步阻塞的,而Netty則不會造成阻塞,因爲ChannelFuture是採取類似觀察者模式的形式進行獲取結果。請看一段代碼演示:

//添加監聽器
channelFuture.addListener(new ChannelFutureListener() {
    //使用匿名內部類,ChannelFutureListener接口
    //重寫operationComplete方法
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        //判斷是否操作成功    
        if (future.isSuccess()) {
            System.out.println("連接成功");
        } else {
            System.out.println("連接失敗");
        }
    }
});

5.4 Bootstrap與ServerBootStrap

Bootstrap和ServerBootStrap是Netty提供的一個創建客戶端和服務端啓動器的工廠類,使用這個工廠類非常便利地創建啓動類,根據上面的一些例子,其實也看得出來能大大地減少了開發的難度。首先看一個類圖:

image.png

可以看出都是繼承於AbstractBootStrap抽象類,所以大致上的配置方法都相同。

一般來說,使用Bootstrap創建啓動器的步驟可分爲以下幾步:

image.png

5.4.1 group()

在上一篇文章《Reactor模式》中,我們就講過服務端要使用兩個線程組:

  • bossGroup 用於監聽客戶端連接,專門負責與客戶端創建連接,並把連接註冊到workerGroup的Selector中。
  • workerGroup用於處理每一個連接發生的讀寫事件。

一般創建線程組直接使用以下new就完事了:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

有點好奇的是,既然是線程組,那線程數默認是多少呢?深入源碼:

    //使用一個常量保存
    private static final int DEFAULT_EVENT_LOOP_THREADS;

    static {
        //NettyRuntime.availableProcessors() * 2,cpu核數的兩倍賦值給常量
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
    }
    
    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
        //如果不傳入,則使用常量的值,也就是cpu核數的兩倍
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }

通過源碼可以看到,默認的線程數是cpu核數的兩倍。假設想自定義線程數,可以使用有參構造器:

//設置bossGroup線程數爲1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//設置workerGroup線程數爲16
EventLoopGroup workerGroup = new NioEventLoopGroup(16);

5.4.2 channel()

這個方法用於設置通道類型,當建立連接後,會根據這個設置創建對應的Channel實例。

使用debug模式可以看到

通道類型有以下:

NioSocketChannel: 異步非阻塞的客戶端 TCP Socket 連接。

NioServerSocketChannel: 異步非阻塞的服務器端 TCP Socket 連接。

常用的就是這兩個通道類型,因爲是異步非阻塞的。所以是首選。

OioSocketChannel: 同步阻塞的客戶端 TCP Socket 連接。

OioServerSocketChannel: 同步阻塞的服務器端 TCP Socket 連接。

稍微在本地調試過,用起來和Nio有一些不同,是阻塞的,所以API調用也不一樣。因爲是阻塞的IO,幾乎沒什麼人會選擇使用Oio,所以也很難找到例子。我稍微琢磨了一下,經過幾次報錯之後,總算調通了。代碼如下:

//server端代碼,跟上面幾乎一樣,只需改三個地方
//這個地方使用的是OioEventLoopGroup
EventLoopGroup bossGroup = new OioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup)//只需要設置一個線程組boosGroup
        .channel(OioServerSocketChannel.class)//設置服務端通道實現類型

//client端代碼,只需改兩個地方
//使用的是OioEventLoopGroup
EventLoopGroup eventExecutors = new OioEventLoopGroup();
//通道類型設置爲OioSocketChannel
bootstrap.group(eventExecutors)//設置線程組
        .channel(OioSocketChannel.class)//設置客戶端的通道實現類型

NioSctpChannel: 異步的客戶端 Sctp(Stream Control Transmission Protocol,流控制傳輸協議)連接。

NioSctpServerChannel: 異步的 Sctp 服務器端連接。

本地沒啓動成功,網上看了一些網友的評論,說是隻能在linux環境下才可以啓動。從報錯信息看:SCTP not supported on this platform,不支持這個平臺。因爲我電腦是window系統,所以網友說的有點道理。

5.4.3 option()與childOption()

首先說一下這兩個的區別。

option()設置的是服務端用於接收進來的連接,也就是boosGroup線程。

childOption()是提供給父管道接收到的連接,也就是workerGroup線程。

搞清楚了之後,我們看一下常用的一些設置有哪些:

SocketChannel參數,也就是childOption()常用的參數:

SO_RCVBUF Socket參數,TCP數據接收緩衝區大小。
TCP_NODELAY TCP參數,立即發送數據,默認值爲Ture。
SO_KEEPALIVE Socket參數,連接保活,默認值爲False。啓用該功能時,TCP會主動探測空閒連接的有效性。

ServerSocketChannel參數,也就是option()常用參數:

SO_BACKLOG Socket參數,服務端接受連接的隊列長度,如果隊列已滿,客戶端連接將被拒絕。默認值,Windows爲200,其他爲128。

由於篇幅限制,其他就不列舉了,大家可以去網上找資料看看,瞭解一下。

5.4.4 設置流水線(重點)

ChannelPipeline是Netty處理請求的責任鏈,ChannelHandler則是具體處理請求的處理器。實際上每一個channel都有一個處理器的流水線。

在Bootstrap中childHandler()方法需要初始化通道,實例化一個ChannelInitializer,這時候需要重寫initChannel()初始化通道的方法,裝配流水線就是在這個地方進行。代碼演示如下:

//使用匿名內部類的形式初始化通道對象
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //給pipeline管道設置自定義的處理器
        socketChannel.pipeline().addLast(new MyServerHandler());
    }
});

處理器Handler主要分爲兩種:

ChannelInboundHandlerAdapter(入站處理器)、ChannelOutboundHandler(出站處理器)

入站指的是數據從底層java NIO Channel到Netty的Channel。

出站指的是通過Netty的Channel來操作底層的java NIO Channel。

ChannelInboundHandlerAdapter處理器常用的事件有

  1. 註冊事件 fireChannelRegistered。
  2. 連接建立事件 fireChannelActive。
  3. 讀事件和讀完成事件 fireChannelRead、fireChannelReadComplete。
  4. 異常通知事件 fireExceptionCaught。
  5. 用戶自定義事件 fireUserEventTriggered。
  6. Channel 可寫狀態變化事件 fireChannelWritabilityChanged。
  7. 連接關閉事件 fireChannelInactive。

ChannelOutboundHandler處理器常用的事件有

  1. 端口綁定 bind。
  2. 連接服務端 connect。
  3. 寫事件 write。
  4. 刷新時間 flush。
  5. 讀事件 read。
  6. 主動斷開連接 disconnect。
  7. 關閉 channel 事件 close。

還有一個類似的handler(),主要用於裝配parent通道,也就是bossGroup線程。一般情況下,都用不上這個方法。

5.4.5 bind()

提供用於服務端或者客戶端綁定服務器地址和端口號,默認是異步啓動。如果加上sync()方法則是同步。

有五個同名的重載方法,作用都是用於綁定地址端口號。不一一介紹了。

5.4.6 優雅地關閉EventLoopGroup

//釋放掉所有的資源,包括創建的線程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

會關閉所有的child Channel。關閉之後,釋放掉底層的資源。

5.5 Channel

Channel是什麼?不妨看一下官方文檔的說明:

A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind

翻譯大意:一種連接到網絡套接字或能進行讀、寫、連接和綁定等I/O操作的組件。

如果上面這段說明比較抽象,下面還有一段說明:

A channel provides a user:

the current state of the channel (e.g. is it open? is it connected?),
the configuration parameters of the channel (e.g. receive buffer size),
the I/O operations that the channel supports (e.g. read, write, connect, and bind), and
the ChannelPipeline which handles all I/O events and requests associated with the channel.

翻譯大意:

channel爲用戶提供:

  1. 通道當前的狀態(例如它是打開?還是已連接?)
  2. channel的配置參數(例如接收緩衝區的大小)
  3. channel支持的IO操作(例如讀、寫、連接和綁定),以及處理與channel相關聯的所有IO事件和請求的ChannelPipeline。

5.5.1 獲取channel的狀態

boolean isOpen(); //如果通道打開,則返回true
boolean isRegistered();//如果通道註冊到EventLoop,則返回true
boolean isActive();//如果通道處於活動狀態並且已連接,則返回true
boolean isWritable();//當且僅當I/O線程將立即執行請求的寫入操作時,返回true。

以上就是獲取channel的四種狀態的方法。

5.5.2 獲取channel的配置參數

獲取單條配置信息,使用getOption(),代碼演示:

ChannelConfig config = channel.config();//獲取配置參數
//獲取ChannelOption.SO_BACKLOG參數,
Integer soBackLogConfig = config.getOption(ChannelOption.SO_BACKLOG);
//因爲我啓動器配置的是128,所以我這裏獲取的soBackLogConfig=128

獲取多條配置信息,使用getOptions(),代碼演示:

ChannelConfig config = channel.config();
Map<ChannelOption<?>, Object> options = config.getOptions();
for (Map.Entry<ChannelOption<?>, Object> entry : options.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
}
/**
SO_REUSEADDR : false
WRITE_BUFFER_LOW_WATER_MARK : 32768
WRITE_BUFFER_WATER_MARK : WriteBufferWaterMark(low: 32768, high: 65536)
SO_BACKLOG : 128
以下省略...
*/

5.5.3 channel支持的IO操作

寫操作,這裏演示從服務端寫消息發送到客戶端:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.channel().writeAndFlush(Unpooled.copiedBuffer("這波啊,這波是肉蛋蔥雞~", CharsetUtil.UTF_8));
}

客戶端控制檯:

//收到服務端/127.0.0.1:6666的消息:這波啊,這波是肉蛋蔥雞~

連接操作,代碼演示:

ChannelFuture connect = channelFuture.channel().connect(new InetSocketAddress("127.0.0.1", 6666));//一般使用啓動器,這種方式不常用

通過channel獲取ChannelPipeline,並做相關的處理:

//獲取ChannelPipeline對象
ChannelPipeline pipeline = ctx.channel().pipeline();
//往pipeline中添加ChannelHandler處理器,裝配流水線
pipeline.addLast(new MyServerHandler());

5.6 Selector

在NioEventLoop中,有一個成員變量selector,這是nio包的Selector,在之前《NIO入門》中,我已經講過Selector了。

Netty中的Selector也和NIO的Selector是一樣的,就是用於監聽事件,管理註冊到Selector中的channel,實現多路複用器。

image.png

5.7 PiPeline與ChannelPipeline

在前面介紹Channel時,我們知道可以在channel中裝配ChannelHandler流水線處理器,那一個channel不可能只有一個channelHandler處理器,肯定是有很多的,既然是很多channelHandler在一個流水線工作,肯定是有順序的。

於是pipeline就出現了,pipeline相當於處理器的容器。初始化channel時,把channelHandler按順序裝在pipeline中,就可以實現按序執行channelHandler了。

image.png

在一個Channel中,只有一個ChannelPipeline。該pipeline在Channel被創建的時候創建。ChannelPipeline包含了一個ChannelHander形成的列表,且所有ChannelHandler都會註冊到ChannelPipeline中。

5.8 ChannelHandlerContext

在Netty中,Handler處理器是有我們定義的,上面講過通過集成入站處理器或者出站處理器實現。這時如果我們想在Handler中獲取pipeline對象,或者channel對象,怎麼獲取呢。

於是Netty設計了這個ChannelHandlerContext上下文對象,就可以拿到channel、pipeline等對象,就可以進行讀寫等操作。

image.png

通過類圖,ChannelHandlerContext是一個接口,下面有三個實現類。

實際上ChannelHandlerContext在pipeline中是一個鏈表的形式。看一段源碼就明白了:

//ChannelPipeline實現類DefaultChannelPipeline的構造器方法
protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);
    //設置頭結點head,尾結點tail
    tail = new TailContext(this);
    head = new HeadContext(this);
    
    head.next = tail;
    tail.prev = head;
}

下面我用一張圖來表示,會更加清晰一點:
image.png

5.9 EventLoopGroup

我們先看一下EventLoopGroup的類圖:

image.png

其中包括了常用的實現類NioEventLoopGroup。OioEventLoopGroup在前面的例子中也有使用過。

從Netty的架構圖中,可以知道服務器是需要兩個線程組進行配合工作的,而這個線程組的接口就是EventLoopGroup。

每個EventLoopGroup裏包括一個或多個EventLoop,每個EventLoop中維護一個Selector實例。

5.9.1 輪詢機制的實現原理

我們不妨看一段DefaultEventExecutorChooserFactory的源碼:

private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;

@Override
public EventExecutor next() {
    //idx.getAndIncrement()相當於idx++,然後對任務長度取模
    return executors[idx.getAndIncrement() & executors.length - 1];
}

這段代碼可以確定執行的方式是輪詢機制,接下來debug調試一下:

它這裏還有一個判斷,如果線程數不是2的N次方,則採用取模算法實現。

@Override
public EventExecutor next() {
    return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章