Netty Bootstrap

 轉:https://www.cnblogs.com/crazymakercircle/p/9998643.html

圖解幾個重要概念

下面的幾個概念,非常重要。

之前沒有認真介紹,下面圖解說明一下。

父子 channel

​ 在 Netty 中, Channel 是一個 Socket 連接的抽象, 它爲用戶提供了關於底層 Socket 狀態(是否是連接還是斷開) 以及對 Socket 的讀寫等操作。

​ 每當 Netty 建立了一個連接後, 都會有一個對應的 Channel 實例。

​ 並且,有父子channel 的概念。 服務器連接監聽的channel ,也叫 parent channel。 對應於每一個 Socket 連接的channel,也叫 child channel。

EventLoop 線程與線程組

在看本文之前,如果不明白 reactor 線程和reactor模式,請 查看 瘋狂創客圈的專門文章:Reactor模式 。

在Netty 中,每一個 channel 綁定了一個thread 線程。

一個 thread 線程,封裝到一個 EventLoop , 多個EventLoop ,組成一個線程組 EventLoopGroup。

反過來說,EventLoop 這個相當於一個處理線程,是Netty接收請求和處理IO請求的線程。 EventLoopGroup 可以理解爲將多個EventLoop進行分組管理的一個類,是EventLoop的一個組。

他們的對應關係,大致如下:

在這裏插入圖片描述

通道與Reactor線程組

​ 這裏主要是涉及的是服務器端。

​ 服務器端,一般有設置兩個線程組,監聽連接的 parent channel 工作在一個獨立的線程組,這裏名稱爲boss線程組(有點像負責招人的包工頭)。

​ 連接成功後,負責客戶端連接讀寫的 child channel 工作在另一個線程組,這裏名稱爲 worker 線程組,專門負責搬數據(有點兒像搬磚)。
在這裏插入圖片描述

Channel 通道的類型

​ 除了 TCP 協議以外, Netty 還支持很多其他的連接協議, 並且每種協議還有 NIO(異步 IO) 和 OIO(Old-IO, 即傳統的阻塞 IO) 版本的區別。

不同協議不同的阻塞類型的連接都有不同的 Channel 類型與之對應,下面是一些常用的 Channel 類型:
  • NioSocketChannel, 代表異步的客戶端 TCP Socket 連接.
  • NioServerSocketChannel, 異步的服務器端 TCP Socket 連接.
  • NioDatagramChannel, 異步的 UDP 連接
  • NioSctpChannel, 異步的客戶端 Sctp 連接.
  • NioSctpServerChannel, 異步的 Sctp 服務器端連接.
  • OioSocketChannel, 同步的客戶端 TCP Socket 連接.
  • OioServerSocketChannel, 同步的服務器端 TCP Socket 連接.
  • OioDatagramChannel, 同步的 UDP 連接
  • OioSctpChannel, 同步的 Sctp 服務器端連接.
  • OioSctpServerChannel, 同步的客戶端 TCP Socket 連接.

啓動器初步介紹

Bootstrap 是 Netty 提供的一個便利的工廠類,可以通過它來完成 Netty 的客戶端或服務器端的 Netty 初始化。

當然,Netty 的官方解釋說,可以不用這個啓動器。

但是,一點點去手動創建channel 並且完成一些的設置和啓動,會非常麻煩。還是使用這個便利的工具類,會比較好。

有兩個啓動器,分別應用在服務器和客戶端。

如下圖:

在這裏插入圖片描述

兩個啓動器大致的配置,都是相同的。

下面以服務器serverBootstrap 啓動類爲主要的介紹對象。

圖解 Bootstrap執行流程

​ 首先,創建了一個引導器 ServerBootstrap 實例,這個專門用於引導服務端的啓動工作,直接new 創建即可。(客戶端的引導器差不多,不過是創建Bootstrap 實例)

// 啓動引導器 private static ServerBootstrap b = new ServerBootstrap();

​ 啓動一個Bootstrap,大致有8步,如下圖:
在這裏插入圖片描述

代碼如下:

 
    try {   //1 設置reactor 線程
            b.group(bossLoopGroup, workerLoopGroup);
            //2 設置nio類型的channel
            b.channel(NioServerSocketChannel.class);
            //3 設置監聽端口
            b.localAddress(new InetSocketAddress(port));
            //4 設置通道選項
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

            //5 裝配流水線
            b.childHandler(new ChannelInitializer<SocketChannel>()
            {
                //有連接到達時會創建一個channel
                protected void initChannel(SocketChannel ch) throws Exception
                {
                    ch.pipeline().addLast(new ProtobufDecoder());
                    ch.pipeline().addLast(new ProtobufEncoder());
                    // pipeline管理channel中的Handler
                    // 在channel隊列中添加一個handler來處理業務
                    ch.pipeline().addLast("serverHandler", serverHandler);
                }
            });
            // 6 開始綁定server
            // 通過調用sync同步方法阻塞直到綁定成功

            ChannelFuture channelFuture = b.bind().sync();
            LOGGER.info(ChatServer.class.getName() +
                    " started and listen on " + 
                    channelFuture.channel().localAddress());

            // 7 監聽通道關閉事件
            // 應用程序會一直等待,直到channel關閉
            ChannelFuture closeFuture=  channelFuture.channel().closeFuture();
            closeFuture.sync();
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally
        {
            // 8 優雅關閉EventLoopGroup,
            // 釋放掉所有資源包括創建的線程
            workerLoopGroup.shutdownGracefully();
            bossLoopGroup.shutdownGracefully();
        }

 

​ 接下來就是精彩的8個步驟

1:設置reactor 線程組

​ 在設置 reactor 反應器線程組之前,創建了兩個 NioEventLoopGroup 線程組:

  • bossLoopGroup 表示服務器連接監聽線程組,專門接受 accept 新的客戶端client 連接

  • workerGroup 表示處理每一條連接的數據收發的線程組

    在線程組和啓動器都創建完成後,就可以開始設置線程組:通過 b.group(bossGroup, workerGroup) 方法,給引導器配置兩大線程組。

    配置完成之後,整個引導類的 reactor 線程正式確定。這裏確定的工作模式,爲父子線程的模型。

也可以不設置兩個線程組,只設置一個線程組。

如果只設置一個線程組,具體的方法爲 —— b.group( workerGroup) 。

配置完成一個線程組,則所有的 channel ,包括服務監聽通道父親channel 和所有的子channel ,都工作在同一個線程組中。

說明一下,一個線程組,可不止一條線程哈。

在這裏插入圖片描述

2 :設置通道的IO類型

Netty 不止支持 Java NIO ,也支持阻塞式的 BIO (在Netty 中 叫做OIO)。

這裏配置的是NIO,方法如下。

//2 設置nio類型的channel b.channel(NioServerSocketChannel.class);

如果想指定 IO 模型爲 BIO,那麼這裏配置上Netty的 OioServerSocketChannel.class 類型即可。由於NIO 的優勢巨大,通常不會在Netty中使用BIO。

3:設置監聽端口

//3 設置監聽端口 b.localAddress(new InetSocketAddress(port));

這是最爲簡單的一步操作。

4:設置通道參數

  • childOption() 方法

    給每條child channel 連接設置一些TCP底層相關的屬性,比如上面,我們設置了兩種TCP屬性,其中 ChannelOption.SO_KEEPALIVE表示是否開啓TCP底層心跳機制,true爲開

  • option() 方法

    對於server bootstrap而言,這個方法,是給parent channel 連接設置一些TCP底層相關的屬性。

    TCP連接的參數詳細介紹如下。

option設置的參數:

SO_RCVBUF ,SO_SNDBUF

這兩個選項就是來設置TCP連接的兩個buffer尺寸的。

每個TCP socket在內核中都有一個發送緩衝區和一個接收緩衝區,TCP的全雙工的工作模式以及TCP的滑動窗口便是依賴於這兩個獨立的buffer以及此buffer的填充狀態。

SO_SNDBUF
  Socket參數,TCP數據發送緩衝區大小。該緩衝區即TCP發送滑動窗口,linux操作系統可使用命令:cat /proc/sys/net/ipv4/tcp_smem 查詢其大小。

TCP_NODELAY
  TCP參數,立即發送數據,默認值爲Ture(Netty默認爲True而操作系統默認爲False)。該值設置Nagle算法的啓用,改算法將小的碎片數據連接成更大的報文來最小化所發送的報文的數量,如果需要發送一些較小的報文,則需要禁用該算法。Netty默認禁用該算法,從而最小化報文傳輸延時。

​ 這個參數,與是否開啓Nagle算法是反着來的,true表示關閉,false表示開啓。通俗地說,如果要求高實時性,有數據發送時就馬上發送,就關閉,如果需要減少發送次數減少網絡交互,就開啓。

SO_KEEPALIVE
  底層TCP協議的心跳機制。Socket參數,連接保活,默認值爲False。啓用該功能時,TCP會主動探測空閒連接的有效性。可以將此功能視爲TCP的心跳機制,需要注意的是:默認的心跳間隔是7200s即2小時。Netty默認關閉該功能。

SO_REUSEADDR
  Socket參數,地址複用,默認值False。有四種情況可以使用:
(1).當有一個有相同本地地址和端口的socket1處於TIME_WAIT狀態時,而你希望啓動的程序的socket2要佔用該地址和端口,比如重啓服務且保持先前端口。
(2).有多塊網卡或用IP Alias技術的機器在同一端口啓動多個進程,但每個進程綁定的本地IP地址不能相同。
(3).單個進程綁定相同的端口到多個socket上,但每個socket綁定的ip地址不同。(4).完全相同的地址和端口的重複綁定。但這隻用於UDP的多播,不用於TCP。

SO_LINGER
  Socket參數,關閉Socket的延遲時間,默認值爲-1,表示禁用該功能。-1表示socket.close()方法立即返回,但OS底層會將發送緩衝區全部發送到對端。0表示socket.close()方法立即返回,OS放棄發送緩衝區的數據直接向對端發送RST包,對端收到復位錯誤。非0整數值表示調用socket.close()方法的線程被阻塞直到延遲時間到或發送緩衝區中的數據發送完畢,若超時,則對端會收到復位錯誤。

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

b.option(ChannelOption.SO_BACKLOG, 1024)

表示系統用於臨時存放已完成三次握手的請求的隊列的最大長度,如果連接建立頻繁,服務器處理創建新連接較慢,可以適當調大這個參數.

SO_BROADCAST
  Socket參數,設置廣播模式。

5: 裝配流水線

​ ChannelPipeline 這是Netty處理請求的責任鏈,這是一個ChannelHandler的鏈表,而ChannelHandler就是用來處理網絡請求的內容的。

​ 每一個channel ,都有一個處理器流水線。

​ 裝配 child channel 流水線,調用 childHandler()方法,傳遞一個ChannelInitializer 的實例。

​ 在 child channel 創建成功,開始通道初始化的時候,在bootstrap啓動器中配置的 ChannelInitializer 實例就會被調用。

​ 這個時候,才真正的執行去執行 initChannel 初始化方法,開始通道流水線裝配。

​ 流水線裝配,主要是在流水線pipeline 的後面,增加負責數據讀寫、處理業務邏輯的handler。

b.childHandler(new ChannelInitializer<SocketChannel>() { //有連接到達時會創建一個channel protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ProtobufDecoder()); ch.pipeline().addLast(new ProtobufEncoder()); // pipeline管理channel中的Handler // 在channel隊列中添加一個handler來處理業務 ch.pipeline().addLast("serverHandler", serverHandler); } });

說明一下,ChannelInitializer這個類中,有一個泛型參數 SocketChannel,這裏的類型,需要和前面的Channel類型對應上。

順便說一下處理器。

處理器 ChannelHandler 用來處理網絡請求內容,有ChannelInboundHandler和ChannelOutboundHandler兩種,ChannlPipeline會從頭到尾順序調用ChannelInboundHandler處理網絡請求內容,從尾到頭調用ChannelOutboundHandler 處理網絡請求內容。

pipeline 流水線的圖,大致如下:

img

如何裝配parent 通道呢?

使用serverBootstrap.handler() 方法 。 handler()方法,可以和前面分析的childHandler()方法對應起來。childHandler()用於指定處理新連接數據的讀寫處理邏輯。 handler()方法裝配parent 通道。

比方說:

serverBootstrap.handler(new ChannelInitializer() { protected void initChannel(NioServerSocketChannel ch) { System.out.println("服務端啓動中"); } } )

handler()用於指定在服務端啓動過程中的一些邏輯,通常情況下呢,我們用不着這個方法。

6: 開始綁定server

// 通過調用sync同步方法阻塞直到綁定成功 ChannelFuture channelFuture = b.bind().sync(); LOGGER.info(ChatServer.class.getName() + " started and listen on " + channelFuture.channel().localAddress());

這個也很簡單。

7: ChannelFuture

ChannelFuture 在Netty中的所有的I/O操作都是異步執行的,這就意味着任何一個I/O操作會立刻返回,不保證在調用結束的時候操作會執行完成。因此,會返回一個ChannelFuture的實例,通過這個實例可以獲取當前I/O操作的狀態。

// 7 監聽通道關閉事件 // 應用程序會一直等待,直到channel關閉 ChannelFuture closeFuture= channelFuture.channel().closeFuture(); closeFuture.sync();

對於客戶端來說,Bootstrap是開發netty客戶端的基礎,通過Bootstrap的connect方法來連接服務器端。該方法返回的也是ChannelFuture。

8 優雅關閉EventLoopGroup

// 8 優雅關閉EventLoopGroup, // 釋放掉所有資源包括創建的線程 workerLoopGroup.shutdownGracefully(); bossLoopGroup.shutdownGracefully();

這個,會關閉所有的child channel,這是非常重要的。

關閉之後,會釋放掉底層的資源,如TCP Socket 文件描述符,等等。

 

9 UDP 廣播服務器端實現代碼:

package com.zst.aim.config;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.util.CharsetUtil;

/**
 * 
 * @ClassName: NettyUdpConfig
 * @Description:udp監聽
 * @author: zhangzeyin
 * @date: 2021年4月25日 上午9:42:09
 * @Copyright:
 */
public class NettyUdpConfig {

    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 
     * 啓動服務
     */

    public void bind(Integer prot) {

        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            // UDP方式使用Bootstrap
            Bootstrap serverBootstrap = new Bootstrap();
            serverBootstrap = serverBootstrap.group(eventLoopGroup);
            serverBootstrap = serverBootstrap.channel(NioDatagramChannel.class);
            serverBootstrap = serverBootstrap.option(ChannelOption.SO_BROADCAST, true); //廣播
            serverBootstrap = serverBootstrap.handler(getSimpleChannelInboundHandler()); //處理函數
            ChannelFuture f = serverBootstrap.bind(prot).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }

    private SimpleChannelInboundHandler<DatagramPacket> getSimpleChannelInboundHandler() {

        return new SimpleChannelInboundHandler<DatagramPacket>() {

            @Override
            protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
                try {
                    String strdata = msg.content().toString(CharsetUtil.UTF_8);
                    if (strdata.equals("server")) {
                        ctx.writeAndFlush(new DatagramPacket(
                                Unpooled.copiedBuffer(objectMapper.writeValueAsString(LoadServiceConfig.getList()),
                                        CharsetUtil.UTF_8),
                                msg.sender()));
                    }

                } catch (Exception e) {

                }
            }

        };
    }

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章