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

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

前言

關於Netty的學習,最近看了不少有關視訊和書籍,也收穫不少,希望把我知道的分享給你們,一起加油,一起成長。前面我們對 Java IOBIONIOAIO進行了分析,相關文章連結如下:

深入分析 Java IO (一)概述

深入分析 Java IO (二)BIO

深入分析 Java IO (三)NIO

深入分析 Java IO (四)AIO

本篇文章我們就開始對 Netty來進行深入分析,首先我們來了解一下 JAVA NIOAIO的不足之處。

Java原生API之痛

雖然JAVA NIOJAVA AIO框架提供了多路複用IO/非同步IO的支援,但是並沒有提供上層“資訊格式”的良好封裝。用這些API實現一款真正的網路應用則並非易事。

JAVA NIOJAVA AIO並沒有提供斷連重連、網路閃斷、半包讀寫、失敗快取、網路擁塞和異常碼流等的處理,這些都需要開發者自己來補齊相關的工作。

AIO在實踐中,並沒有比NIO更好。AIO在不同的平臺有不同的實現,windows系統下使用的是一種非同步IO技術:IOCP;Linux下由於沒有這種非同步 IO 技術,所以使用的是epoll 對非同步 IO 進行模擬。所以 AIO 在 Linux 下的效能並不理想。AIO 也沒有提供對 UDP 的支援。

綜上,在實際的大型網際網路專案中,Java 原生的 API 應用並不廣泛,取而代之的是一款第三方Java 框架,這就是Netty

Netty的優勢

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

非阻塞 I/O

Netty 是基於 Java NIO API 實現的網路應用框架,使用它可以快速簡單的開發網路應用程式,如伺服器和客戶端程式。Netty 大大簡化了網路程式開發的過程,如 TCP 和 UDP 的 Socket 服務的開發。

由於是基於 NIO 的 API,因此,Netty 可以提供非阻塞的 I/O操作,極大的提升了效能。同時,Netty 內部封裝了 Java NIO API 的複雜性,並提供了執行緒池的處理,使得開發 NIO 的應用變得極其簡單。

豐富的協議

Netty 提供了簡單、易用的 API ,但這並不意味著應用程式會有難維護和效能低的問題。Netty 是一個精心設計的框架,它從許多協議的實現中吸收了很多的經驗,如 FTP 、SMTP、 HTTP、許多二進位制和基於文字的傳統協議。

Netty 支援豐富的網路協議,如TCPUDPHTTPHTTP/2WebSocketSSL/TLS等,這些協議實現開箱即用,因此,Netty 開發者能夠在不失靈活的前提下來實現開發的簡易性、高效能和穩定性。

非同步和事件驅動

Netty 是非同步事件驅動的框架,該框架體現為所有的I/O操作都是非同步的,所有的I/O呼叫會立即返回,並不保證呼叫成功與否,但是呼叫會返回ChannelFuture。Netty 會通過 ChannelFuture通知呼叫是成功了還是失敗了,抑或是取消了。

同時,Netty 是基於事件驅動的,呼叫者並不能立即獲得結果,而是通過事件監聽機制,使用者可以方便地主動獲取或者通過通知機制獲得I/O操作的結果。

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

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

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

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

相比傳統的阻塞 I/O,Netty 非同步處理的好處是不會造成執行緒阻塞,執行緒在 I/O操作期間可以執行其他的程式,在高併發情形下會更穩定並擁有更高的吞吐量。

精心設計的API

Netty 從開始就為使用者提供了體驗最好的API及實現設計。

例如,在使用者數較小的時候可能會選擇傳統的阻塞API,畢竟與 Java NIO 相比使用阻塞 API 將會更加容易一些。然而,當業務量呈指數增長並且伺服器需要同時處理成千上萬的客戶連線,便會遇到問題。這種情況下可能會嘗試使用 Java NIO,但是複雜的 NIO Selector 程式設計介面又會耗費大量的時間並最終會阻礙快速開發。

Netty 提供了一個叫作 channel的統一的非同步I/O程式設計介面,這個程式設計介面抽象了所有點對點的通訊操作。也就是說,如果應用是基於Netty 的某一種傳輸實現,那麼同樣的,應用也可以執行在 Netty 的另一種傳輸實現上。Channel常見的子介面有:

image-20210804105936809

豐富的緩衝實現

Netty 使用自建的快取 API,而不是使用 Java NIO 的 ByteBuffer 來表示一個連續的位元組序列。與 ByteBuffer 相比,這種方式擁有明顯的優勢。

Netty 使用新的緩衝型別 ByteBuf ,並且被設計為可從底層解決 ByteBuffer 問題,同時還滿足日常網路應用開發需要的緩衝型別。

Netty 重要有以下特性:

  • 允許使用自定義的緩衝型別。
  • 複合緩衝型別中內建透明的零拷貝實現。
  • 開箱即用動態緩衝型別,具有像 StringBuffer 一樣的動態緩衝能力。
  • 不再需要呼叫flip()方法。
  • 正常情況下具有比ByteBuffer更快的響應速度。

高效的網路傳輸

Java 原生的序列化主要存在以下幾個弊端:

  • 無法跨語言。

  • 序列化後碼流太大。

  • 序列化後效能太低。

業界有非常多的框架用於解決上述問題,如 Google ProtobufJBoss MarshallingFacebook Thrift等。針對這些框架,Netty 都提供了相應的包將這些框架整合到應用中。同時,Netty 本身也提供了眾多的編解碼工具,方便開發者使用。開發者可以基於 Netty 來開發高效的網路傳輸應用,例如:高效能的訊息中介軟體 Apache RocketMQ、高效能RPC框架Apache Dubbo等。

Netty 核心概念

Netty功能特性圖

從上述的架構圖可以看出,Netty 主要由三大塊組成:

  • 核心元件
  • 傳輸服務
  • 協議

核心元件

核心元件包括:事件模型、位元組緩衝區和通訊API

事件模型

Netty 是基於非同步事件驅動的,該框架體現為所有的I/O操作都是非同步的,呼叫者並不能立即獲得結果,而是通過事件監聽機制,使用者可以方便地主動獲取或者通過通知機制獲得I/O操作的結果。

Netty 將所有的事件按照它們與入站或出站資料流的相關性進行了分類。

可能由入站資料或者相關的狀態更改而觸發的事件包括以下幾項:

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

出站事件是未來將會觸發的某個動作的操作結果,包括以下動作:

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

每個事件都可以被分發到ChannelHandler類中的某個使用者實現的方法。

位元組緩衝區

Netty 使用了區別於Java ByteBuffer 的新的緩衝型別ByteBuf,ByteBuf提供了豐富的特性。

通訊API

Netty 的通訊API都被抽象到Channel裡,以統一的非同步I/O程式設計介面來滿足所有點對點的通訊操作。

傳輸服務

Netty 內建了一些開箱即用的傳輸服務。因為並不是它們所有的傳輸都支援每一種協議,所以必須選擇一個和應用程式所使用的協議相相容的傳輸。以下是Netty提供的所有的傳輸。

NIO

io.netty.channel.socket.nio包用於支援NIO。該包下面的實現是使用java.nio.channels包作為基礎(基於選擇器的方式)。

epoll

io.netty.channel.epoll包用於支援由 JNI 驅動的 epoll 和 非阻塞 IO。

需要注意的是,這個epoll傳輸只能在 Linux 上獲得支援。epoll同時提供多種特性,如:SO_REUSEPORT 等,比 NIO傳輸更快,而且是完全非阻塞的。

OIO

io.netty.channel.socket.oio包用於支援使用java.net包作為基礎的阻塞I/O

本地

io.netty.channel.local包用於支援在 VM 內部通過管道進行通訊的本地傳輸。

內嵌

io.netty.channel.embedded包作為內嵌傳輸,允許使用ChannelHandler而又不需要一個真正的基於網路的傳輸。

協議支援

Netty 支援豐富的網路協議,如TCPUDPHTTPHTTP/2WebSocketSSL/TLS等,這些協議實現開箱即用,因此,Netty 開發者能夠在不失靈活的前提下來實現開發的簡易性、高效能和穩定性。

Netty簡單應用

引入Maven依賴

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

服務端的管道處理器

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    //讀取資料實際(這裡我們可以讀取客戶端傳送的訊息)
    /*
    1. ChannelHandlerContext ctx:上下文物件, 含有 管道pipeline , 通道channel, 地址
    2. Object msg: 就是客戶端傳送的資料 預設Object
     */
    @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 channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //writeAndFlush 是 write + flush
        //將資料寫入到快取,並重新整理
        //一般講,我們對這個傳送的資料進行編碼
        ctx.writeAndFlush(Unpooled.copiedBuffer("公司最近賬戶沒啥錢,再等幾天吧!", CharsetUtil.UTF_8));
    }

    //處理異常, 一般是需要關閉通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

NettyServerHandler繼承自ChannelInboundHandlerAdapter,這個類實現了ChannelInboundHandler介面。ChannelInboundHandler提供了許多事件處理的介面方法。

這裡覆蓋了channelRead()事件處理方法。每當從客戶端收到新的資料時,這個方法會在收到訊息時被呼叫。

channelReadComplete()事件處理方法是資料讀取完畢時被呼叫,通過呼叫ChannelHandlerContextwriteAndFlush()方法,把訊息寫入管道,並最終傳送給客戶端。

exceptionCaught()事件處理方法是,當出現Throwable物件時才會被呼叫。

服務端主程式

public class NettyServer {

    public static void main(String[] args) throws Exception {
        //建立BossGroup 和 WorkerGroup
        //說明
        //1. 建立兩個執行緒組 bossGroup 和 workerGroup
        //2. bossGroup 只是處理連線請求 , 真正的和客戶端業務處理,會交給 workerGroup完成
        //3. 兩個都是無限迴圈
        //4. bossGroup 和 workerGroup 含有的子執行緒(NioEventLoop)的個數
        //   預設實際 cpu核數 * 2
        //
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
        try {
            //建立伺服器端的啟動物件,配置引數
            ServerBootstrap bootstrap = new ServerBootstrap();
            //使用鏈式程式設計來進行設定
            bootstrap.group(bossGroup, workerGroup) //設定兩個執行緒組
                    .channel(NioServerSocketChannel.class) //bossGroup使用NioSocketChannel 作為伺服器的通道實現
                    .option(ChannelOption.SO_BACKLOG, 128) // 設定執行緒佇列得到連線個數 option主要是針對boss執行緒組,
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //設定保持活動連線狀態 child主要是針對worker執行緒組
                    .childHandler(new ChannelInitializer<SocketChannel>() {//workerGroup使用 SocketChannel建立一個通道初始化物件																														(匿名物件)
                        //給pipeline 設定處理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //可以使用一個集合管理 SocketChannel, 再推送訊息時,可以將業務加入到各個channel 對應的 NIOEventLoop 的 									taskQueue 或者 scheduleTaskQueue
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    }); // 給我們的workerGroup 的 EventLoop 對應的管道設定處理器

            System.out.println(".....伺服器 is ready...");
            //繫結一個埠並且同步, 生成了一個 ChannelFuture 物件
            //啟動伺服器(並繫結埠)
            ChannelFuture cf = bootstrap.bind(7788).sync();
            //給cf 註冊監聽器,監控我們關心的事件
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("服務已啟動,埠號為7788...");
                    } else {
                        System.out.println("服務啟動失敗...");
                    }
                }
            });
            //對關閉通道進行監聽
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

NioEventLoopGroup是用來處理I/O操作的多執行緒事件迴圈器。Netty 提供了許多不同的EventLoopGroup的實現來處理不同的傳輸。

上面的服務端應用中,有兩個NioEventLoopGroup被使用。第一個叫作bossGroup,用來接收進來的連線。第二個叫作workerGroup,用來處理已經被接收的連線,一旦 bossGroup接收連線,就會把連線的資訊註冊到workerGroup上。

ServerBootstrap是一個NIO服務的引導啟動類。可以在這個服務中直接使用Channel

  • group方法用於 設定EventLoopGroup
  • 通過Channel方法,可以指定新連線進來的Channel型別為NioServerSocketChannel類。
  • childHandler用於指定ChannelHandler,也就是前面實現的NettyServerHandler
  • 可以通過option設定指定的Channel來實現NioServerSocketChannel的配置引數。
  • childOption主要設定SocketChannel的子Channel的選項。
  • bind用於繫結埠啟動服務。

客戶端管道處理器

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //當通道就緒就會觸發該方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client ctx =" + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("老闆,工資什麼時候發給我啊?", CharsetUtil.UTF_8));
    }

    //當通道有讀取事件時,會觸發
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("伺服器回覆的訊息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("伺服器的地址: "+ ctx.channel().remoteAddress());
    }

    //處理異常, 一般是需要關閉通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

channelRead方法中將接收到的訊息轉化為字串,方便在控制檯上列印出來。

channelRead接收到的訊息型別為ByteBufByteBuf提供了轉為字串的方便方法。

客戶端主程式

public class NettyClient {

    public static void main(String[] args) throws Exception {
        //客戶端需要一個事件迴圈組
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //建立客戶端啟動物件
            //注意客戶端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            //設定相關引數
            bootstrap.group(group) //設定執行緒組
                    .channel(NioSocketChannel.class) // 設定客戶端通道的實現類(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler()); //加入自己的處理器
                        }
                    });
            System.out.println("客戶端 ok..");
            //啟動客戶端去連線伺服器端
            //關於 ChannelFuture 要分析,涉及到netty的非同步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7788).sync();
            //給關閉通道進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

客戶端只需要一個NioEventLoopGroup就可以了。

測試執行

分別啟動伺服器 NettyServer 和客戶端 NettyClient程式

服務端控制檯輸出內容:

.....伺服器 is ready...
服務已啟動,埠號為7788...
server ctx =ChannelHandlerContext(NettyServerHandler#0, [id: 0xa1b2233c, L:/127.0.0.1:7788 - R:/127.0.0.1:63239])
客戶端傳送訊息是:老闆,工資什麼時候發給我啊?
客戶端地址:/127.0.0.1:63239

客戶端控制檯輸出內容:

客戶端 ok..
client ctx =ChannelHandlerContext(NettyClientHandler#0, [id: 0x21d6f98e, L:/127.0.0.1:63239 - R:/127.0.0.1:7788])
伺服器回覆的訊息:公司最近賬戶沒啥錢,再等幾天吧!
伺服器的地址: /127.0.0.1:7788

至此,一個簡單的基於Netty開發的服務端和客戶端就完成了。

總結

本篇文章主要講解了 Netty 產生的背景、特點、核心元件及如何快速開啟第一個 Netty 應用。

後面我們會分析Netty架構設計ChannelChannelHandler、位元組緩衝區ByteBuf執行緒模型編解碼載入程式等方面的知識。

結尾

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

相關文章