自顶向下深入分析Netty(八)--ChannelHandler

8.1 总述

由第七节的讲述可知ChannelHandler并不处理事件,而由其子类代为处理:ChannelInboundHandler拦截和处理入站事件,ChannelOutboundHandler拦截和处理出站事件。ChannelHandlerChannelHandlerContext通过组合或继承的方式关联到一起成对使用。事件通过ChannelHandlerContext主动调用如fireXXX()write(msg)等方法,将事件传播到下一个处理器。注意:入站事件在ChannelPipeline双向链表中由头到尾正向传播,出站事件则方向相反。
当客户端连接到服务器时,Netty新建一个ChannelPipeline处理其中的事件,而一个ChannelPipeline中含有若干ChannelHandler。如果每个客户端连接都新建一个ChannelHandler实例,当有大量客户端时,服务器将保存大量的ChannelHandler实例。为此,Netty提供了Sharable注解,如果一个ChannelHandler状态无关,那么可将其标注为Sharable,如此,服务器只需保存一个实例就能处理所有客户端的事件。

8.2 源码分析

8.2.1 核心类

ChannelHandler类图

上图是ChannelHandler的核心类类图,其继承层次清晰,我们逐一分析。

1.ChannelHandler

ChannaleHandler 作为最顶层的接口,并不处理入站和出站事件,所以接口中只包含最基本的方法:

    // Handler本身被添加到ChannelPipeline时调用
    void handlerAdded(ChannelHandlerContext ctx) throws Exception;
    // Handler本身被从ChannelPipeline中删除时调用
    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
    // 发生异常时调用
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;

其中也定义了Sharable标记注解:

    @Inherited
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface Sharable {
        // no value
    }

作为ChannelHandler的默认实现,ChannelHandlerAdapter有个重要的方法isSharable(),代码如下:

    public boolean isSharable() {
        Class<?> clazz = getClass();
        // 每个线程一个缓存
        Map<Class<?>, Boolean> cache = 
                InternalThreadLocalMap.get().handlerSharableCache();
        Boolean sharable = cache.get(clazz);
        if (sharable == null) {
            // Handler是否存在Sharable注解
            sharable = clazz.isAnnotationPresent(Sharable.class);
            cache.put(clazz, sharable);
        }
        return sharable;
    }

这里引入了优化的线程局部变量InternalThreadLocalMap,将在以后分析,此处可简单理解为线程变量ThreadLocal,即每个线程都有一份ChannelHandler是否Sharable的缓存。这样可以减少线程间的竞争,提升性能。

2.ChannelInboundHandler

ChannelInboundHandler处理入站事件,以及用户自定义事件:

    // 类似的入站事件
    void channeXXX(ChannelHandlerContext ctx) throws Exception;
    // 用户自定义事件
    void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;

ChannelInboundHandlerAdapter作为ChannelInboundHandler的实现,默认将入站事件自动传播到下一个入站处理器。其中的代码高度一致,如下:

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.fireChannelRead(msg);
    }

3.ChannelOutboundHandler

ChannelOutboundHandler处理出站事件:

    // 类似的出站事件
    void read(ChannelHandlerContext ctx) throws Exception;

同理,ChannelOutboundHandlerAdapter作为ChannelOutboundHandler的事件,默认将出站事件传播到下一个出站处理器:

    @Override
    public void read(ChannelHandlerContext ctx) throws Exception {
        ctx.read();
    }

4.ChannelDuplexHandler

ChannelDuplexHandler则同时实现了ChannelInboundHandlerChannelOutboundHandler接口。如果一个所需的ChannelHandler既要处理入站事件又要处理出站事件,推荐继承此类。

至此,ChannelHandler的核心类已分析完毕,接下来将分析一些Netty自带的Handler。

8.3 ChannelHandler实例

8.3.1 LoggingHandler

日志处理器LoggingHandler是使用Netty进行开发时的好帮手,它可以对入站\出站事件进行日志记录,从而方便我们进行问题排查。首先看类签名:

    @Sharable
    public class LoggingHandler extends ChannelDuplexHandler

注解Sharable说明LoggingHandler没有状态相关变量,所有Channel可以使用一个实例。继承自ChannelDuplexHandler表示对入站出站事件都进行日志记录。最佳实践:使用static修饰LoggingHandler实例,并在生产环境删除LoggingHandler
该类的成员变量如下:

    // 实际使用的日志处理,slf4j、log4j等
    protected final InternalLogger logger;
    // 日志框架使用的日志级别
    protected final InternalLogLevel internalLevel;
    // Netty使用的日志级别
    private final LogLevel level;
    
    // 默认级别为Debug
    private static final LogLevel DEFAULT_LEVEL = LogLevel.DEBUG;

看完成员变量,在移目构造方法,LoggingHandler的构造方法较多,一个典型的如下:

    public LoggingHandler(LogLevel level) {
        if (level == null) {
            throw new NullPointerException("level");
        }
        // 获得实际的日志框架
        logger = InternalLoggerFactory.getInstance(getClass());
        // 设置日志级别
        this.level = level;
        internalLevel = level.toInternalLevel();
    }

在构造方法中获取用户实际使用的日志框架,如slf4j、log4j等,并日志设置记录级别。其他的构造方法也类似,不在赘述。
记录出站、入站事件的过程类似,我们以ChannelRead()为例分析,代码如下:

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logMessage(ctx, "RECEIVED", msg);   // 记录日志
        ctx.fireChannelRead(msg);   // 传播事件
    }
    
    private void logMessage(ChannelHandlerContext ctx, String eventName, Object msg) {
        if (logger.isEnabled(internalLevel)) {
            logger.log(internalLevel, format(ctx, formatMessage(eventName, msg)));
        }
    }
    
    protected String formatMessage(String eventName, Object msg) {
        if (msg instanceof ByteBuf) {
            return formatByteBuf(eventName, (ByteBuf) msg);
        } else if (msg instanceof ByteBufHolder) {
            return formatByteBufHolder(eventName, (ByteBufHolder) msg);
        } else {
            return formatNonByteBuf(eventName, msg);
        }
    }

其中的代码都简单明了,主要分析formatByteBuf()方法:

    protected String formatByteBuf(String eventName, ByteBuf msg) {
        int length = msg.readableBytes();
        if (length == 0) {
            StringBuilder buf = new StringBuilder(eventName.length() + 4);
            buf.append(eventName).append(": 0B");
            return buf.toString();
        } else {
            int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
            StringBuilder buf = new StringBuilder(eventName.length() + 
                        2 + 10 + 1 + 2 + rows * 80);

            buf.append(eventName)
                      .append(": ").append(length).append('B').append(NEWLINE);
            appendPrettyHexDump(buf, msg);

            return buf.toString();
        }

其中的数字计算,容易让人失去耐心,使用逆向思维,放上结果反推:

日志打印效果

有了这样的结果,请反推实现。需要注意的是其中的appendPrettyHexDump()方法,这是在ByteBufUtil里的static方法,当我们也需要查看多字节数据时,这是一个特别有用的展现方法,记得可在以后的Debug中可加以使用。

8.3.2 TimeoutHandler

在开发TCP服务时,一个常见的需求便是使用心跳保活客户端。而Netty自带的三个超时处理器IdleStateHandlerReadTimeoutHandlerWriteTimeoutHandler可完美满足此需求。其中IdleStateHandler可处理读超时(客户端长时间没有发送数据给服务端)、写超时(服务端长时间没有发送数据到客户端)和读写超时(客户端与服务端长时间无数据交互)三种情况。这三种情况的枚举为:

    public enum IdleState {
        READER_IDLE,    // 读超时
        WRITER_IDLE,    // 写超时
        ALL_IDLE    // 数据交互超时
    }

IdleStateHandler的读超时事件为例进行分析,首先看类签名:

    public class IdleStateHandler extends ChannelDuplexHandler

注意到此Handler没有Sharable注解,这是因为每个连接的超时时间是特有的即每个连接有独立的状态,所以不能标注Sharable注解。继承自ChannelDuplexHandler是因为既要处理读超时又要处理写超时。
该类的一个典型构造方法如下:

    public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, 
                int allIdleTimeSeconds) {
        this(readerIdleTimeSeconds, writerIdleTimeSeconds,  
                allIdleTimeSeconds, TimeUnit.SECONDS);
    }

分别设定各个超时事件的时间阈值。以读超时事件为例,有以下相关的字段:

    // 用户配置的读超时时间
    private final long readerIdleTimeNanos;
    // 判定超时的调度任务Future
    private ScheduledFuture<?> readerIdleTimeout;
    // 最近一次读取数据的时间
    private long lastReadTime;
    // 是否第一次读超时事件
    private boolean firstReaderIdleEvent = true;
    // 状态,0 - 无关, 1 - 初始化完成 2 - 已被销毁
    private byte state; 
    // 是否正在读取
    private boolean reading;

首先看初始化方法initialize()

    private void initialize(ChannelHandlerContext ctx) {
        switch (state) {
        case 1: // 初始化进行中或者已完成
        case 2: // 销毁进行中或者已完成
            return;
        }
        
        state = 1;
        lastReadTime = ticksInNanos();
        if (readerIdleTimeNanos > 0) {
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }

初始化的工作较为简单,设定最近一次读取时间lastReadTime为当前系统时间,然后在用户设置的读超时时间readerIdleTimeNanos截止时,执行一个ReaderIdleTimeoutTask进行检测。其中使用的方法很简洁,如下:

     long ticksInNanos() {
        return System.nanoTime();
    }
    
    ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, 
              long delay, TimeUnit unit) {
        return ctx.executor().schedule(task, delay, unit);
    }

然后,分析销毁方法destroy()

    private void destroy() {
        state = 2;  // 这里结合initialize对比理解
        if (readerIdleTimeout != null) {
            // 取消调度任务,并置null
            readerIdleTimeout.cancel(false);
            readerIdleTimeout = null;
        }
    }

可知销毁的处理也很简单,分析完初始化和销毁,再看这两个方法被调用的地方,initialize()在三个方法中被调用:

    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        if (ctx.channel().isActive() &&
                ctx.channel().isRegistered()) {
            initialize(ctx);
        } 
    }
    
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        if (ctx.channel().isActive()) {
            initialize(ctx);
        }
        super.channelRegistered(ctx);
    }
    
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        initialize(ctx);
        super.channelActive(ctx);
    }

当客户端与服务端成功建立连接后,Channel被激活,此时channelActive的初始化被调用;如果Channel被激活后,动态添加此Handler,则handlerAdded的初始化被调用;如果Channel被激活,用户主动切换Channel的执行线程Executor,则channelRegistered的初始化被调用。这一部分较难理解,请仔细体会。destroy()则有两处调用:

    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        destroy();
        super.channelInactive(ctx);
    }
    
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        destroy();
    }

即该Handler被动态删除时,handlerRemoved的销毁被执行;Channel失效时,channelInactive的销毁被执行。
分析完这些,在分析核心的调度任务ReaderIdleTimeoutTask

    private final class ReaderIdleTimeoutTask implements Runnable {
        
        private final ChannelHandlerContext ctx;
        
        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        protected void run() {
            if (!ctx.channel().isOpen()) {
                // Channel不再有效
                return;
            }
            
            long nextDelay = readerIdleTimeNanos;
            if (!reading) {
                // nextDelay<=0 说明在设置的超时时间内没有读取数据
                nextDelay -= ticksInNanos() - lastReadTime;
            }
            // 隐含正在读取时,nextDelay = readerIdleTimeNanos > 0

            if (nextDelay <= 0) {
                // 超时时间已到,则再次调度该任务本身
                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, 
                    TimeUnit.NANOSECONDS);

                boolean first = firstReaderIdleEvent;
                firstReaderIdleEvent = false;

                try {
                    IdleStateEvent event =
                        newIdleStateEvent(IdleState.READER_IDLE, first);
                    channelIdle(ctx, event); // 模板方法处理
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // 注意此处的nextDelay值,会跟随lastReadTime刷新
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }

这个读超时检测任务执行的过程中又递归调用了它本身进行下一次调度,请仔细品味该种使用方法。再列出channelIdle()的代码:

    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) 
                  throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

本例中,该方法将写超时事件作为用户事件传播到下一个Handler,用户需要在某个Handler中拦截该事件进行处理。该方法标记为protect说明子类通常可覆盖,ReadTimeoutHandler子类即定义了自己的处理:

    @Override
    protected final void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt)
                   throws Exception {
        assert evt.state() == IdleState.READER_IDLE;
        readTimedOut(ctx);
    }

    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        if (!closed) {
            ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }

可知在ReadTimeoutHandler中,如果发生读超时事件,将会关闭该Channel。当进行心跳处理时,使用IdleStateHandler较为麻烦,一个简便的方法是:直接继承ReadTimeoutHandler然后覆盖readTimedOut()进行用户所需的超时处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容