Netty介紹

什么是Netty

Netty可以幫助我們快速搭建服務端與客戶端的Socket網絡通信庫,他是對JDK的NIO的封裝

問?什么是IO,什么是NIO
NIO:非阻塞IO編程
IO:阻塞IO編程

什么是IO編程

  • IO編程中服務端實現
public class IOServer {
    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket = new ServerSocket(8000);

        // (1) 接收新連接線程
        new Thread(() -> {
            while (true) {
                try {
                    // (1) 阻塞方法獲取新的連接
                    Socket socket = serverSocket.accept();

                    // (2) 每一個新的連接都創建一個線程,負責讀取數據
                    new Thread(() -> {
                        try {
                            byte[] data = new byte[1024];
                            InputStream inputStream = socket.getInputStream();
                            while (true) {
                                int len;
                                // (3) 按字節流方式讀取數據
                                while ((len = inputStream.read(data)) != -1) {
                                    System.out.println(new String(data, 0, len));
                                }
                            }
                        } catch (IOException e) {
                        }
                    }).start();

                } catch (IOException e) {
                }

            }
        }).start();
    }
}
  • IO編程中客戶端實現
public class IOClient {

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Socket socket = new Socket("127.0.0.1", 8000);
                while (true) {
                    try {
                        socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                        socket.getOutputStream().flush();
                        Thread.sleep(2000);
                    } catch (Exception e) {
                    }
                }
            } catch (IOException e) {
            }
        }).start();
    }
}

缺點:IO編程模型在客戶端較少的情況下運行良好,對于客戶端比較多的業務來說,單機服務端可能需要支撐成千上萬的連接,IO模型可能就不太合適了
原因:每個連接創建成功之后都需要一個線程來維護,每個線程包含一個while死循環,那么1w個連接對應1w個線程,繼而1w個while死循環,這就帶來如下幾個問題:
1.線程資源受限:線程是操作系統中非常寶貴的資源,同一時刻有大量的線程處于阻塞狀態是非常嚴重的資源浪費,操作系統耗不起
2.線程切換效率低下:單機cpu核數固定,線程爆炸之后操作系統頻繁進行線程切換,應用性能急劇下降。
3.IO編程中,我們看到數據讀寫是以字節流為單位,效率不高。

NIO編程

為了解決上面三個問題,NIO橫空出世

線程資源受限

NIO編程模型中,新來一個連接不再創建一個新的線程,而是可以把這條連接直接綁定到某個固定的線程,然后這條連接所有的讀寫都由這個線程來負責。

image

NIO模型中selector:把連接注冊到selector上,然后,通過檢查這個selector來批量監測是否有數據可讀的連接,進而讀取數據。
舉例。

實際開發過程中,我們會開多個線程,每個線程都管理著一批連接,相對于IO模型中一個線程管理一條連接,消耗的線程資源大幅減少

線程切換效率低下

由于NIO模型中線程數量大大降低,線程切換效率因此也大幅度提高

IO讀寫以字節為單位

NIO解決這個問題的方式是數據讀寫不再以字節為單位,而是以字節塊為單位。

IO:每次都是從操作系統底層一個字節一個字節地讀取數據。
NIO:維護一個緩沖區,每次可以從這個緩沖區里面讀取一塊的數據。

NIO服務端實現

public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        new Thread(() -> {
            try {
                // 對應IO編程中服務端啟動
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 監測是否有新的連接,這里的1指的是阻塞的時間為1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isAcceptable()) {
                                try {
                                    // (1) 每來一個新連接,不需要創建一個線程,而是直接注冊到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }

        }).start();

        new Thread(() -> {
            try {
                while (true) {
                    // (2) 批量輪詢是否有哪些連接有數據可讀,這里的1指的是阻塞的時間為1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // (3) 讀取數據以塊為單位批量讀取
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();

    }
}

  1. NIO模型中通常會有兩個線程,每個線程綁定一個輪詢器selector。
    例子中的:
    serverSelector負責輪詢是否有新的連接
    clientSelector負責輪詢連接是否有數據可讀
  2. 服務端監測到新的連接之后,不再創建一個新的線程,而是直接將新連接綁定到clientSelector上。
  3. clientSelector被一個while死循環包裹著,如果在某一時刻有多條連接有數據可讀,那么通過 clientSelector.select(1)方法可以輪詢出來,進而批量處理。
  4. 數據的讀寫以內存塊為單位。

原生NIO缺點:
1、JDK的NIO編程需要了解很多的概念,編程復雜,對NIO入門非常不友好,編程模型不友好,ByteBuffer的api簡直反人類
2、對NIO編程來說,一個比較合適的線程模型能充分發揮它的優勢,而JDK沒有給你實現,你需要自己實現,就連簡單的自定義協議拆包都要你自己實現
3、JDK的NIO的selector該實現飽受詬病的空輪訓bug會導致cpu飆升100%

Netty編程

簡單來說:Netty封裝了JDK的NIO
官方說明:Netty是一個異步事件驅動的網絡應用框架,用于快速開發可維護的高性能服務器和客戶端。

使用Netty不使用JDK原生NIO的原因:

  • 編程簡單:使用JDK自帶的NIO需要了解太多的概念,編程復雜,一不小心bug橫飛
  • io切換:Netty底層IO模型隨意切換,而這一切只需要做微小的改動,改改參數,Netty可以直接從NIO模型變身為IO模型
  • Netty自帶的拆包解包:異常檢測等機制讓你從NIO的繁重細節中脫離出來,讓你只需要關心業務邏輯
  • niobug:Netty解決了JDK的很多包括空輪詢在內的bug
  • nio優化:Netty底層對線程,selector做了很多細小的優化,精心設計的reactor線程模型做到非常高效的并發處理
  • 協議棧:自帶各種協議棧讓你處理任何一種通用協議都幾乎不用親自動手

Netty服務端

public class NettyServer {
    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup boos = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
                .group(boos, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                .bind(8000);
    }
}

1.boos對應,IOServer.java中的接受新連接線程,主要負責創建新連接
2.worker對應 IOClient.java中的負責讀取數據的線程,主要用于讀取數據以及業務邏輯處理

Netty客戶端

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();

        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                });

        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

        while (true) {
            channel.writeAndFlush(new Date() + ": hello world!");
            Thread.sleep(2000);
        }
    }
}

Netty重要類介紹

EventLoopGroup

事件循環組(NioEventLoopGroup是異步事件循環組)。EventLoopGroup中包含了多個EventLoop。主要提供了兩類方法:
① next()方法用于返回下一個EventLoop來使用。
② register方法,來將一個Channel注冊到EventLoop當中,同時返回一個ChannelFuture,當注冊完成的時候這個ChannelFuture將得到一個通知。可以見該方法是一個異步的方法,在調用完register后會立即返回,然后我們根據ChannelFuture中的相關方法來判斷注冊操作是否完成。

NioEventLoopGroup ——— 異步事件循環組

它是MultithreadEventLoopGroup的一個實現,它是用于基于Channel的NIO Selector。

EventLoop

事件循環類。將處理一個已經注冊到該EventLoop的Channel的所有I/O操作。
服務I/O和Channels事件的EventLoops包含在一個EventLoopGroup里。EventLoops被創建和分配的方式是根據傳輸實現而有所不同。

image.png
NioEventLoop
  1. NioEventLoop是一個基于JDK NIO的異步事件循環類,它負責處理注冊在它其中的Channel的所有事件。
  2. NioEventLoop的整個生命周期只會依賴于一個單一的線程來完成。
  3. 一個NioEventLoop可以分配給多個Channel,NioEventLoop通過JDK Selector來實現I/O多路復用,以對多個Channel進行管理。
  4. 如果調用Channel操作的線程是EventLoop所關聯的線程(注冊到Selector的Channel的監控如連接、讀、寫操作)那么該操作會被立即執行。否則會將該操作封裝成任務放入EventLoop的任務隊列中。
image
SingleThreadEventExecutor
  • 它會執行所有提交的任務在一個單一的線程中。而OrderedEventExecutor作為一個標記接口,它會執行所有提交的任務以有序/連續的方式
  • 持有一個MpscQueue taskQueue成員變量,來維護提交上來的任務。
  • SingleThreadEventExecutor中還維護有該線程的五個狀態:a)ST_NOT_STARTED;b)ST_STARTED;c)ST_SHUTTING_DOWN;d)ST_SHUTDOWN;e)ST_TERMINATED。

SingleThreadEventExecutor的execute(Runnable task)方法:

image

image
  • execute方法會接收一個個任務,將任務依次放入taskQueue
  • ThreadPerTaskExecutor.execute(Runnable)來創建并啟動執行任務的唯一線程
    任務線程會在滿足如下條件時被創建并執行:
  • a) 提交任務的線程不為EventLoop所關聯的線程
    b) EventLoop所關聯的線程還不存在,即EventLoop所關聯的線程的狀態為ST_NOT_STARTED.
    如我們的啟動程序“serverBootstrap.bind(8080)”就會觸發EventLoop所關聯的線程創建并執行。
  • SingleThreadEventExecutor的doStartThread()方法:會調用SingleThreadEventExecutor.this.run();而這是SingleThreadEventExecutor的一個抽象方法,實際上會調用NioEventLoop類的run()方法,是的我們又回到了NioEventLoop類中,這是一個很重要的方法。
Run事件循環

run為NioEventLoop內的事件循環方法

image
image

NioEventLoop的事件循環主要完成下面幾件事:

  1. 根據當前NioEventLoop中是否有待完成的任務得出select策略,進行相應的select操作
  • 如果‘selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())’操作返回的是一個>0的值,則說明有就緒的的I/O事件待處理,則直接進入流程②處理。
  • 否則,如果返回的是’SelectStrategy.SELECT’則進行select(wakenUp.getAndSet(false))操作:
    首先先通過自旋鎖(自旋 + CAS)方式獲得wakenUp當前的標識,并再將wakenUp標識設置為false。將wakenUp作為參數傳入select(boolean oldWakenUp)方法中。
  • 注意這個select方法不是JDK NIO的Selector.select方法,是NioEventLoop類自己實現的一個方法,只是方法名一樣而已。NioEventLoop的這個select方法還做了一件很重要的時,就是解決“JDK NIO類庫的selector bug”問題。
  1. 處理select操作得到的已經準備好處理的I/O事件,以及處理提交到當前EventLoop的任務(包括定時和周期任務)。
  2. 如果NioEventLoop所在線程執行了關閉操作,則執行相關的關閉操作處理

ChannelHandler 和 ChannelPipeline

ChannelHandler

Channel生命周期

Channel接口定義了簡單但強大的狀態模式來緊密的聯系ChannelInboundHandler API。包括了4種狀態


image

Channel正常生命周期狀態改變如下圖:

image

ChannelPipeline

  • ChannelPipeline是一個ChannelHandler的集合,這些ChannelHandler會處理或攔截一個Channel的入站事件或出站操作。
  • 每一個新的Channel被創建時都會分配一個新的ChannelPipeline。他們的關系是永久不變的,Channel既不能依附于其他ChannelPipeline也不能和當前ChannelPipeline分離。
  • ChannelPipeline根本上是一系列的ChannelHandlers。ChannelPipeline還提供了方法用于傳播事件通過ChannelPipeline本身。
  • ChannelPipeline是一個實現了攔截過濾器模式的高級形式,它使得用戶能夠完全控制事件的處理方式以及ChannelPipeline中的ChannelHandler間的交互。
  • 隨后該事件通過一個ChannelHandlerContext來實現傳遞給下一個具有一樣父類的處理器,即同一個方向的處理器。
  • 一個事件要么被一個ChannelInboundHandler處理要么被一個ChannelOutboundHandler處理。隨后該事件通過一個ChannelHandlerContext來實現傳遞給ChannelPipeline中的下一個具有一樣父類的處理器,即同一個方向的處理器。
  • ChannelHandler在ChannelPipeline中的順序是由我們通過ChannelPipeline.add*()方法來確定的。
    下圖描述了ChannelPipeline中的ChannelHandlers是如何處理I/O事件的
image

下表列出了ChannelInboundHandler接口生命周期的所有方法。這些方法將被調用在接收數據或者Channel相關狀態改變時。正如我們早前提及的,這些方法與Channel的生命周期緊密映射。

image

相同的方法通過ChannelHandlerContext被調用時,它將從當前關聯的ChannelHandler開始并傳播給管道中下一個能夠處理該事件的ChannelHandler

HeadContext 與 TailContext

image
  1. 賦值NioServerSocketChannel給成員變量channel
  2. 根據channel構建SucceededChannelFuture、VoidChannelPromise成員變量
  3. 構建ChannelHandler鏈表頭HeadContext和鏈表尾TailContext,并賦值給成員變量head、tail。將head.next指向tail,將tail.prev指向head。

HeadConext作為鏈表頭,它同時實現了ChannelOutboundHandler和ChannelInboundHandler,也就是說它即會處理入站數據也會處理出站數據、它持有NioMessageUnsafe對象,該類用于完成Channel真實的I/O操作和傳輸。

TailContext作為ChannelHandler鏈中的最后一個ChannelHandler,它僅實現了ChannelInboundHandler,因此TailContext是入站事件的最后一個ChannelHandler,它主要完成了一些資源的釋放工作。

入站事件會依次被從head ——> ... ——> tail中的所有ChannelInboundHandler處理。
出站事件會依次被從tail ——> ... ——> head中的所有ChannelOutboundHandler處理。
注意,我們程序中通過add*(...)加進來的ChannelHandler都會處于head和tail之間,也就是說鏈表頭是HeadConext,鏈表尾是TailContext,這是固定不會改變的。

image

Bootstrapping

  • 引導客戶端和服務器端


    image.png

1.ServerBootstrap綁定本地port, Bootstrap綁定遠程host和port
2.服務端(ServerBootstrap)需要兩個EventLoopGroup( 這兩個可以是同一個實例 )。客戶端需要一個EventLoopGroup。
一個服務端需要兩個不同的Channel集合。第一個集合包含了ServerChannel,該ServerChannel代表服務自己所監聽的綁定本地端口的socket。第二個集合將包含所有已經創建了的Channel,這些Channel ( 該Channel由ServerChannel創建 )用于處理客戶端連接,服務端收到的每一個客戶端的連接都將創建一個Channel。

引用參考:

  1. 《跟閃電俠學Netty》開篇:Netty是什么?
  2. 《深入探索Netty原理及源碼分析》文集小結
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容