Netty學習筆記之ChannelHandler

土豆肉絲蓋澆飯發表於2018-04-23

前言

ChannelHandler是netty中的核心處理部分,我們使用netty的絕大部分程式碼都寫在這部分,所以瞭解它的一些機制和特性是很有必要的

Channel

Channel介面抽象了底層socket的一些狀態屬性以及呼叫方法

Netty學習筆記之ChannelHandler
針對不同型別的socket提供不同的子類實現。
Netty學習筆記之ChannelHandler

Channel生命週期

Netty學習筆記之ChannelHandler

ChannelHandler

ChannelHandler介面裡面只定義了三個生命週期方法,我們主要實現它的子介面ChannelInboundHandler和ChannelOutboundHandler,為了便利,框架提供了ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter和ChannelDuplexHandler這三個適配類提供一些預設實現,在使用的時候只需要實現你關注的方法即可

ChannelHandler生命週期方法

Netty學習筆記之ChannelHandler
ChannelHandler裡面定義三個生命週期方法,分別會在當前ChannelHander加入ChannelHandlerContext中,從ChannelHandlerContext中移除,以及ChannelHandler回撥方法出現異常時被回撥

ChannelInboundHandler

Netty學習筆記之ChannelHandler

介紹一下這些回撥方法被觸發的時機

回撥方法 觸發時機
channelRegistered 當前channel註冊到EventLoop
channelUnregistered 當前channel從EventLoop取消註冊
channelActive 當前channel活躍的時候
channelInactive 當前channel不活躍的時候,也就是當前channel到了它生命週期末
channelRead 當前channel從遠端讀取到資料
channelReadComplete channel read消費完讀取的資料的時候被觸發
userEventTriggered 使用者事件觸發的時候
channelWritabilityChanged channel的寫狀態變化的時候觸發

可以注意到每個方法都帶了ChannelHandlerContext作為引數,具體作用是,在每個回撥事件裡面,處理完成之後,使用ChannelHandlerContext的fireChannelXXX方法來傳遞給下個ChannelHandler,netty的codec模組和業務處理程式碼分離就用到了這個鏈路處理

ChannelOutboundHandler

image.png

回撥方法 觸發時機
bind bind操作執行前觸發
connect connect 操作執行前觸發
disconnect disconnect 操作執行前觸發
close close操作執行前觸發
deregister deregister操作執行前觸發
read read操作執行前觸發
write write操作執行前觸發
flush flush操作執行前觸發

注意到一些回撥方法有ChannelPromise這個引數,我們可以呼叫它的addListener註冊監聽,當回撥方法所對應的操作完成後,會觸發這個監聽 下面這個程式碼,會在寫操作完成後觸發,完成操作包括成功和失敗

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg,promise);
    System.out.println("out write");
    promise.addListener(new GenericFutureListener<Future<? super Void>>() {
        @Override
        public void operationComplete(Future<? super Void> future) throws Exception {
            if(future.isSuccess()){
                System.out.println("OK");
            }
        }
    });
}
複製程式碼

ChannelInboundHandler和ChannelOutboundHandler的區別

個人感覺in和out的區別主要在於ChannelInboundHandler的channelRead和channelReadComplete回撥和ChannelOutboundHandler的write和flush回撥上,ChannelOutboundHandler的channelRead回撥負責執行入棧資料的decode邏輯,ChannelOutboundHandler的write負責執行出站資料的encode工作。其他回撥方法和具體觸發邏輯有關,和in與out無關。

ChannelHandlerContext

每個ChannelHandler通過add方法加入到ChannelPipeline中去的時候,會建立一個對應的ChannelHandlerContext,並且繫結,ChannelPipeline實際維護的是ChannelHandlerContext 的關係 在DefaultChannelPipeline原始碼中可以看到會儲存第一個ChannelHandlerContext以及最後一個ChannelHandlerContext的引用

final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
複製程式碼

而在AbstractChannelHandlerContext原始碼中可以看到

volatile AbstractChannelHandlerContext next;
volatile AbstractChannelHandlerContext prev;
複製程式碼

每個ChannelHandlerContext之間形成雙向連結串列

ChannelPipeline

在Channel建立的時候,會同時建立ChannelPipeline

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}
複製程式碼

在ChannelPipeline中也會持有Channel的引用

protected DefaultChannelPipeline newChannelPipeline() {
    return new DefaultChannelPipeline(this);
}
複製程式碼

ChannelPipeline會維護一個ChannelHandlerContext的雙向連結串列

final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
複製程式碼

連結串列的頭尾有預設實現

protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

    tail = new TailContext(this);
    head = new HeadContext(this);

    head.next = tail;
    tail.prev = head;
}
複製程式碼

我們新增的自定義ChannelHandler會插入到head和tail之間,如果是ChannelInboundHandler的回撥,根據插入的順序從左向右進行鏈式呼叫,ChannelOutboundHandler則相反

具體關係如下,但是下圖沒有把預設的head和tail畫出來,這兩個ChannelHandler做的工作相當重要

Netty學習筆記之ChannelHandler

上面的整條鏈式的呼叫是通過Channel介面的方法直接觸發的,如果使用ChannelContextHandler的介面方法間接觸發,鏈路會從ChannelContextHandler對應的ChannelHandler開始,而不是從頭或尾開始

HeadContext

HeadContext實現了ChannelOutboundHandler,ChannelInboundHandler這兩個介面

class HeadContext extends AbstractChannelHandlerContext
            implements ChannelOutboundHandler, ChannelInboundHandler
複製程式碼

因為在頭部,所以說HeadContext中關於in和out的回撥方法都會觸發 關於ChannelInboundHandler,HeadContext的作用是進行一些前置操作,以及把事件傳遞到下一個ChannelHandlerContext的ChannelInboundHandler中去 看下其中channelRegistered的實現

public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    invokeHandlerAddedIfNeeded();
    ctx.fireChannelRegistered();
}
複製程式碼

從語義上可以看出來在把這個事件傳遞給下一個ChannelHandler之前會回撥ChannelHandler的handlerAdded方法 而有關ChannelOutboundHandler介面的實現,會在鏈路的最後執行,看下write方法的實現

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    unsafe.write(msg, promise);
}
複製程式碼

這邊的unsafe介面封裝了底層Channel的呼叫,之所以取名為unsafe,是不需要使用者手動去呼叫這些方法。這個和阻塞原語的unsafe不是同一個 也就是說,當我們通過Channel介面執行write之後,會執行ChannelOutboundHandler鏈式呼叫,在鏈尾的HeadContext ,在通過unsafe回到對應Channel做相關呼叫 從netty Channel介面的實現就能論證這個

public ChannelFuture write(Object msg) {
    return pipeline.write(msg);
}
複製程式碼

TailContext

TailContext實現了ChannelInboundHandler介面,會在ChannelInboundHandler呼叫鏈最後執行,只要是對呼叫鏈完成處理的情況進行處理,看下channelRead實現

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    onUnhandledInboundMessage(msg);
}
複製程式碼

如果我們自定義的最後一個ChannelInboundHandler,也把處理操作交給下一個ChannelHandler,那麼就會到TailContext,在TailContext會提供一些預設處理

protected void onUnhandledInboundMessage(Object msg) {
    try {
        logger.debug(
                "Discarded inbound message {} that reached at the tail of the pipeline. " +
                        "Please check your pipeline configuration.", msg);
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
複製程式碼

比如channelRead中的onUnhandledInboundMessage方法,會把msg資源回收,防止記憶體洩露

強調一點的是,如果要執行整個鏈路,必須通過呼叫Channel方法觸發,ChannelHandlerContext引用了ChannelPipeline,所以也能間接操作channel的方法,但是會從當前ChannelHandlerContext繫結的ChannelHandler作為起點開始,而不是ChannelHandlerContext的頭和尾 這個特性在不需要呼叫整個鏈路的情況下可以使用,可以增加一些效率

上述元件之間的關係

image.png

  1. 每個Channel會繫結一個ChannelPipeline,每個ChannelPipeline會持有一個Channel
  2. 每個ChannelHandler對應一個ChannelHandlerContext,ChannelPipeline持有ChannelHandlerContext連結串列,也就相當於持有ChannelHandler連結串列
  3. ChannelHandlerContext作為上下文,持有ChannelPipeline和它對應ChannelHandler的引用,持有ChannelPipeline相當於間接持有Channel,同時持有它上/下一個ChannelHandlerContext的引用

示例程式

public class NettyServer {

    public static void main(String[] args) throws InterruptedException {
        //兩個執行緒池
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        NioEventLoopGroup childEventLoopGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap
                .group(eventLoopGroup,childEventLoopGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(9999))
                //對serversocketchannel的回撥
                .handler(new ChannelInitializer<ServerSocketChannel>() {
                    @Override
                    protected void initChannel(ServerSocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline()
                                .addLast(new TestInboundChannelHandler("server  in "))
                                .addLast(new TestOutboundChannelHandler("server  out "));

                    }
                })
                //對socketchannel的回撥
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline()
                                .addLast(new TestInboundChannelHandler("first in "))
                                .addLast(new TestInboundChannelHandler("second in "))
                                .addLast(new EchoChannelHandler())
                                .addLast(new TestOutboundChannelHandler("first out "))
                                .addLast(new TestOutboundChannelHandler("second out "));

                    }
                });
        //等到繫結完成
        ChannelFuture channelFuture = bootstrap.bind().sync();
        //等到serversocketchannel close在退出
        channelFuture.channel().closeFuture().sync();

    }

    static class TestInboundChannelHandler extends ChannelInboundHandlerAdapter{

        private String name;

        public TestInboundChannelHandler(String name) {
            this.name = name;
        }

        @Override
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            System.out.println(name+"channelRegistered spread");
            ctx.fireChannelRegistered();
        }

        @Override
        public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
            System.out.println(name+"channelUnregistered not spread");
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(name+"channelActive not spread");
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(name+"channelInactive not spread");
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println(name+"channelRead spread");
            ctx.fireChannelRead(msg);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            System.out.println(name+"channelReadComplete spread");
            ctx.fireChannelReadComplete();
        }
    }

    static class EchoChannelHandler extends ChannelInboundHandlerAdapter{
        @Override
        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            System.out.println("echo channelRegistered");
            //ctx.writeAndFlush(Unpooled.copiedBuffer("Hello\r\n".getBytes()));
        }

        @Override
        public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
            System.out.println("echo channelUnregistered");
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("echo channelInactive");
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("echo channelActive");
            ctx.writeAndFlush(Unpooled.copiedBuffer("Hello\r\n".getBytes()));
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("echo channelRead");
            ByteBuf byteBuf = (ByteBuf)msg;
            System.out.println(new String(ByteBufUtil.getBytes(byteBuf)));
            //ctx.write(msg);
            ctx.channel().write(msg);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            System.out.println("echo channelReadComplete");
            ctx.channel().flush();
        }
    }

    static class TestOutboundChannelHandler extends ChannelOutboundHandlerAdapter{

        private String name;

        public TestOutboundChannelHandler(String name) {
            this.name = name;
        }

        @Override
        public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
            //super.bind(ctx,localAddress,promise);
            System.out.println(name+ "bind");
        }

        @Override
        public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception {
            super.connect(ctx,remoteAddress,localAddress,promise);
            System.out.println(name+"connect");
        }

        @Override
        public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
            //super.disconnect(ctx,promise);
            System.out.println(name+"disconnect");
        }

        @Override
        public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
            //super.close(ctx,promise);
            System.out.println(name+"close");
        }

        @Override
        public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
            System.out.println(name+"deregister");
        }

        @Override
        public void read(ChannelHandlerContext ctx) throws Exception {
            ctx.read();
            System.out.println(name+"read");
        }

        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            //super.write(ctx,msg,promise);
            ctx.write(msg,promise);
            System.out.println(name+"write");
            //promise.setSuccess();
            promise.addListener(new GenericFutureListener<Future<? super Void>>() {
                @Override
                public void operationComplete(Future<? super Void> future) throws Exception {
                    if(future.isSuccess()){
                        System.out.println(name+" listener trigger");
                    }
                }
            });
        }

        @Override
        public void flush(ChannelHandlerContext ctx) throws Exception {
            System.out.println(name+"flush");
            ctx.flush();
        }
    }
}
複製程式碼

這個demo主要測試了ChannelHandler鏈的傳遞以及觸發時機

最後

希望大家關注下我的公眾號,歡迎朋友們投稿

image

相關文章