• Netty之Channel*


    Netty之Channel*

    本文内容主要参考**<<Netty In Action>> ** 和Netty的文档和源码,偏笔记向.

    先简略了解一下ChannelPipelineChannelHandler.

    想象一个流水线车间.当组件从流水线头部进入,穿越流水线,流水线上的工人按顺序对组件进行加工,到达流水线尾部时商品组装完成.

    可以将ChannelPipeline当做流水线,ChannelHandler当做流水线工人.源头的组件当做event,如read,write等等.

    1.1 Channel

    Channel连接了网络套接字或能够进行I/O操作的组件,如 read, write, connect, bind.

    我们可以通过Channel获取一些信息.

    • Channel的当前状态(如,是否连接,是否打开)
    • Channel的配置参数,如buffer的size
    • 支持的I/O操作
    • 处理所有I/O事件的ChannelPipeline和与通道相关的请求

    Channel接口定义了一组和ChannelInboundHandler API密切相关的状态模型.

    52896437562

    Channel的状态改变,会生成对应的event.这些event会转发给ChannelPipeline中的ChannelHandler,handler会对其进行响应.

    1.2 ChannelHandler生命周期

    下面列出了 interface ChannelHandler 定义的生命周期操作, 在 ChannelHandler被添加到 ChannelPipeline 中或者被从 ChannelPipeline 中移除时会调用这些操作。这些方法中的每一个都接受一个 ChannelHandlerContext 参数

    1.3 ChannelInboundHandler 接口

    ChannelInboundHandler处理入站数据以及各种状态变化,当Channel状态发生改变会调用ChannelInboundHandler中的一些生命周期方法.这些方法与Channel的生命密切相关.

    入站数据,就是进入socket的数据.下面展示一些该接口的生命周期API

    当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它将负责显式地
    释放与池化的 ByteBuf 实例相关的内存。 Netty 为此提供了一个实用方法ReferenceCountUtil.release() .

    @Sharable
    public class DiscardHandler extends ChannelInboundHandlerAdapter {
    	@Override
    	public void channelRead(ChannelHandlerContext ctx, Object msg) {
    		ReferenceCountUtil.release(msg);
    	}
    }
    

    这种方式还挺繁琐的,Netty提供了一个SimpleChannelInboundHandler ,重写channelRead0()方法,就可以在调用过程中会自动释放资源.

    public class SimpleDiscardHandler
    	extends SimpleChannelInboundHandler<Object> {
    	@Override
    	public void channelRead0(ChannelHandlerContext ctx,
    									Object msg) {
    			// 不用调用ReferenceCountUtil.release(msg)也会释放资源
    	}
    }
    

    原理就是这样,channelRead方法包装了channelRead0方法.

    @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            boolean release = true;
            try {
                if (acceptInboundMessage(msg)) {
                    @SuppressWarnings("unchecked")
                    I imsg = (I) msg;
                    channelRead0(ctx, imsg);
                } else {
                    release = false;
                    ctx.fireChannelRead(msg);
                }
            } finally {
                if (autoRelease && release) {
                    ReferenceCountUtil.release(msg);
                }
            }
        }
    

    1.4 ChannelOutboundHandler

    出站操作和数据将由 ChannelOutboundHandler 处理。它的方法将被 Channel、 ChannelPipeline 以及 ChannelHandlerContext 调用。
    ChannelOutboundHandler 的一个强大的功能是可以按需推迟操作或者事件,这使得可以通过一些复杂的方法来处理请求。例如, 如果到远程节点的写入被暂停了, 那么你可以推迟冲刷操作并在稍后继续。

    ChannelPromiseChannelFuture: ChannelOutboundHandler中的大部分方法都需要一个ChannelPromise参数, 以便在操作完成时得到通知。 ChannelPromiseChannelFuture的一个子类,其定义了一些可写的方法,如setSuccess()和setFailure(), 从而使ChannelFuture不可变.

    1.5 ChannelHandler适配器

    ChannelHandlerAdapter顾名思义,就是handler的适配器.你需要知道什么是适配器模式,假设有一个A接口,我们需要A的subclass实现功能,但是B类中正好有我们需要的功能,不想复制粘贴B中的方法和属性了,那么可以写一个适配器类Adpter继承B实现A,这样一来Adpter是A的子类并且能直接使用B中的方法,这种模式就是适配器模式.

    就比如Netty中的SslHandler类,想使用ByteToMessageDecoder中的方法进行解码,但是必须是ChannelHandler子类对象才能加入到ChannelPipeline中,通过如下签名和其实现细节(SslHandler实现细节就不贴了)就能够作为一个Handler去处理消息了.

    public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundHandler
    

    下图是ChannelHandler和Adpter的UML图示.

    ChannelHandlerAdapter提供了一些实用方法isSharable() 如果其对应的实现被标注为 Sharable, 那么这个方法将返回 true, 表示它可以被添加到多个 ChannelPipeline中 .

    如果想在自己的ChannelHandler中使用这些适配器类,只需要扩展他们,重写那些想要自定义的方法即可.

    1.6 资源管理

    在使用ChannelInboundHandler.channelRead() ChannelOutboundHandler.write() 方法处理数据时要避免资源泄露,ByteBuf那篇文章提到过引用计数,当使用完某个ByteBuf之后记得调整引用计数.

    Netty提供了一个class ResourceLeakDetector 来帮助诊断资源泄露,这能够帮助你判断应用的运行情况,但是如果希望提高吞吐量(比如搞一些竞赛),关闭内存诊断可以提高吞吐量.

    泄露检测级别可以通过将下面的 Java 系统属性设置为表中的一个值来定义:
    java -Dio.netty.leakDetectionLevel=ADVANCED

    如果带着该 JVM 选项重新启动你的应用程序,你将看到自己的应用程序最近被泄漏的缓冲
    区被访问的位置。下面是一个典型的由单元测试产生的泄漏报告:

    Running io.netty.handler.codec.xml.XmlFrameDecoderTest
    15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
    ByteBuf.release() was not called before it's garbage-collected.
    Recent access records: 1
    #1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString(
    AdvancedLeakAwareByteBuf.java:697)
    io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(
    XmlFrameDecoderTest.java:157)
    io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(
    XmlFrameDecoderTest.java:133)
    ...
    

    应用程序处理消息释放资源

    消费入站消息释放资源

    @Sharable
    public class DiscardInboundHandler extends ChannelInboundHandlerAdapter {
    	@Override
    	public void channelRead(ChannelHandlerContext ctx, Object msg) {
    		ReferenceCountUtil.release(msg);// 用于释放资源的工具类
    	}
    }
    

    SimpleChannelInboundHandler 中的channelRead0()会消费消息之后自动释放资源.

    出站释放资源

    @Sharable
    public class DiscardOutboundHandler
    						extends ChannelOutboundHandlerAdapter {
    	@Override
    	public void write(ChannelHandlerContext ctx,
            Object msg, ChannelPromise promise) {
            // 还是通过util工具类释放资源
            ReferenceCountUtil.release(msg);
            // 通知ChannelPromise,消息已经处理
            promise.setSuccess();
    	}
    }
    

    重要的是, 不仅要释放资源,还要通知 ChannelPromise。否则可能会出现 ChannelFutureListener 收不到某个消息已经被处理了的通知的情况。总之,如果一个消息被消费或者丢弃了, 并且没有传递给 ChannelPipeline 中的下一个ChannelOutboundHandler, 那么用户就有责任调用ReferenceCountUtil.release()。如果消息到达了实际的传输层, 那么当它被写入时或者 Channel 关闭时,都将被自动释放。

    2 ChannelPipelin接口

    Channel和ChannelPipeline

    每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline。这项关联是永久性的; Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。在 Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。

    ChannelHandler和ChannelHandlerContext

    根据事件的起源,事件将会被 ChannelInboundHandler 或者 ChannelOutboundHandler 处理。随后, 通过调用 ChannelHandlerContext 实现,它将被转发给同一超类型的下一个ChannelHandler。

    ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler 交 互 。 ChannelHandler 可 以 通 知 其 所 属 的 ChannelPipeline 中 的 下 一 个ChannelHandler,甚至可以动态修改它所属的ChannelPipeline.

    ChannelPipelin和ChannelHandler

    这是一个同时具有入站和出站 ChannelHandler 的 ChannelPipeline 的布局,并且印证了我们之前的关于 ChannelPipeline 主要由一系列的 ChannelHandler 所组成的说法。 ChannelPipeline 还提供了通过 ChannelPipeline 本身传播事件的方法。如果一个入站事件被触发,它将被从 ChannelPipeline 的头部开始一直被传播到 Channel Pipeline 的尾端。

    你可能会说, 从事件途经 ChannelPipeline 的角度来看, ChannelPipeline 的头部和尾端取决于该事件是入站的还是出站的。然而 Netty 总是将 ChannelPipeline 的入站口(图 的左侧)作为头部,而将出站口(该图的右侧)作为尾端。
    当你完成了通过调用 ChannelPipeline.add*()方法将入站处理器( ChannelInboundHandler)和 出 站 处 理 器 ( ChannelOutboundHandler ) 混 合 添 加 到 ChannelPipeline 之 后 , 每 一 个ChannelHandler 从头部到尾端的顺序位置正如同我们方才所定义它们的一样。因此,如果你将图 6-3 中的处理器( ChannelHandler)从左到右进行编号,那么第一个被入站事件看到的 ChannelHandler 将是1,而第一个被出站事件看到的 ChannelHandler 将是 5。

    在 ChannelPipeline 传播事件时,它会测试 ChannelPipeline 中的下一个 ChannelHandler 的类型是否和事件的运动方向相匹配。如果不匹配, ChannelPipeline 将跳过该ChannelHandler 并前进到下一个,直到它找到和该事件所期望的方向相匹配的为止。 (当然, ChannelHandler 也可以同时实现ChannelInboundHandler 接口和 ChannelOutboundHandler 接口。)

    2.1 修改ChannelPipeline

    修改指的是添加或删除ChannelHandler

    代码示例

    ChannelPipeline pipeline = ..;
    FirstHandler firstHandler = new FirstHandler();
    // 先添加一个Handler到ChannelPipeline中
    pipeline.addLast("handler1", firstHandler);
    // 这个Handler放在了first,意味着放在了handler1之前
    pipeline.addFirst("handler2", new SecondHandler());
    // 这个Handler被放到了last,意味着在handler1之后
    pipeline.addLast("handler3", new ThirdHandler());
    ...
    // 通过名称删除
    pipeline.remove("handler3");
    // 通过对象删除
    pipeline.remove(firstHandler);
    // 名称"handler2"替换成名称"handler4",并切handler2的实例替换成了handler4的实例
    pipeline.replace("handler2", "handler4", new ForthHandler());
    

    这种方式非常灵活,按照需要更换或插入handler达到我们想要的效果.

    ChannelHandler的执行和阻塞

    通常 ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop( I/O 线程)来处理传递给它的事件的。所以至关重要的是不要阻塞这个线程,因为这会对整体的 I/O 处理产生负面的影响。

    但有时可能需要与那些使用阻塞 API 的遗留代码进行交互。对于这种情况, ChannelPipeline 有一些接受一个 EventExecutorGroup 的 add()方法。如果一个事件被传递给一个自定义的 EventExecutorGroup ,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,从而被从该Channel 本身的 EventLoop 中移除。对于这种用例, Netty 提供了一个叫 DefaultEventExecutorGroup 的默认实现。

    pipeline对handler的操作

    2.2 ChannelPipeline的出入站api

    入站

    出站

    • ChannelPipeline 保存了与 Channel 相关联的 ChannelHandler
    • ChannelPipeline 可以根据需要,通过添加或者删除 ChannelHandler 来动态地修改
    • ChannelPipeline 有着丰富的 API 用以被调用,以响应入站和出站事件

    3 ChannelHandlerContext接口

    每当有ChannelHandler添加到ChannelPipeline中,都会创建ChannelHandlerContext.如果调用ChannelChannelPipeline上的方法,会沿着整个ChannelPipeline传播,如果调用ChannelHandlerContext上的相同方法,则会从对应的当前ChannelHandler进行传播.

    API

    • ChannelHandlerContextChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
    • 如同我们在本节开头所解释的一样,相对于其他类的同名方法,ChannelHandlerContext的方法将产生更短的事件流, 应该尽可能地利用这个特性来获得最大的性能。

    3.1 使用CHannelHandlerContext

    从ChannelHandlerContext访问channel

    ChannelHandlerContext ctx = ..;
    // 获取channel引用
    Channel channel = ctx.channel();
    // 通过channel写入缓冲区
    channel.write(Unpooled.copiedBuffer("Netty in Action",
    CharsetUtil.UTF_8));
    

    从ChannelHandlerContext访问ChannelPipeline

    ChannelHandlerContext ctx = ..;
    // 获取ChannelHandlerContext
    ChannelPipeline pipeline = ctx.pipeline();
    // 通过ChannelPipeline写入缓冲区
    pipeline.write(Unpooled.copiedBuffer("Netty in Action",
    CharsetUtil.UTF_8));
    

    有时候我们不想从头传递数据,想跳过几个handler,从某个handler开始传递数据.我们必须获取目标handler之前的handler关联的ChannelHandlerContext.

    ChannelHandlerContext ctx = ..;
    // 直接通过ChannelHandlerContext写数据,发送到下一个handler
    ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
    

    好了,ChannelHandlerContext的基本使用应该掌握了,但是你真的理解ChannelHandlerContext,ChannelPipeline和Channelhandler之间的关系了吗.我们老看一下Netty的源码.

    先看一下AbstractChannelHandlerContext类,这个类像不像双向链表中的一个Node,

    abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
            implements ChannelHandlerContext, ResourceLeakHint {
            ...
            volatile AbstractChannelHandlerContext next;
        	volatile AbstractChannelHandlerContext prev;
        	...
        	}
    

    再来看一看DefaultChannelPipeline,ChannelPipeline中拥有ChannelHandlerContext这个节点的head和tail,

    而且DefaultChannelPipeline类中并没有ChannelHandler成员或handler数组.

    public class DefaultChannelPipeline implements ChannelPipeline {
        ...
            
        final AbstractChannelHandlerContext head;
        final AbstractChannelHandlerContext tail;
        ...
    

    所以addFirst向pipeline中添加了handler到底添加到哪了呢.看一下pipeline中的addFirst方法

        @Override
        public final ChannelPipeline addFirst(String name, ChannelHandler handler) {
            return addFirst(null, name, handler);
        }
    
        @Override
        public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) {
            final AbstractChannelHandlerContext newCtx;
            synchronized (this) {
                // 检查handler是否具有复用能力,不重要
                checkMultiplicity(handler);
    			// 名称,不重要.
                name = filterName(name, handler);
    // 这个方法创建了DefaultChannelHandlerContext,handler是其一个成员属性
    // 你现在应该明白了上面说的添加handler会创建handlerContext了吧
                newCtx = newContext(group, name, handler);
    // 这个方法
                addFirst0(newCtx);
    
    // 这个方法是调整pipeline中HandlerContext的指针,
    // 就是更新HandlerContext链表节点之间的位置
    private void addFirst0(AbstractChannelHandlerContext newCtx) {
            AbstractChannelHandlerContext nextCtx = head.next;
            newCtx.prev = head;
            newCtx.next = nextCtx;
            head.next = newCtx;
            nextCtx.prev = newCtx;
        }
    

    简单总结一下,pipeline拥有context(本身像一个链表的节点)组成的节点的双向链表首尾,可以看做pipeline拥有一个context链表,context拥有成员handler,这便是三者之间的关系.实际上,handler作为消息处理的主要组件,实现了和pipeline的解耦,我们可以只有一个handler,但是被封装进不同的context能够被不同的pipeline使用.

    3.2 handler和context高级用法

    缓存ChannelHandlerContext引用

    @Sharable
    public class WriteHandler extends ChannelHandlerAdapter {
    	private ChannelHandlerContext ctx;
    	@Override
    	public void handlerAdded(ChannelHandlerContext ctx) {
    		this.ctx = ctx;
    	}
        public void send(String msg) {
        	ctx.writeAndFlush(msg);
        }
    }
    

    因为一个 ChannelHandler 可以从属于多个 ChannelPipeline,所以它也可以绑定到多个 ChannelHandlerContext 实例。 对于这种用法指在多个ChannelPipeline 中共享同一个 ChannelHandler, 对应的 ChannelHandler 必须要使用@Sharable 注解标注; 否则,试图将它添加到多个 ChannelPipeline 时将会触发异常。

    @Sharable错误用法

    @Sharable
    public class UnsharableHandler extends ChannelInboundHandlerAdapter {
        private int count;
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
        	count++;
        	System.out.println("channelRead(...) called the "
        		+ count + " time");
        	ctx.fireChannelRead(msg);
        }
    }
    

    这段代码的问题在于它拥有状态 , 即用于跟踪方法调用次数的实例变量count。将这个类的一个实例添加到ChannelPipeline将极有可能在它被多个并发的Channel访问时导致问题。(当然,这个简单的问题可以通过使channelRead()方法变为同步方法来修正。)

    总之,只应该在确定了你的 ChannelHandler 是线程安全的时才使用@Sharable 注解。

    4.1 入站异常处理

    处理入站事件的过程中有异常被抛出,那么它将从它在ChannelInboundHandler里被触发的那一点开始流经 ChannelPipeline。要想处理这种类型的入站异常,你需要在你的 ChannelInboundHandler 实现中重写下面的方法。

    public void exceptionCaught(
    ChannelHandlerContext ctx, Throwable cause) throws Exception 
    // 基本处理方式
    public class InboundExceptionHandler extends 		ChannelInboundHandlerAdapter {
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx,
        								Throwable cause) {
        	cause.printStackTrace();
        	ctx.close();
        }
    }
    

    因为异常将会继续按照入站方向流动(就像所有的入站事件一样), 所以实现了前面所示逻辑的 ChannelInboundHandler 通常位于 ChannelPipeline 的最后。这确保了所有的入站异常都总是会被处理,无论它们可能会发生在ChannelPipeline 中的什么位置。

    • ChannelHandler.exceptionCaught()的默认实现是简单地将当前异常转发给ChannelPipeline 中的下一个 ChannelHandler;

    • 如果异常到达了 ChannelPipeline 的尾端,它将会被记录为未被处理;

    • 要想定义自定义的处理逻辑,你需要重写 exceptionCaught()方法。然后你需要决定是否需要将该异常传播出去。

    4.2 出站异常处理

    • 每个出站操作都将返回一个 ChannelFuture。 注册到 ChannelFuture 的 ChannelFutureListener 将在操作完成时被通知该操作是成功了还是出错了。
    • 几乎所有的 ChannelOutboundHandler 上的方法都会传入一个 ChannelPromise
      的实例。作为 ChannelFuture 的子类, ChannelPromise 也可以被分配用于异步通
      知的监听器。但是, ChannelPromise 还具有提供立即通知的可写方法:
    ChannelPromise setSuccess();
    ChannelPromise setFailure(Throwable cause);
    

    1.添加ChannelFutureListener到ChannelFuture

        ChannelFuture future = channel.write(someMessage);
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture f) {
                if (!f.isSuccess()) {
                    f.cause().printStackTrace();
                    f.channel().close();
                }
             }
        });
    

    2.添加ChannelFutureListener到ChannelPromise

    public class OutboundExceptionHandler extends 			ChannelOutboundHandlerAdapter {
        @Override
        public void write(ChannelHandlerContext ctx, Object msg,
            ChannelPromise promise) {
                promise.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture f) {
                        if (!f.isSuccess()) {
                            f.cause().printStackTrace();
                            f.channel().close();
                        }
                	}
            });
        }
    }
    
  • 相关阅读:
    MYSQL读写分离
    AIR loadbytes executable code error 解决办法
    文本处理常用命令
    使用awk找出两个大文件的相同部分
    nginx修改php.ini无效的解决办法
    python 正则表达式反向引用的问题
    解决memcached错误:Catastrophic: event fd doesn't match conn fd
    php编译iconv错误解决办法
    grep剔除两个文件中相同部分
    周末闲话
  • 原文地址:https://www.cnblogs.com/krcys/p/9297092.html
Copyright © 2020-2023  润新知