Netty实战(六):ChannelPipeline与ChannelHandlerContext

1,002 阅读5分钟
原文链接: fageiguanbing.gitee.io

ChannelPipeline 接口

Pipeline 在 Netty 中的作用

现在我们在pipeline中添加一个 ServerHandler

public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf requestByteBuf = (ByteBuf) msg;
		
        // 处理后获得需要的对象
        Packet packet = PacketCodeC.INSTANCE.decode(requestByteBuf);
		
        // 如果为登录请求
        if (packet instanceof LoginRequestPacket) {
            
        }
        // 如果为发送消息请求
        else if (packet instanceof MessageRequestPacket) {
            
        }
    }
}

  我们还可能涉及到在业务逻辑之前的验证和编解码问题,总不可能写很多 if else ,Netty 中的 pipeline 正是用来解决这个问题的:它通过责任链设计模式来组织代码逻辑,并且能够支持逻辑的动态添加和删除。

  每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline ,有且只能有一个。

  以一个聊天系统为例,如果我们需要处理许多同级的业务逻辑(私聊请求,登录请求,群聊请求等等),可以在解码后继承 SimpleChannelInboundHandler<业务对象> ,在这个 handler 中直接写业务逻辑,这就实现了对上面代码块 ServerHandler 解耦。

Pipeline 执行顺序

代码如下:

.childHandler(new ChannelInitializer<NioSocketChannel>() {
    protected void initChannel(NioSocketChannel ch) {
        // 处理读数据的pipeline
        ch.pipeline().addLast(new InBoundHandlerA());
        ch.pipeline().addLast(new InBoundHandlerB());
        ch.pipeline().addLast(new InBoundHandlerC());

        // 处理写数据的pipeline
        ch.pipeline().addLast(new OutBoundHandlerA());
        ch.pipeline().addLast(new OutBoundHandlerB());
        ch.pipeline().addLast(new OutBoundHandlerC());
    }
})

执行各个handler后(每个handler的逻辑只有打印自己)输出结果:

InBoundHandlerA
InBoundHandlerB
InBoundHandlerC
OutBoundHandlerC
OutBoundHandlerB
OutBoundHandlerA

Pepeline 的一些操作

名称 描述
addFirst
addBefore
addAfter
addLast
将一个新的 ChannelHandler 添加到 ChannelPipeline中
remove 将一个 ChannelHandler 从 ChannelPipeline 中删除
replace 将 ChannelPipeline 中的一个 ChannelHandler 替换为另一个 ChannelHandler
FirstHandler firstHandler = new FirstHandler();

// "尾插法",往pipeline中的尾部添加handler
// 在添加handler的时候指定名称
pipeline.addLast("firstHandler", firstHandler);

// "头插法",往pipeline中的头部添加handler
// 添加handler的时候直接以"new一个对象的方式",不指定对象引用
pipeline.addFirst("secondHandler", new SecondHandler());

// 直接以对象引用作为key,从pipeline中删除handler
pipeline.remove(firstHandler);

// 以名称作为key,从pipeline中删除handler
pipeline.remove("secondHandler");

注意事项

   关于 ChannelPipeline 中的 ChannelHandler 的阻塞问题。pipeline 中每个 handler 都是通过 EventLoop 来处理的,所以不要阻塞这个handler

   阻塞操作请使用接受 EventExecutorGroupadd*() 重载方法。使用了这个方法的业务逻辑并不会被该 ChannelEventLoop 处理,将会使用这个 EventExecutorGroup 的某个 EventExecutor 处理,从而被该 ChannelEventLoop 删除。

ChannelHandlerContext 接口

ChannelHandlerContext 接口的作用

   每当有 ChannelHandler 添加到 ChannelPipeline 中,都会创建一个 ChannelHandlerContextChannelHandlerContext 的主要功能是管理(它所关联的 handler 和所在 pipeline)其他handler交互。

  ChannelHandlerContext 有很多和 ChannelChannelPipeline 同名方法,他们之间最大的不同点是:调用 ChannelChannelPipeline 上面的方法将会沿着整个 pipeline 传播,而 ChannelHandlerContext 只从当前的 handler 开始,向下传递给一个能够处理该事件的 handler

ChannelHandlerContext 的使用

ChannelHandlerContext ctx = ...;
// 从context上下文中获取相关联的channel
Channel channel = ctx.channel();
channel.write(Unpoolled.copiedBuffer("Netty in action", ChannelPipeline.UTF_8));
ChannelHandlerContext ctx = ...;
// 从context上下文中获取相关联的pipeline
Pipeline pipeline = ctx.pipeline();
pipeline.write(Unpoolled.copiedBuffer("Netty in action", ChannelPipeline.UTF_8));

   上面两个代码块中的事件流是一样的,但是在 ChannelHandlerContext 的级别上,事件从一个 handler 到另一个 handler 的流转是靠 ChannelHandlerContext 上调用完成的。如下代码块:

ChannelHandlerContext ctx = ...;
ctx.write(Unpoolled.copiedBuffer("Netty in action", ChannelPipeline.UTF_8));

这样使用的好处

  • 减少事件流经对它不感兴趣的 handler 的开销
  • 避免提前流经那些对它感兴趣的 handler
  • 想要从某个特定的 handler 开始处理,必须提前获得这个 ChannelHandlerContext 引用;可以提前缓存这个引用,因为 ChannelHandlerContextChannelHandler 之间的关联关系不可变,是线程安全的。
缓存 handler

如下代码块可以实现缓存 context 引用,供稍后使用。

public class WriteHandler extends ChannelHandlerAdapter {
    private ChannelHandlerContext ctx;
    
    @Override
    public void handlerAdded (ChannelHandler ctx) {
        this.ctx = ctx;
    }
    
    public void send (String msg) {
        ctx.writeAndFlush(msg);
    }
}
共享 handler
@Sharable
public class SharableHandler extend ChannelInboundHandlerAdapter {
    @Override
    publci void channelRead (ChannelHandlerContext ctx, Obejct msg) {
        System.out.println("Channel读到消息" + msg);
        // 记录方法调用,并转发给下一个 ChannelHandler
        ctx.fireChannelRead(msg);
    }
}

要实现共享的handler有两个要求:

  • 使用 @Sharable 注解标记,否则在试图将它添加到多个 ChannelPipeline 会抛出异常
  • 不持有任何状态或者这个状态是线程安全的

异常处理

入站异常

public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught (ChannelHandlerContext ctx, Throwable cause) {
        // 异常处理
    }
}

   exceptionCaught 的默认实现是简单的将当前异常转发给 ChannelPipeline 的下一个 ChannelHandler,如果异常到了尾端还未处理,则会被记录(warn级别)为异常未被处理。

出站异常

出站操作的正常或者异常的选项,都基于以下的通知机制:

  • 每个出站操作将会返回一个 ChannelFuture 。注册到上面的 ChannelFutureListener 将在操作完成时被通知这个操作是否成功。

  • 好多 ChannelOutboundHandler 的方法都会要求传入一个 ChannelPromise 参数;它是 ChannelFuture 的子类,他还提供立即通知(调用者可立即感知到)的方法:

    ChannelPromise setSuccess();
    ChannelPromise setFailure(Throwable cause);
    

处理异常可通过两种方式:

第一种方式:处理自己的异常

ChannelFuture future = channel.write(msg);
future.addListener(new ChannelFutureListener () {
    @Override
    public void operationComplete (ChannelFuture f) {
        if(!f.isSuccess()){
            // 失败处理
        }
    }
});

第二种方式:将如下 handler 置为 pipeline 执行链的最尾端,可以处理全局异常

public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
    @Override
    public void write (ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        promise.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                if (!future.isSuccess()) {
                    // 失败处理
                }
            }
        });
    }
}

参考资料

本文由 发给官兵 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出 处。如转载至微信公众号,请在文末添加作者公众号二维码。