ChannelPipeline 和 ChannelHandler 是 Netty 重要的组件之一,通过这篇文章,重点了解这些组件是如何驱动数据流动和处理的。

一、ChannelHandler

上一篇的整体架构图里可以看到,ChannelHandler 负责处理入站和出站的数据。对于入站和出站,ChannelHandler 由不同类型的 Handler 进行处理。下面通过一个示例来演示,将上一篇文章里的 Demo 做一些修改:

增加以下类:

// OneChannelInBoundHandler.java

package com.niklai.demo.handler.inbound;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class OneChannelInBoundHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(OneChannelInBoundHandler.class.getSimpleName()); @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("channel active.....");
ctx.fireChannelActive();
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
logger.info("read message: {}....", buf.toString(CharsetUtil.UTF_8));
ctx.fireChannelRead(msg);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.write(Unpooled.copiedBuffer("OneChannelInBoundHandler answer...", CharsetUtil.UTF_8));
ctx.fireChannelReadComplete();
}
}
// TwoChannelInBoundHandler.java

package com.niklai.demo.handler.inbound;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class TwoChannelInBoundHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(TwoChannelInBoundHandler.class.getSimpleName()); @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("channel active.....");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
logger.info("read message: {}....", buf.toString(CharsetUtil.UTF_8));
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.write(Unpooled.copiedBuffer("TwoChannelInBoundHandler answer...", CharsetUtil.UTF_8));
ctx.close().addListener(ChannelFutureListener.CLOSE);
}
}
// OneChannelOutBoundHandler.java

package com.niklai.demo.handler.outbound;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class OneChannelOutBoundHandler extends ChannelOutboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(OneChannelOutBoundHandler.class.getSimpleName()); @Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = (ByteBuf) msg;
logger.info("write msg: {}.....", buf.toString(CharsetUtil.UTF_8));
ctx.writeAndFlush(msg, promise);
}
}

修改 Server.java 类初始化的 childHandler 逻辑:

// Server.java

// 省略部分代码

public static void init() {
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
serverBootstrap.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress("localhost", 9999))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 添加ChannelHandler
socketChannel.pipeline().addLast(new OneChannelOutBoundHandler()); socketChannel.pipeline().addLast(new OneChannelInBoundHandler());
socketChannel.pipeline().addLast(new TwoChannelInBoundHandler());
}
}); ChannelFuture future = serverBootstrap.bind().sync();
future.channel().closeFuture().sync();
group.shutdownGracefully().sync();
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
} // 省略部分代码

在上面的例子里,我们声明了 OneChannelInBoundHandler 和 TwoChannelInBoundHandler 两个类继承 ChannelInBoundHandlerAdapter 处理入站数据,一个 OneChannelOutBoundHandler 类继承 ChannelOutBoundHandlerAdapter 处理出站数据,依次添加到 ChannelPipeline 里。两个 ChannelInBoundHandler 类都重写了 channelActive、channelRead 和 channelReadComplete 方法,OneChannelOutBoundHandler 类重写了 write 方法。

运行单元测试,控制台得到如下结果:



通过日志输出结果,我们可以看到 Client 发送消息后,OneChannelInBoundHandler 的 channelRead 方法被触发先获得消息内容,调用 ctx.fireChannelRead(msg)方法后 TwoChannelInBoundHandler 的 channelRead 方法被触发再次获得到消息内容,此时消息已经到达队尾。在 channelReadComplete 方法里调用 ctx.write(obj)方法依次写入应答消息后,消息将会反向出站,OneChannelOutBoundHandler 的 write 被触发获得应答消息内容,在这个方法里调用 ctx.writeAndFlush(msg, promise)将应答消息继续发送出去。

注意两个 ChannelInBoundHandler 获取消息是有先后顺序的,顺序取决于添加到 ChannelPipeline 的先后,并且只有当前 ChannelInBoundHandler 的 channelRead 方法里调用了 ctx.fireChannelRead(msg)方法后,消息才能被传递到后面的 ChannelInBoundHandler 的 channelRead 方法,channelReadComplete 方法同理。而在出站时,ChannelOutBoundHandler 的 write 方法会获取到将要写出的消息,可以选择是否对消息进行再次处理后发送出去。

ChannelHandler 相关的类关系图如下,ChannelInBoundHandlerAdapter 和 ChannelOutBoundHandlerAdapter 分别实现了 ChannelInBoundHandler 和 ChannelOutBoundHandler。接口一般通过继承 ChannelInBoundHandlerAdapter 和 ChannelOutBoundHandlerAdapter 来实现业务数据处理:



以下两个接口部分事件方法,更多方法可以查阅官方文档

ChannelInBoundHandler

方法 描述
channelActive Channel 已经连接就绪时被调用
channelRead 当从 Channel 读取数据时被调用
channelReadComplete 当 Channel 的读取操作完成时被调用
exceptionCaught 当入站事件处理过程中出现异常时被调用

ChannelOutBoundHandler

方法 描述
write 当通过 Channel 写数据时被调用
read 当从 Channel 读取数据时被调用

二、ChannelPipeline

从上面的例子可以看到,加入到 ChannelPipeline 的一系列 ChannelHandler 组成了一个有序的链。每一个新创建的 Channel 都将被分配一个 ChannelPipeline,Channel 不能自己附加另外一个 ChannelPipeline,也不能取消当前的,这个是由框架决定的,不需要开发人员干预。



从上图可以看到,事件消息会从头部传递到尾部,然后再从尾部传递到头部。在传递过程中,将会识别 ChannelHandler 的类型,入站事件由 InBoundHandler 处理,出站事件由 OutBoundHandler 处理,如果传递到下一个 ChannelHandler 时发现类型与当前方向不匹配,将会直接跳过并前进到下一个。如果某个 ChannelHandler 同时实现了 ChannelInBoundHandler 和 ChannelOutBundHandler 接口,那么当前 ChannelHandler 将会同时处理入站和出站事件。

以下是 ChannelPipeline 的一些主要方法:

方法 说明
addFirst
addLast
添加 ChannelHander 到当前 ChannelPipeline 的头/尾部
addBefore
addAfter
添加 ChannelHander 到当前 ChannelPipeline 里某个 ChannelHandler 的前/后面
remove 将某个 ChannelHandler 从当前 ChannelPipeline 里移除
replace 将当前 ChannelPipeline 里的某个 ChannelHandler 替换成另外一个 ChannelHandler

除此之外,ChannelPipeline 也有一些触发事件的方法,以下列出跟当前演示例子相关的事件方法,更多方法可以查阅官方文档

方法 描述
fireChannelActive 调用 ChannelPipeline 里下一个 ChannelInBoundHandler 的 channelActive 方法
fireChannelRead 调用 ChannelPipeline 里下一个 ChannelInBoundHandler 的 ChannelRead 方法
write 调用 ChannelPipeline 里下一个 ChannelOutBoundHandler 的 write 方法

我们修改一下 Demo 中的例子:

// OneChannelInBoundHandler.java

// 省略代码

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
logger.info("read message: {}....", buf.toString(CharsetUtil.UTF_8));
ctx.pipeline().fireChannelRead(msg); // 调用ChannelPipeline的fireChannelRead方法
} // 省略代码

运行单元测试查看控制台日志,发现事件会反复触发 OneChannelInBoundHandler 的 channelRead 方法,直到死循环。对比之前的运行结果可以看到,ChannelPipeline 的 fireChannelRead 方法会将事件重新从头部开始向后传递,而 ctx.fireChannelRead 方法会将事件从当前的下一个 ChannelHandler 开始向后传递。

三、ChannelHandlerContext

ChannelHandlerContext 是一个接口,它维护了 ChannelHandler 和 ChannelPipeline 两者之间的关系。当一个 ChannelHandler 加入到 ChannelPipeline 里时,就会创建一个 ChannelHandlerContext 关联它们。下图展示了它们之间的关系,当调用 ChannelHandlerContext 的 fire...方法时,事件都将会被传递到它关联的 ChannelHandler 的下一个 ChannelHandler 上



ChannelHandlerContext 部分的 API 如下,更多 API 可以查阅官方文档

方法 描述
pipeline 获取关联的 ChannelPipeline
handler 获取关联的 ChannelHandler
fireChannelRead 触发下一个 ChannelInBoundHandler 的 channelRead 方法

四、异常处理

入站异常

如果在处理入站事件过程中发生了异常,则该异常将会从它所在的 ChannelInBoundHandler 开始传递直到 ChannelPipeline 尾部。通过重写 exceptionCaught 方法,可以处理异常。

修改一下 Demo,增加 exceptionCaught

// OneChannelInBoundHandler.java

// 省略部分代码

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.write(Unpooled.copiedBuffer("OneChannelInBoundHandler answer...", CharsetUtil.UTF_8));
ctx.fireChannelReadComplete();
throw new Exception("This is an exception!"); // 模拟抛出一个异常
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("OneChannelInBoundHandler exception:{}....", cause.getMessage(), cause);
} // 省略部分代码

运行测试,可以看到异常信息已经打印到控制台日志:



再次修改 Demo,调用 ChannelHandlerContext 的 fireExceptionCaught 方法将异常继续传递下去

// OneChannelInBoundHandler.java

// 省略部分代码

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("OneChannelInBoundHandler exception:{}....", cause.getMessage(), cause);
ctx.fireExceptionCaught(cause); // 将异常传递下去
} // 省略部分代码
// TwoChannelInBoundHandler.java

// 省略部分代码

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("TwoChannelInBoundHandler exception:{}....", cause.getMessage(), cause);
} // 省略部分代码

运行测试,查看控制台日志,两个 ChannelInBoundHandler 都会打印异常日志:



如果,两个 ChannelInBoundHandler 都不重写 exceptionCaught 方法处理异常,会怎样?修改 Demo,删除 exceptionCaught 后再次运行测试,查看控制台日志:



控制台输出一条日志信息:An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.如果异常发生但是没有被处理,异常将会一直传递到 ChannelPipeline 并记录为未处理异常,以 WARN 级别日志输出。

出站异常

出站操作的相关方法是异步的,处理异常信息都是基于通知机制。处理方式有两种:

第一种是通过在方法返回值上注册 listener:

// OneChannelOutBoundHandler.java

// 省略部分代码

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = (ByteBuf) msg;
logger.info("write msg: {}.....", buf.toString(CharsetUtil.UTF_8));
ctx.close(); // 在发送消息之前关闭channel,后序写入数据将会引发异常。
ChannelFuture channelFuture = ctx.writeAndFlush(msg);
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (!f.isSuccess()) {
logger.error("OneChannelOutBoundHandler cause:{}.......", f.cause().getMessage(), f.cause());
}
}
});
} // 省略部分代码

第二种是在传入的参数 promise 上注册 listener:

// OneChannelOutBoundHandler.java

// 省略部分代码

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = (ByteBuf) msg;
logger.info("write msg: {}.....", buf.toString(CharsetUtil.UTF_8));
ctx.close(); // 在发送消息之前关闭channel,后序写入数据将会引发异常。 promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (!f.isSuccess()) {
logger.error("OneChannelOutBoundHandler cause:{}.......", f.cause().getMessage(), f.cause());
}
}
}); ctx.writeAndFlush(msg, promise);
} // 省略部分代码

Netty学习笔记(二) - ChannelPipeline和ChannelHandler的更多相关文章

  1. Netty学习笔记(二) 实现服务端和客户端

    在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...

  2. Netty学习笔记(二)——netty组件及其用法

    1.Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端. 原生NIO存在的问题 1) NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector.Se ...

  3. Netty学习笔记(二)

    只是代码,建议配合http://ifeve.com/netty5-user-guide/此网站观看 package com.demo.netty; import org.junit.Before;im ...

  4. Netty 学习笔记(1)通信原理

    前言 本文主要从 select 和 epoll 系统调用入手,来打开 Netty 的大门,从认识 Netty 的基础原理 —— I/O 多路复用模型开始.   Netty 的通信原理 Netty 底层 ...

  5. Netty学习笔记-入门版

    目录 Netty学习笔记 前言 什么是Netty IO基础 概念说明 IO简单介绍 用户空间与内核空间 进程(Process) 线程(thread) 程序和进程 进程切换 进程阻塞 文件描述符 文件句 ...

  6. Netty 学习(四):ChannelHandler 的事件传播和生命周期

    Netty 学习(四):ChannelHandler 的事件传播和生命周期 作者: Grey 原文地址: 博客园:Netty 学习(四):ChannelHandler 的事件传播和生命周期 CSDN: ...

  7. [Firefly引擎][学习笔记二][已完结]卡牌游戏开发模型的设计

    源地址:http://bbs.9miao.com/thread-44603-1-1.html 在此补充一下Socket的验证机制:socket登陆验证.会采用session会话超时的机制做心跳接口验证 ...

  8. 自顶向下深入分析Netty(七)--ChannelPipeline和ChannelHandler总述

    自顶向下深入分析Netty(七)--ChannelPipeline和ChannelHandler总述 自顶向下深入分析Netty(七)--ChannelPipeline源码实现 自顶向下深入分析Net ...

  9. Netty 学习(二):服务端与客户端通信

    Netty 学习(二):服务端与客户端通信 作者: Grey 原文地址: 博客园:Netty 学习(二):服务端与客户端通信 CSDN:Netty 学习(二):服务端与客户端通信 说明 Netty 中 ...

  10. WPF的Binding学习笔记(二)

    原文: http://www.cnblogs.com/pasoraku/archive/2012/10/25/2738428.htmlWPF的Binding学习笔记(二) 上次学了点点Binding的 ...

随机推荐

  1. P5343 【XR-1】分块(dp矩阵加速)

    \(大意是用数组a里的数字,组成一个序列,使得序列和为n的方案种数\)传送门 \(先考虑dp.\) \(但是不能直接用背包转移,因为是序列,要考虑顺序.\) \(所以,为了去重,我们令dp[i][j] ...

  2. 第3章:关系数据库标准语言 SQL

    目录 第3章:关系数据库标准语言 SQL 3.1.SQL概述 3.1.1.历史 3.3.2.SQL语言的功能 3.3.3.SQL的特点 3.3.4.基本概念 3.2.学生-课程数据库 3.3.数据定义 ...

  3. js获取session对象

  4. 【Spark】SparkStreaming的容错机制

    文章目录 检查点机制 驱动器程序容错 工作节点容错 接收器容错 处理保证 检查点机制 Metadata checkpointing -- 将定义流计算的信息存入容错的系统如HDFS. Data che ...

  5. Day_09【常用API】扩展案例7_验证QQ号是否正确

    需求说明 请用户输入一个"QQ号码",我们来判断这个QQ号码是否正确 要求:使用方法来完成判断功能 验证规则: 1)必须是5-12位数字: 2)首位不能是0: package co ...

  6. 简单服务发现协议(SSDP)编程指导

    本文主要讲解如何使用ssdp进行编程,不涉及过多关于ssdp的理论知识 1. 前言   网上对ssdp理论介绍的一大把,缺乏从编程角度的一个指导,这里不会长篇大论ssdp理论,只是根据实际编码经验对用 ...

  7. Ubuntu系统make menuconfig的依赖包ncurses安装

    Linux内核或者u-boot进行make menuconfig的时候,如果系统上没有安装ncurses,就会出现以下报错 *** Unable to find the ncurses librari ...

  8. git版本控制系统小白教程(下)

    前言:本文主要介绍git版本控制系统的一些基础使用,适合小白入门,因为内容较多,会分为两部分进行分享,查看上部请点传送门. 删除文件 ​ git删除文件一般有三种情况,第一种是在工作区修改了文件,但是 ...

  9. JVM-Java内存区域与内存溢出异常

    第二章:Java内存区域与内存溢出异常 2.1 运行时数据区 1:程序计数器  : 线程私有,较小的内存空间,当前线程所执行的字节码的行号指示器,唯一不会发生错误的内存区域 2:虚拟机栈: 线程私有, ...

  10. 【教程】高德地图使用ECharts实现数据可视化

    关于百度地图结合ECharts实现数据可视化的资料已经很多了,毕竟是官方提供支持的,这里就不再赘述.今天我们来讲一下让高德地图与ECharts结合来实现数据可视化图表的展示. 一.ECharts 高德 ...