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。
阻塞操作请使用接受 EventExecutorGroup 的 add*() 重载方法。使用了这个方法的业务逻辑并不会被该 Channel 的 EventLoop 处理,将会使用这个 EventExecutorGroup 的某个 EventExecutor 处理,从而被该 Channel 的 EventLoop 删除。
ChannelHandlerContext 接口
ChannelHandlerContext 接口的作用
每当有 ChannelHandler 添加到 ChannelPipeline 中,都会创建一个 ChannelHandlerContext,ChannelHandlerContext 的主要功能是管理(它所关联的 handler 和所在 pipeline)其他handler交互。
ChannelHandlerContext 有很多和 Channel, ChannelPipeline 同名方法,他们之间最大的不同点是:调用 Channel 和 ChannelPipeline 上面的方法将会沿着整个 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引用;可以提前缓存这个引用,因为ChannelHandlerContext和ChannelHandler之间的关联关系不可变,是线程安全的。
缓存 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()) {
// 失败处理
}
}
});
}
}
参考资料
-
《Netty in action》
-
掘金小册《Netty 入门与实战:仿写微信 IM 即时通讯系统》: juejin.cn/book/684473…
Github: github.com/lightningMa…
本文由 发给官兵 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出 处。如转载至微信公众号,请在文末添加作者公众号二维码。