Netty In Action-第六章 ChannelHandler

本章介绍

  • ChannelPipeline
  • ChannelHandlerContext
  • ChannelHandler
  • Inbound vs Outbound(入站和出站)

接收连接或创建他们只是应用程序的一部分,但是一个网络应用往往更复杂的,需要更多的代码编写的是处理传入和传出数据。Netty提供了一个强大的处理这些事情的功能,允许用户自定义ChannelHandler的实现来处理数据。使得ChannelHandler更强大的是可以连接每个ChannelHandler来实现任务,这有助于代码的整洁和重用。但是处理数据只是ChannelHandler所做的事情之一,也可以压制IO操作,例如写请求。所有这些都可以动态实现。

1.ChannelPipeline

ChannelPipeline是ChannelHandler实例的列表,用于处理或截获通道的接收和发送数据。ChannelPipeline提供了一种高级的截取过滤模式,让用户可以在ChannelPipeline中完全控制一个事件及如何处理ChannelHandler与ChannelPipeline的交互。

对于每个新的通道,会创建一个新的ChannelPipeline并附加至通道。一旦连接,Channel和ChannelPipeline之间的耦合是永久性的。Channel不能附加其他的ChannelPipeline或从ChannelPipeline分离。

下图描述了ChannelHandler在ChannelPipeline中的IO处理,一个IO操作可以由一个ChannelInboundHandler或ChannelOutboundHandler进行处理,并通过调用ChannelInboundHandler处理入站或通过ChannelOutboundHandler处理出站IO。

如上图所示,ChannelPipeline是ChannelHandler的一个列表;如果一个入站I/O事件被触发,这个事件会从第一个开始依次通过ChannelPipeline中的ChannelHandler;若是一个出站事件,则会从最后一个开始依次通过ChannelPipeline中的ChannelHandler。ChannelHandler可以处理事件并检查类型,如果某个ChannelHandler不能处理则会跳过,并将事件传递到下一个ChannelHandler。ChannelPipeline可以动态添加、删除、替换其中的ChannelHandler,这样的机制可以提高灵活性。

修改ChannelPipeline的方法:

  • addFirst(...),添加ChannelHandler在ChannelPipeline的第一个位置
  • addBefore(...),在ChannelPipeline中指定ChannelHandler名称之前添加ChannelHandler
  • addAfter(...),在ChannelPipeline中指定ChannelHandler名称之后添加ChannelHandler
  • addLast(ChannelHandler...),在ChannelPipeline的末尾添加ChannelHandler
  • remove(...),删除ChannelPipeline中指定的ChannelHandler
  • replace(...),替换ChannelPipeline中指定的ChannelHandler

被添加到ChannelPipeline的ChannelHandler将通过IO-Thread处理事件,这意味了必须不能有其他的IO-Thread阻塞来影响IO的整体处理。有时候可能需要阻塞,例如JDBC。因此,Netty允许通过一个EventExecutorGroup到每个ChannelPipeline.add* 方法,自定义事件会被包含在EventExecutorGroup中的EventExecutor来处理,默认的实现是DefaultEventExecutorGroup。

ChannelPipeline除了一些修改的方法,还有很多其他的方法,具体是方法及使用可以看API文档或源码。

2.ChannelHandlerContext

每个ChannelHandler被添加到ChannelPipeline后,都会创建一个ChannelHandlerContext并与之创建的ChannelHandler关联绑定。ChannelHandlerContext允许ChannelHandler与其他的ChannelHandler实现进行交互,这是相同ChannelPipeline的一部分。ChannelHandlerContext不会改变添加到其中的ChannelHandler,因此它是安全的。

2.1 通知下一个ChannelHandler

在相同的ChannelPipeline中通过调用ChannelInboundHandler和ChannelOutboundHandler中各个方法中的一个方法来通知最近的handler,通知开始的地方取决于你如何设置。下图显示了ChannelHandlerContext、ChannelHandler、ChannelPipeline的关系:

如果你想有一些事件流全部通过ChannelPipeline,有两个方法可以做到:

  • 调用Channel的方法
  • 调用ChannelPipeline的方法

这两个方法都可以让事件流全部通过ChannelPipeline。无论从头部还是尾部开始,因为它主要依赖于事件的性质。如果是一个“入站事件”,它开始于头部;如果是一个“出站事件”,它开始于尾部。

下面的飞马显示了一个写事件如何通过ChannelPipeline从尾部开始:

@Override  
protected void initChannel(SocketChannel ch) throws Exception {  
    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {  
        @Override  
        public void channelActive(ChannelHandlerContext ctx) throws Exception {  
            //Event via Channel  
            Channel channel = ctx.channel();  
            channel.write(Unpooled.copiedBuffer("netty in action", CharsetUtil.UTF_8));  
            //Event via ChannelPipeline  
            ChannelPipeline pipeline = ctx.pipeline();  
            pipeline.write(Unpooled.copiedBuffer("netty in action", CharsetUtil.UTF_8));  
        }  
    });  
}  

可能你想从ChannelPipeline的指定位置开始,不想流经整个ChannelPipeline,如下情况:

  • 为了节省开销,不感兴趣的ChannelHandler不让通过
  • 排除一些ChannelHandler

在这种情况下,你可以使用ChannelHandlerContext执行下一个ChannelHandler。下面代码显示了直接使用ChannelHandlerContext的操作:

// Get reference of ChannelHandlerContext  
ChannelHandlerContext ctx = ..;  
// Write buffer via ChannelHandlerContext  
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));  

该消息流经ChannelPipeline到下一个ChannelHandler,在这种情况下使用ChannelHandlerContext开始下一个ChannelHandler。

如上代码,从指定的ChannelHandlerContext开始,跳过签名所有的ChannelHandler,使用ChannelHandlerContext操作是常见的模式,最常用的是从ChannelHandler调用操作,也可以在外部使用ChannelHandlerContext,因为是线程安全的。

2.2 修改ChannelPipeline

调用ChannelHandlerContext的pipeline()方法能访问ChannelPipeline,能在运行时动态的增加、删除、替换ChannelPipeline中的ChannelHandler。可以保持ChannelHandlerContext供以后使用,如外部Handler方法触发一个事件,甚至从不同的线程。

下面代码显示了保存ChannelHandlerContext供之后使用或其他线程使用:

package netty.in.action.chapter6;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;

public class WriteHandler extends ChannelHandlerAdapter {
    private ChannelHandlerContext ctx;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        this.ctx = ctx;
    }

    public void send(String msg) {
        ctx.write(msg);
    }
}

请注意,ChannelHandler实例如果带有@Sharable注解则可以被添加到多个ChannelPipeline。也就是说单个ChannelHandler实例可以有多个ChannelHandlerContext,因此可以调用不同ChannelHandlerContext获取同一个ChannelHandler。如果添加不带@Sharable注解的ChannelHandler到多个ChannelPipeline则会抛出异常;使用@Sharable注解后的ChannelHandler必须在不同的线程和不同的通道上安全使用。怎么是不安全使用?看下面代码:

package netty.in.action.chapter6;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

@ChannelHandler.Sharable
public class NoSharableHandler extends ChannelInboundHandlerAdapter {
    private int count;

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

上面是一个带@Sharable注解的Handler,它多多个线程使用时,里面的count是不安全的,会导致count错误。

为什么要共享ChannelHandler?使用@Sharable注解共享一个ChannelHandler在一些需求中还是很好用的,如使用一个ChannelHandler来统计连接数或来处理一些全局数据等等。

3.状态模型

netty有一个简单但枪法的装填模型,并完美映射到ChannelInboundHandler的各个方法。下面是Channel生命周期四个不同的状态:

  • channelUnregistered
  • channelRegistered
  • channelActive
  • channelInactive

Channel的状态在其生命周期中的变化,因为状态变化需要触发,下图显示了Channel状态变化:

还可以看到额外的状态变化,因为用户允许从EventLoop中注销Channel暂停事件执行,然后再重新注册。在这种情况下,你会看到多个channelRegistered和channelUnregistered状态的变化,而永远只有一个channelActive和channelInactive的状态,因为一个通道在其生命周期内只能连接一次,之后就会被回收;重新连接,则是创建一个新的通道。

下图显示了从EventLoop中注销Channel后再重新注册的状态变化:

4.ChannelHandler和其子类

Netty中3个实现了ChannelHandler接口的类,其中2个是接口,一个是抽象类。如下图:

4.1 ChannelHandler中的方法

Netty定义了良好的类型层次结构来表示不同的处理程序类型,所有的类型的父类是ChannelHandler。ChannelHandler提供了在其生命周期内添加或从ChannelPipeline中删除的方法。

  • handlerAdded,ChannelHandler将添加到事件上下文中准备处理事件
  • handlerRemoved,将ChannelHandler从事件上下文中删除,不再处理事件
  • exceptionCaught,处理抛出的异常

上面三个方法都需要传递ChannelHandlerContext参数,每个ChannelHandler被添加到ChannelPipeline时会自动创建ChannelHandlerContext。ChannelHandlerContext允许在本地通道安全的存储和检索值。Netty还提供了一个实现ChannelHandler的抽象类:ChannelHandlerAdapter。ChannelHandlerAdapter实现了父类的所有方法,基本上就是传递事件到ChannelPipeline中的下一个ChannelHandler直到结束。

4.2 ChannelInboundHandler

ChannelInboundHandler提供了一些方法在接收数据或Channel状态改变时被调用。下面是ChannelInboundHandler的一些方法:

  • channelRegister,ChannelHandlerContext的Channel被注册到EventLoop
  • channelUnregister,ChannelHandlerContext的Channel从EventLoop中注销
  • channelActive,ChannelHandlerContext的Channel已激活
  • channelInactive,ChannelHandlerContext的Channel结束生命周期
  • channelRead,从当前Channel的对端读取消息
  • channelReadComplete,消息读取完后执行
  • userEventTriggered,一个用户事件被处罚
  • channelWritabilityChanged,改变通道的可写状态,可以使用Channel.isWritable()检查
  • exceptionCaught,重写父类ChannelHandler的方法,处理异常

Netty提供了一个实现了ChannelInboundHandler接口并集成ChannelHandlerAdapter的类:ChannelInboundHandlerAdapter。ChannelInboundHandlerAdapter实现了ChannelInboundHandler的所有方法,作用就是处理消息并将消息转发到ChannelPipeline中的下一个ChannelHandler。ChannelHandlerAdapter的channelRead方法处理完消息后不会自动释放消息,若想自动释放收到的消息,可以使用SimpleChannelInboundHandler<I>。

package netty.in.action.chapter6;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

public class DiscardHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //手动释放消息
        ReferenceCountUtil.release(msg);
    }
}
package netty.in.action.chapter6;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        //不需要手动释放
    }
}

如果需要其他状态改变的通知,可以重写Handler的其他方法。通常自定义消息类型来解码字节,可以实现ChannelInboundHandler或ChannelInboundHandlerAdapter。有一个更好的解决办法,使用编码器的框架可以很容易实现。使用ChannelInboundHandler、ChannelInboundHandlerAdapter、SimpleChannelInboundHandler三个中的一个来处理接收消息,使用哪一个取决于需求;大多数时候使用SimpleChannelInboundHandler处理消息,使用ChannelInboundHandlerAdapter处理其他的“入站”事件或改变状态。

ChannelInitializer用来初始化ChannelHandler,将自定义的各种ChannelHandler添加到ChannelPipeline中。

4.3 ChannelOutboundHandler

ChannelOutboundHandler用来处理“出站”的数据消息。ChannelOutboundHandler提供了下面一些方法:

  • bind,Channel绑定本地地址
  • connect,Channel连接操作
  • disconnect,Channel断开连接操作
  • close,关闭Channel
  • deregister,注销Channel
  • read,读取消息,实际是截获ChannelHandlerContext.read()
  • write,写操作,实际是通过ChannelPipeline写消息,Channel.flush()到实际通道
  • flush,刷新消息到通道

ChannelOutboundHandler是ChannelHandler的子类,实现了ChannelHandler的所有方法。所有重要的方法采取ChannelPromise,因此一旦请求停止从ChannelPipeline转发参数则必须的大通知。Netty提供了ChannelOutboundHandler的实现:ChannelOutboundHandlerAdapter。ChannelOutboundHandlerAdapter实现了父类的所有方法,并且可以根据需要重写感兴趣的方法。所有这些方法的实现,在默认情况下,都是通过调用ChannelHandlerContext的方法将事件转发到ChannelPipeline中下一个ChannelHandler。

看下面的代码。

package netty.in.action.chapter6;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.util.ReferenceCountUtil;

public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        ReferenceCountUtil.release(msg);
        promise.setSuccess();
    }
}

重要的是要记得释放资源并通知ChannelPromise,若ChannelPromise没有被通知可能会导致其中一个ChannelFutureListener不被通知去处理一个消息。

如果消息被消息并且被传递到ChannelPipeline中的下一个ChannelOutboundHandler,那么就需要调用ReferenceCountUtil.release(message)来释放消息资源。一旦消息被传递到实际的通道,它会自动写入消息或在通道关闭是释放。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值