netty系列之:channel和channelGroup

flydean發表於2022-02-17

簡介

channel是netty中資料傳輸和資料處理的渠道,也是netty程式中不可或缺的一環。在netty中channel是一個介面,針對不同的資料型別或者協議channel會有具體的不同實現。

雖然channel很重要,但是在程式碼中確實很神秘,基本上我們很少能夠看到直接使用channel的情況,那麼事實真的如此嗎?和channel相關的ChannelGroup又有什麼作用呢?一起來看看吧。

神龍見首不見尾的channel

其實netty的程式碼是有固定的模板的,首先根據是server端還是client端,然後建立對應的Bootstrap和ServerBootstrap。然後給這個Bootstrap配置對應的group方法。然後為Bootstrap配置channel和handler,最後啟動Bootstrap即可。

這樣一個標準的netty程式就完成了。你需要做的就是為其挑選合適的group、channel和handler。

我們先看一個最簡單的NioServerSocketChannel的情況:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChatServerInitializer());

            b.bind(PORT).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

這裡,我們將NioServerSocketChannel設定為ServerBootstrap的channel。

這樣就完了嗎?channel到底是在哪裡用到的呢?

別急,我們仔細看一下try block中的最後一句:

b.bind(PORT).sync().channel().closeFuture().sync();

b.bind(PORT).sync()實際上返回了一個ChannelFuture物件,透過呼叫它的channel方法,就返回了和它關聯的Channel物件。

然後我們呼叫了channel.closeFuture()方法。closeFuture方法會返回一個ChannelFuture物件,這個物件將會在channel關閉的時候收到通知。

而sync方法會實現同步阻塞,一直等到channel關閉為止,從而進行後續的eventGroup的shutdown操作。

在ServerBootstrap中構建模板中,channel其實有兩個作用,第一個作用是指定ServerBootstrap的channel,第二個作用就是透過channel獲取到channel關閉的事件,最終關閉整個netty程式。

雖然我們基本上看不到channel的直接方法呼叫,但是channel毋庸置疑,它就是netty的靈魂。

接下來我們再看一下具體訊息處理的handler的基本操作:

    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // channel活躍
        ctx.write("Channel Active狀態!\r\n");
        ctx.flush();
    }

通常如果需要在handler中向channel寫入資料,我們呼叫的是ChannelHandlerContext的write方法。這個方法和channel有什麼關係呢?

首先write方法是ChannelOutboundInvoker介面中的方法,而ChannelHandlerContext和Channel都繼承了ChannelOutboundInvoker介面,也就是說,ChannelHandlerContext和Channel都有write方法:

ChannelFuture write(Object msg);

因為這裡我們使用的是NioServerSocketChannel,所以我們來具體看一下NioServerSocketChannel中write的實現。

經過檢查程式碼我們會發現NioServerSocketChannel繼承自AbstractNioMessageChannel,AbstractNioMessageChannel繼承自AbstractNioChannel,AbstractNioChannel繼承自AbstractChannel,而這個write方法就是AbstractChannel中實現的:

    public ChannelFuture write(Object msg) {
        return pipeline.write(msg);
    }

Channel的write方法,實際上呼叫了pipeline的write方法。下面是pipeLine中的write方法:

    public final ChannelFuture write(Object msg) {
        return tail.write(msg);
    }

這裡的tail是一個AbstractChannelHandlerContext物件。

這樣我們就得出了這樣的結論:channel中的write方法最終實際上呼叫的是ChannelHandlerContext中的write方法。

所以上面的:

ctx.write("Channel Active狀態!\r\n");

實際上可以直接從channel中呼叫:

Channel ch = b.bind(0).sync().channel();

// 將訊息寫入channel中
ch.writeAndFlush("Channel Active狀態!\r\n").sync();

channel和channelGroup

channel是netty的靈魂,對於Bootstrap來說,要獲取到對應的channel,可以透過呼叫:

b.bind(PORT).sync().channel()

來得到,從上面程式碼中我們也可以看到一個Bootstrap只會對應一個channel。

channel中有一個parent()方法,用來返回它的父channel,所以channel是有層級結構的,

我們再來看一下channelGroup的定義:

public interface ChannelGroup extends Set<Channel>, Comparable<ChannelGroup> 

可以看到ChannelGroup實際上是Channel的集合。ChannelGroup用來將類似的Channel構建成集合,從而可以對多個channel進行統一的管理。

可以能有小夥伴要問了,一個Bootstrap不是隻對應一個channel嗎?那麼哪裡來的channel的集合?

事實上,在一些複雜的程式中,我們可能啟動多個Bootstrap來處理不同的業務,所以相應的就會有多個channel。

如果建立的channel過多,並且這些channel又是很同質化的時候,就有需求對這些channel進行統一管理。這時候就需要用到channelGroup了。

channelGroup的基本使用

先看下channelGroup的基本使用,首先是建立一個channelGroup:

ChannelGroup recipients =
           new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

有了channelGroup之後,可以呼叫add方法,向其中新增不同的channel:

   recipients.add(channelA);
   recipients.add(channelB);

並且還可以統一向這些channel中傳送訊息:

recipients.write(Unpooled.copiedBuffer(
           "這是從channelGroup中發出的統一訊息.",
           CharsetUtil.UTF_8));

基本上channelGroup提供了write,flush,flushAndWrite,writeAndFlush,disconnect,close,newCloseFuture等功能,用於對集合中的channel進行統一管理。

如果你有多個channel,那麼可以考慮使用channelGroup。

另外channelGroup還有一些特性,我們來詳細瞭解一下。

將關閉的channel自動移出

ChannelGroup是一個channel的集合,當然我們只希望儲存open狀態的channel,如果是close狀態的channel,還要手動從ChannelGroup中移出的話實在是太麻煩了。

所以在ChannelGroup中,如果一個channel被關閉了,那麼它會自動從ChannelGroup中移出,這個功能是怎麼實現的呢?

先來看下channelGroup的add方法:

   public boolean add(Channel channel) {
        ConcurrentMap<ChannelId, Channel> map =
            channel instanceof ServerChannel? serverChannels : nonServerChannels;

        boolean added = map.putIfAbsent(channel.id(), channel) == null;
        if (added) {
            channel.closeFuture().addListener(remover);
        }

        if (stayClosed && closed) {
            channel.close();
        }

        return added;
    }

可以看到,在add方法中,為channel區分了是server channel還是非server channel。然後根據channel id將其存入ConcurrentMap中。

如果新增成功,則給channel新增了一個closeFuture的回撥。當channel被關閉的時候,會去呼叫這個remover方法:

private final ChannelFutureListener remover = new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            remove(future.channel());
        }
    };

remover方法會將channel從serverChannels或者nonServerChannels中移出。從而保證ChannelGroup中只儲存open狀態的channel。

同時關閉serverChannel和acceptedChannel

雖然 ServerBootstrap的bind方法只會返回一個channel,但是對於server來說,可以有多個worker EventLoopGroup,所以當客戶端和伺服器端建立連線之後建立的accepted Channel是server channel的子channel。

也就是說一個伺服器端有一個server channel和多個accepted channel。

那麼如果我們想要同時關閉這些channel的話, 就可以使用ChannelGroup的close方法。

因為如果Server channel和非Server channel在同一個ChannelGroup的話,所有的IO命令都會先發給server channel,然後才會發給非server channel。

所以我們可以將Server channel和非Server channel統統加入同一個ChannelGroup中,在程式的最後,統一呼叫ChannelGroup的close方法,從而達到該目的:

   ChannelGroup allChannels =
           new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
  
   public static void main(String[] args) throws Exception {
       ServerBootstrap b = new ServerBootstrap(..);
       ...
       b.childHandler(new MyHandler());
  
       // 啟動伺服器
       b.getPipeline().addLast("handler", new MyHandler());
       Channel serverChannel = b.bind(..).sync();
       allChannels.add(serverChannel);
  
       ... 等待shutdown指令 ...
  
       // 關閉serverChannel 和所有的 accepted connections.
       allChannels.close().awaitUninterruptibly();
   }
  
   public class MyHandler extends ChannelInboundHandlerAdapter {
        @Override
       public void channelActive(ChannelHandlerContext ctx) {
           // 將accepted channel新增到allChannels中
           allChannels.add(ctx.channel());
           super.channelActive(ctx);
       }
   }

ChannelGroupFuture

另外,和channel一樣,channelGroup的操作都是非同步的,它會返回一個ChannelGroupFuture物件。

我們看下ChannelGroupFuture的定義:

public interface ChannelGroupFuture extends Future<Void>, Iterable<ChannelFuture>

可以看到ChannelGroupFuture是一個Future,同時它也是一個ChannelFuture的遍歷器,可以遍歷ChannelGroup中所有channel返回的ChannelFuture。

同時ChannelGroupFuture提供了isSuccess,isPartialSuccess,isPartialFailure等方法判斷命令是否部分成功。

ChannelGroupFuture還提供了addListener方法用來監聽具體的事件。

總結

channel是netty的核心,當我們有多個channel不便進行管理的時候,就可以使用channelGroup進行統一管理。

本文已收錄於 http://www.flydean.com/04-1-netty-channel-group/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章