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密切相关的状态模型.

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();
}
}
});
}
}

Netty之Channel*的更多相关文章

  1. Netty 源码解析(二):Netty 的 Channel

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty源码解析(一):开始 当前:Netty 源码解析(二): Netty 的 Channel ...

  2. spark2.1源码分析3:spark-rpc如何实现将netty的Channel隐藏在inbox中

    class TransportServer bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Overri ...

  3. spark-rpc是如何实现将netty的Channel隐藏在inbox中的

    class TransportServer bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Overri ...

  4. Netty:Channel 建立后消息发送失败

    1. 问题现象 Channel 建立后消息发送失败: ChannelFuture future = DeviceManager.getBootstrap().connect(); deviceChan ...

  5. Netty Associated -- Channel

    A nexus to a network socket or a component which is capable of I/O operations such as read, write, c ...

  6. Netty的Channel

    Channel是一个网络端口连接,或者是可以进行读,写,链接,绑定端口的组件的连接.  Channel就是一个链接,它提供了如下的功能. 1:获取当前链接的状态 2:配置当前链接参数 3:进行read ...

  7. netty笔记-:Channel与ChannelHandlerContext执行write方法的区别

      在netty中有我们一般有两种发送数据的方式,即使用ChannelHandlerContext或者Channel的write方法,这两种方法都能发送数据,那么其有什么区别呢.这儿引用netty文档 ...

  8. Netty:Channel

    上一篇我们通过一个简单的Netty代码了解到了Netty中的核心组件,这一篇我们将围绕核心组件中的Channel来展开学习. Channel的简介 Channel代表着与网络套接字或者能够进行IO操作 ...

  9. 项目系统Netty的Channel和用户之间的关系绑定正确做法,以及Channel通道的安全性方案

    前言 考虑一个功能业务,在web程序中向指定的某个用户进行实时通讯 在Web运用的Socket通讯功能中(如在线客服),为保证点对点通讯.而这个看似简单的根据用户寻到起channel通道实际会碰到不少 ...

随机推荐

  1. 双链路接入(双出口)isp运营商(负载分担)

    USG作为校园或大型企业出口网关可以实现内网用户通过两个运营商访问Internet,并保护内网不受网络攻击. 组网需求 某学校网络通过USG连接到Internet,校内组网情况如下: 校内用户主要分布 ...

  2. 创建双向 CA x509 验证证书 kube-apiserver

    1. 设置 kube-apiserver 的 CA 证书相关的文件和启动参数 使用 OpenSSL 工具在 Master 服务器上创建 CA 证书和私钥相关的文件: # openssl genrsa ...

  3. testNG安装与使用

    1.Eclipse集成TestNG插件 a.下载TestNG离线插件并解压得到features和plugins两个文件夹: b.将features文件下的org.testng.eclipse_6.9. ...

  4. js 实现匀速移动

    js 实现匀速移动 <!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...

  5. nohup、&、 2>&1详解

    前言 对一个程序员来说,java项目的打包部署也是一项必须掌握的一项技术任务,现我将自己平时在maven下打包以及部署项目总结,希望对有这方面诉求的小伙伴有所帮助! 一.maven项目打包及命令 (1 ...

  6. JS中innerHTML、outerHTML、innerText 、outerText、value的区别与联系?

    1.innerHTML 属性 (参考自<JavaScript高级程序设计>294页) 在读模式下,innerHTML 属性返回与调用元素的所有子节点(包括元素.注释和文本节点)对应的 HT ...

  7. Spring Boot中如何自定义starter?

    Spring Boot starter 我们知道Spring Boot大大简化了项目初始搭建以及开发过程,而这些都是通过Spring Boot提供的starter来完成的.品达通用权限系统就是基于Sp ...

  8. dart系列之:创建Library package

    目录 简介 Library package的结构 导入library 条件导入和导出library 添加其他有效的文件 library的文档 发布到pub.dev 总结 简介 在dart系统中,有pu ...

  9. [loj3304]作业题

    (以下假设$T=(V,\{e_{1},e_{2},...,e_{n-1} \})$是一棵树) 根据莫比乌斯反演,有$\gcd(w_{1},w_{2},...,w_{e_{n-1}})=\sum_{d| ...

  10. Go语言程序结构之变量

    初识Go语言之变量 var声明创建一个具体类型的变量,然后给它附加一个名字,设置他的初始值,这种声明都是一个通用的形式: var name type = expression 在实际的开发中,为了方便 ...