netty(七)初識Netty-EventLoop介紹 一、什麼是Netty? 二、使用Netty的著名組件有哪些? 三、簡單使用 三、主要組件

一、什麼是Netty?

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

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

二、使用Netty的著名組件有哪些?

  • Cassandra - nosql 數據庫
  • Spark - 大數據分佈式計算框架
  • Hadoop - 大數據分佈式存儲框架
  • RocketMQ - 阿里巴巴開源的消息隊列
  • ElasticSearch - 搜索引擎
  • gRPC - rpc 框架
  • Dubbo - rpc 框架
  • Spring 5.x - flux api 完全拋棄了 tomcat ,使用 netty 作爲服務器端
  • Zookeeper - 分佈式協調框架

三、簡單使用

下面我們先簡單看下如何使用Netty完成hello world。

在java中使用Netty,必然要引入其依賴。關於Netty的版本變更如下:

  • 2.x 2004
  • 3.x 2008
  • 4.x 2013
  • 5.x 已廢棄(沒有明顯的性能提升,維護成本高)

如上所示,5.x沒有太大的意義,所以我們這裏也是用maven4.x的版本,首先在項目中引入maven依賴:

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.52.Final</version>
</dependency>

服務端代碼:

public class HelloWorldServer {

    public static void main(String[] args) {
        new ServerBootstrap()
                // 1、創建 NioEventLoopGroup,可以簡單理解爲 `線程池 + Selector`
                .group(new NioEventLoopGroup())
                // 2、選擇服務 ServerSocketChannel 實現類,這裏選擇Nio
                .channel(NioServerSocketChannel.class)
                // 3、此處是給客戶端SocketChannel使用,ChannelInitializer執行一次,待客戶端建立連接後,執行initChannel,添加更多的處理器
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        // 5、客戶端SocketChannel處理器,解碼:ByteBuffer -> String
                        ch.pipeline().addLast(new StringDecoder());
                        // 6、客戶端SocketChannel業務處理器,使用上一個處理器的結果
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                // 4、服務端ServerSocketChannel綁定監聽端口
                .bind(8080);
    }
}

客戶端代碼:

public class HelloWorldClient {

    public static void main(String[] args) throws InterruptedException {
        new Bootstrap()
                // 1、創建 NioEventLoopGroup,可以簡單理解爲 `線程池 + Selector`
                .group(new NioEventLoopGroup())
                // 2、選擇服務 ServerSocketChannel 實現類,這裏選擇Nio
                .channel(NioSocketChannel.class)
                // 3、此處是給客戶端SocketChannel使用,ChannelInitializer執行一次,待客戶端建立連接後,執行initChannel,添加更多的處理器
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        // 8、消息會經過通道 handler 處理,這裏是將 String => ByteBuf 發出
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                // 4、指定要連接的服務器端口
                .connect("127.0.0.1", 8080)
                // 5、同步方法,等待connect()連接完畢
                .sync()
                // 6、獲取channel對象,即通道,可讀寫操作
                .channel()
                // 7、寫入消息並清空緩衝區
                .writeAndFlush(new Date() + ": hello world!");
    }
}

結果:

Tue Nov 09 10:21:18 CST 2021: hello world!

可以使用以下方式快速理解和記憶:

  • 把 channel 理解爲數據的通道
  • 把 msg 理解爲流動的數據,最開始輸入是 ByteBuf,但經過 pipeline 的加工,會變成其它類型對象,最後輸出又變成 ByteBuf
  • 把 handler 理解爲數據的處理工序
    • 工序有多道,合在一起就是 pipeline,pipeline 負責發佈事件(讀、讀取完成...)傳播給每個 handler, handler 對自己感興趣的事件進行處理(重寫了相應事件處理方法)
    • handler 分 Inbound 和 Outbound 兩類
  • 把 eventLoop 理解爲處理數據的工人
    • 工人可以管理多個 channel 的 io 操作,並且一旦工人負責了某個 channel,就要負責到底(綁定)
    • 工人既可以執行 io 操作,也可以進行任務處理,每位工人有任務隊列,隊列裏可以堆放多個 channel 的待處理任務,任務分爲普通任務、定時任務
    • 工人按照 pipeline 順序,依次按照 handler 的規劃(代碼)處理數據,可以爲每道工序指定不同的工人

三、主要組件

3.1 EventLoop

根據其名稱直譯叫做“事件循環”對象,其用於處理channel的IO操作。

EventLoop 本質是一個單線程執行器(同時維護了一個 Selector),裏面有 run 方法處理 Channel 上源源不斷的 io 事件。

其依賴關係如下:

如上如所示,其繼承關係較複雜。

  • 一條線是繼承自 j.u.c.ScheduledExecutorService 因此包含了線程池中所有的方法。
  • 另一條線是繼承自 netty 自己的 OrderedEventExecutor。

3.2 EventLoopGroup

事件循環組。

EventLoopGroup 是一組 EventLoop,Channel 一般會調用 EventLoopGroup 的 register 方法來綁定其中一個 EventLoop,後續這個 Channel 上的 io 事件都由此 EventLoop 來處理(保證了 io 事件處理時的線程安全)

  • 繼承自 netty 自己的 EventExecutorGroup
    • 實現了 Iterable 接口提供遍歷 EventLoop 的能力
    • 另有 next 方法獲取集合中下一個 EventLoop

3.2.1 常用EventLoopGroup

主要有以下兩種EventLoopGroup:
1)NioEventLoopGroup:處理IO事件,普通任務,定時任務
2)DefaultEventLoopGroup:處理普通任務,定時任務

這裏先給出一個結論,EventLoopGroup當中的每一個EventLoop,和客戶端Channel實際是綁定的:簡單來說,就是一個channel發送的內容,會被同一個線程處理,後面代碼會體現。

3.2.2 EventLoopGroup代碼使用示例

下面以NioEventLoopGroup爲例,介紹簡單使用:

3.2.2.1 遍歷EventLoopGroup:

public class TestEventLoopGroup {

    public static void main(String[] args) {
        //構造方法可以指定線程數,默認不設置會首先根據Netty的環境變量,否則根據線程核心數*2,最小爲1
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(2);

        // 使用期next方法 獲取內部的EventLoop
        System.out.println(nioEventLoopGroup.next());
        System.out.println(nioEventLoopGroup.next());
        System.out.println(nioEventLoopGroup.next());

        System.out.println("-------------------------------------");

        // for循環獲取內部的EventLoop
        for (EventExecutor group: nioEventLoopGroup) {
            System.out.println(group);
        }
    }
}

結果:

io.netty.channel.nio.NioEventLoop@294425a7
io.netty.channel.nio.NioEventLoop@67d48005
io.netty.channel.nio.NioEventLoop@294425a7
-------------------------------------
io.netty.channel.nio.NioEventLoop@294425a7
io.netty.channel.nio.NioEventLoop@67d48005

Process finished with exit code 0

3.2.2.2 執行普通任務 和 定時任務

public class TestEventLoopGroup {

    public static void main(String[] args) throws InterruptedException {
        //構造方法可以指定線程數,默認不設置會首先根據Netty的環境變量,否則根據線程核心數*2,最小爲1
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(2);

        // 執行普通任務
        nioEventLoopGroup.next().execute(TestEventLoopGroup::print);

        // 執行定時任務,延後一秒打印
        System.out.println(new Date());
        nioEventLoopGroup.next().schedule(TestEventLoopGroup::print, 1000, TimeUnit.MILLISECONDS);

    }

    private static void print() {
        System.out.println(new Date() + " " +Thread.currentThread());
    }
}

結果:

Tue Nov 09 14:45:50 CST 2021
Tue Nov 09 14:45:50 CST 2021 Thread[nioEventLoopGroup-2-1,10,main]
Tue Nov 09 14:45:51 CST 2021 Thread[nioEventLoopGroup-2-2,10,main]

3.2.2.3 執行IO任務:

其實所謂IO任務就是,客戶端和服務端的通信,在此例子的基礎上,我們在添加一個職責劃分的概念。

1)職責劃分1
何爲職責劃分?就是在我們創建EventLoopGroup時,指定兩個,不使用單一的一個,讓職責更加明確,其構造方法如下所示:

    /**
      * 爲父級(接受者)和子級(客戶端)設置EventLoopGroup 。
      */
    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
        return this;
    }

在上面的構造當中,第一個參數負責ServerSocketChannel的accept操作,而第二個參數負責SocketChannel的讀寫。

關於第一個參數是否需要指定線程數據呢?實際只需要指定爲1即可,因爲ServerSocketChannel只有一個,只會綁定一個EventLoop。

第二個參數是工作線程,根據實際工作需要設置,默認不設置會首先根據Netty的環境變量,否則根據線程核心數*2,最小爲1。

下面進行代碼的演示,模擬兩個EventLoop處理事件。

根據前面的分析,這裏我們設置兩個EventLoopGroup,第一個給1或者不設置,第二個需要注意了,如果給1的話,那表示只會有一個線程在進行SocketChannel的讀寫操作,並不是兩個線程同時操作,所以下面我們給第二個參數的線程數設置爲2。

服務端代碼:

public class EventLoopGroupServer {

    public static void main(String[] args) throws InterruptedException {
        new ServerBootstrap()
                .group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                                System.out.println(Thread.currentThread().getName() + ": " + s);
                            }
                        });
                    }
                }).bind(8080).sync();
    }
}

分別依次啓動三個客戶端,每個客戶端發送兩個不同的字符串消息(分別是aaa,bbb,ccc),下面只提供aaa,看結果。
客戶端代碼:

public class EventLoopGroupClient {

    public static void main(String[] args) throws InterruptedException {
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup(1))
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        System.out.println("init...");
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                .channel(NioSocketChannel.class).connect("localhost", 8080)
                .sync()
                .channel();

        channel.writeAndFlush("aaa");
        Thread.sleep(1000);
        channel.writeAndFlush("aaa");
    }
}

結果:

nioEventLoopGroup-3-1: aaa
nioEventLoopGroup-3-1: aaa
nioEventLoopGroup-3-2: bbb
nioEventLoopGroup-3-2: bbb
nioEventLoopGroup-3-1: ccc
nioEventLoopGroup-3-1: ccc

根據結果我們得到結論:

客戶端SocketChannel會和EventLoop進行綁定,後面發送的消息,依然由其處理。
下一個客戶端連接後,會默認輪詢到下一個EventLoop。
第三個客戶端來的時候,又會連接第一EventLoop,其內部是多路複用,一個EventLoop管理多個channel。

2)職責劃分2
前面講了一種職責劃分,是在ServerSocketChannel和SocketChannel的劃分。

針對SocketChannel還可以進一步的劃分,實現方式就是我們可以指定另外一個EventLoopGroup,具體如下所示:

public class EventLoopGroupServer {

    public static void main(String[] args) throws InterruptedException {
        // 定義一個defaultEventLoopGroup,對職責進一步劃分
        DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();

        new ServerBootstrap()
                .group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        // 解碼處理器
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                                System.out.println(Thread.currentThread().getName() + ": " + s);
                                // 要想使下了一個處理器,能夠收到此處理器的結果,需要使用西面這個方法傳遞
                                channelHandlerContext.fireChannelRead(s);
                            }
                            // 添加另一個處理器,使用額外的EventLoopGroup
                        }).addLast(defaultEventLoopGroup,"otherHandler",new SimpleChannelInboundHandler<String>(){
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                                System.out.println(Thread.currentThread().getName() + ": " + s);
                            }
                        });
                    }
                }).bind(8080).sync();
    }
}

注意使用 channelHandlerContext.fireChannelRead(s)在處理器傳遞。

客戶端隨便發送個消息,我們看看兩個處理器的打印內容:

nioEventLoopGroup-4-2: Tue Nov 09 15:44:47 CST 2021: hello world!
defaultEventLoopGroup-2-2: Tue Nov 09 15:44:47 CST 2021: hello world!

連個處理內容分別通過不同的EventLoop處理,分別是原本的nio和後創建的default。

並且,同一個客戶端再次發送內容,此處兩個線程仍然會是和channel進行綁定。不具體演示了。

我們最後看下上述的兩個處理器如何切換不同的線程處理的?

通過 channelHandlerContext.fireChannelRead(s)向下跟蹤,到以下代碼處:

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);

    // 下一個 handler 的事件循環是否與當前的事件循環是同一個線程,此處就是EventLoop
    EventExecutor executor = next.executor();
    
    // 是,直接調用
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } 
    // 不是,將要執行的代碼作爲任務提交給下一個事件循環處理(換人)
    else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

看到這了,有幫助的話幫忙點個贊吧~~

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