Netty 框架学习 —— 预置的 ChannelHandler 和编解码器
Netty 为许多提供了许多预置的编解码器和处理器,几乎可以开箱即用,减少了在烦琐事务上话费的时间和精力
空闲的连接和超时
检测空闲连接以及超时对于释放资源来说至关重要,Netty 特地为它提供了几个 ChannelHandler 实现
| 名称 | 描述 |
|---|---|
| IdleStateHandler | 当连接空闲时间太长时,将会触发一个 IdleStateEvent 事件,然后,你可以通过在 ChannelInboundHandler 重写 userEventTriggered() 方法来处理该 IdleStateEvent 事件 |
| ReadTimeoutHandler | 如果在指定的时间间隔内没有收到入站数据,则抛出一个 ReadTimeoutException 并关闭对应的 Channel。可以通过重写你的 ChannelHandler 中的 exceptionCaught() 方法来检测该 ReadTimeoutException |
| WriteTimeoutHandler | 如果在指定的时间间隔内没有出站数据写入,则抛出一个 WriteTimeoutException 并关闭对应的 Channel。可以通过重写你的 ChannelHandler 中的 exceptionCaught() 方法来检测该 WriteTimeoutException |
下述代码展示了当使用通常的发送心跳消息到远程节点的方法时,如果 60 秒内没有接收或者发送任何数据,我们将得到通知,如果没有响应,则连接会关闭
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// IdleStateHandler 将在被触发时发送一个 IdleStateEvent 事件
pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
// 将一个 HeartbeatHandler 添加到 ChannelPipeline
pipeline.addLast(new HeartbeatHandler());
}
public static final class HeartbeatHandler extends SimpleChannelInboundHandler {
// 发送到远程节点的心跳消息
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled
.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// 发送心跳消息,并在发送失败时关闭该连接
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
}
解码基于分隔符的协议
基于分隔符的消息协议使用定义的字符来标记消息或者消息段的开头或者结尾,下表列出的解码器能帮助你定义可以提取由任意标记序列分隔的帧的自定义解码器
| 名称 | 描述 |
|---|---|
| DelimiterBasedFrameDecoder | 使用由用户提供的分隔符来提取帧 |
| LineBasedFrameDecoder | 由行尾符(\n 或者 \r\n)分隔帧 |
下述代码展示了如何使用 LineBasedFrameDecoder 处理由行尾符分隔的帧
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 该 LineBasedFrameDecoder 将提取的帧转发给下一个 ChannelInboundHandler
pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
// 添加 FrameHandler 以接收帧
pipeline.addLast(new FrameHandler());
}
public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// do something
}
}
}
如果你还使用除了行尾符之外的分隔符来分隔帧,那么你还可以使用 DelimiterBasedFrameDecoder,只需要将特定的分隔符序列指定到其构造函数即可
作为示例,我们将使用下面的协议规范:
- 传入数据流是一系列的帧,每个帧都由换行符 \n 来分隔
- 每个帧都由一系列元素组成,每个元素由单个空格字符分隔
- 一个帧的内容代表一个命令,定义为一个命令名称后跟着数目可变的参数
基于这个协议,我们的自定义解码器将定义以下类:
- Cmd —— 将帧的命令存储在 ByteBuf 中,一个 ByteBuf 用于名称,另一个用于参数
- CmdDecoder —— 从被重写了的 decode() 方法中获取一行字符串,从它的内容构建一个 Cmd 实例
- CmdHandler —— 从 CmdDecoder 获取解码的 Cmd 对象,并对它进行一些处理
public class CmdHandlerInitializer extends ChannelInitializer<Channel> {
static final byte SPACE = (byte) ' ';
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new CmdDecoder(64 * 1024));
pipeline.addLast(new CmdHandler());
}
/**
* Cmd POJO
*/
public static final class Cmd {
private final ByteBuf name;
private final ByteBuf args;
public Cmd(ByteBuf name, ByteBuf args) {
this.name = name;
this.args = args;
}
public ByteBuf getArgs() {
return args;
}
public ByteBuf getName() {
return name;
}
}
public static final class CmdDecoder extends LineBasedFrameDecoder {
public CmdDecoder(int maxLength) {
super(maxLength);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
// 从 ByteBuf 中提取由行尾符序列分隔的帧
ByteBuf frame = (ByteBuf) super.decode(ctx, buffer);
// 如果输入中没有帧,则返回 null
if (frame == null) {
return null;
}
// 查找第一个空格字符的索引,前面是命令名称,后面是参数
int index = frame.indexOf(frame.readerIndex(), frame.writerIndex(), SPACE);
// 使用包含命令名称和参数的切片创建新的 Cmd 对象
return new Cmd(frame.slice(frame.readerIndex(), index), frame.slice(index + 1, frame.writerIndex()));
}
}
public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd> {
@Override
protected void messageReceived(ChannelHandlerContext ctx, Cmd msg) throws Exception {
// 处理传经 ChannelPipeline 的 Cmd 对象
}
}
}
基于长度的协议
基于长度的协议通过将它的长度编码到帧的头部来定义帧,而不是使用特殊的分隔符来标记它的结束,下表列出 Netty 提供的用于处理这种类型的协议的两种解码器
| 名称 | 描述 |
|---|---|
| FixedLengthFrameDecoder | 提取在调用构造函数时指定的定长帧 |
| LengthFieldBasedFrameDecoder | 根据帧头部中的长度值来提取帧:该字段的偏移量以及长度在构造函数中指定 |
你经常会遇到被编码到消息头部的帧大小不是固定值的协议,为了处理这种变长帧,你可以使用 LengthFieldBasedFrameDecoder,它将从头部字段确定帧长,然后从数据流中提取指定的字节数
下图展示了一个示例,其中长度字段在帧中的偏移量为 0,并且长度为 2 字节

下述代码展示了如何使用其 3 个构造函数分别为 maxFrameLength、lengthFieldOffser 和 lengthFieldLength 的构造函数。在这个场景下,帧的长度被编码到了帧起始的前 8 个字节中
public class LengthBasedInitializer extends ChannelInitializer<Channel> {
/**
* 使用 LengthFieldBasedFrameDecoder 解码将帧长度编码到帧起始的前 8 个字节中的消息
*/
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8));
pipeline.addLast(new FrameHandler());
}
public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// do something
}
}
}
写大型数据
因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。由于写操作是非阻塞的,所以即时没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture。当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险。所以在写大型数据时,需要考虑处理远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。让我们考虑下将一个文件内容写出到网络的情况
NIO 的零拷贝特性,这种特性消除了将文件的内容从文件系统移动到网络栈的复制过程。所有这一切都发生在 Netty 的核心中,所以应用程序需要做的就是使用一个 FileRegion 接口的实现
下述代码展示了如何通过从 FileInputStream 创建一个 DefaultFileRegion,并将其写入 Channel
// 创建一个 FileInputStream
leInputStream in = new FileInputStream(file);
// 以该文件的的完整长度创建一个新的 DefaultFileRegion
FileRegion region = new DefaultFileRegion(in.getChannel(), 0, file.length());
// 发送该 DefaultFileRegion,并注册一个 ChannelFutureListener
channel.writeAndFlush(region).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
// 处理失败
if(!future.isSuccess()) {
Throwable cause = future.cause();
// do something
}
}
});
这个示例只适用于文件内容的直接传输,不包括应用程序对数据的任何处理。在需要将数据从文件系统复制到用户内存中时,可以使用 ChunkedWriteHandler,它支持异步写大型数据流,而又不会导致大量的内存消耗
interface ChunkedInput<B> 中的类型参数 B 是 readChunk() 方法返回的类型。Netty 预置了该接口的四个实现,如表所示,每个都代表了一个将由 ChunkedWriteHandler 处理的不定长度的数据流
| 名称 | 描述 |
|---|---|
| ChunkedFile | 从文件中逐块获取数据,当你的平台不支持零拷贝或者你需要转换数据时使用 |
| ChunkedNioFile | 和 ChunkedFile 类似,只是它使用了 FileChannel |
| ChunkedStream | 从 InputStream 中逐块传输内容 |
| ChunkedNioStream | 从 ReadableByteChannel 中逐步传输内容 |
下述代码说明了 ChunkedStream 的用法,它是实践中最常用的实现。所示的类使用了一个 File 以及一个 SSLContext 进行实例化,当 initChannel() 方法被调用时,它将使用所示的 ChannelHandler 链初始化该 Channel
当 Channel 的状态变为活动时,WriteStreamHandler 将会逐块地把来自文件中的数据作为 ChunkedStream 写入
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
private final File file;
private final SslContext sslContext;
public ChunkedWriteHandlerInitializer(File file, SslContext sslContext) {
this.file = file;
this.sslContext = sslContext;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new SslHandler(sslContext.newEngine(ch.alloc())));
// 添加 ChunkedWriteHandler 以处理作为 ChunkedInput 传入的数据
pipeline.addLast(new ChunkedWriteHandler());
// 一旦连接建立,WriteStreamHandler 就开始写文件数据
pipeline.addLast(new WriteStreamHandler());
}
public final class WriteStreamHandler extends SimpleChannelInboundHandler<Channel> {
/**
* 当连接建立时,channelActive() 方法将使用 ChunkedInput 写文件数据
*/
@Override
protected void messageReceived(ChannelHandlerContext ctx, Channel msg) throws Exception {
super.channelActive(ctx);
ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
}
}
}
序列化数据
JDK 提供了 ObjectOutputStream 和 ObjectInputStream,用于通过网络对 POJO 的基本数据类型和图进行序列化和反序列化。该 API 并不复杂,可以被应用于任何实现了 java.io.Serializable 接口的对象。但它的性能并不高效,在这一节,我们将看到 Netty 如何实现序列化
1. JDK 序列化
如果你的程序必须要和使用了 ObjectOutputStream 和 ObjectInputStream 的远程节点交互,并且考虑兼容性,那么 JDK 序列化将是正确的选择,下表列出了 Netty 提供的用于和 JDK 进行交互操作的序列化类
| 名称 | 描述 |
|---|---|
| CompatibleObjectDecoder | 和使用 JDK 序列化的非基于 Netty 的远程节点进行互操作的解码器 |
| CompatibleObjectEncoder | 和使用 JDK 序列化的非基于 Netty 的远程节点进行互操作的编码器 |
| ObjectDecoder | 构建于 JDK 序列化之上的使用自定义的序列化来解码的解码器 |
| ObjectEncoder | 构建于 JDK 序列化之上的使用自定义的序列化来编码的编码器 |
2. Protocol Buffers 序列化
Protocol Buffers 是一种由 Google 公司开发的、开源的数据交换格式,以一种紧凑而高效的方式对结构化的数据进行编码以及解码,能跨多语言使用。下表展示了 Netty 为支持 Protobuf 所提供的 ChannelHandler 实现
| 名称 | 描述 |
|---|---|
| ProtobufDecoder | 使用 Protobuf 对消息进行解码 |
| ProtobufEncoder | 使用 Protobuf 对消息进行编码 |
| ProtobufVarint32FrameDecoder | 根据消息中的 Google Protobuf Buffers 的 Base 128 Varints 整型长度字段值动态地分割所接收到的 ByteBuf |
| ProtobufVarint32LengthFieldPrepender | 由 ByteBuf 前追加一个 Google Protobuf Buffers 的 Base 128 Varints 整型的长度字段值 |
Netty 框架学习 —— 预置的 ChannelHandler 和编解码器的更多相关文章
- Netty实战十一之预置的ChannelHandler和编解码器
Netty为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相当繁琐的事务上本来会花费的时间与精力.我们将探讨这些工具以及它们所带来的好处,其中包括Netty对于SSL/TLS和 ...
- Netty 框架学习 —— 单元测试
EmbeddedChannel 概述 ChannelHandler 是 Netty 程序的关键元素,所以彻底地测试它们应该是你的开发过程中的一个标准部分,EmbeddedChannel 是 Netty ...
- Netty 框架学习 —— 编解码器框架
编解码器 每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换.这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将 ...
- Netty 框架学习 —— ChannelHandler 与 ChannelPipeline
ChannelHandler 1. Channel 生命周期 Channel 的生命周期状态如下: 状态 描述 ChannelUnregistered Channel 已经被创建,但还未注册到 Eve ...
- Netty 框架学习 —— 引导
概述 前面我们学习了 ChannelPipeline.ChannelHandler 和 EventLoop 之后,接下来的问题是:如何将它们组织起来,成为一个可实际运行的应用程序呢?答案是使用引导(B ...
- Netty 框架学习 —— 第一个 Netty 应用
概述 在本文,我们将编写一个基于 Netty 实现的客户端和服务端应用程序,相信通过学习该示例,一定能更全面的理解 Netty API 该图展示的是多个客户端同时连接到一台服务器.客户端建立一个连接后 ...
- Netty 框架学习 —— EventLoop 和线程模型
EventLoop 接口 Netty 是基于 Java NIO 的,因此 Channel 也有其生命周期,处理一个连接在其生命周期内发生的事件是所有网络框架的基本功能.通常来说,我们使用一个线程来处理 ...
- Netty 框架学习 —— 基于 Netty 的 HTTP/HTTPS 应用程序
通过 SSL/TLS 保护应用程序 SSL 和 TLS 安全协议层叠在其他协议之上,用以实现数据安全.为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLConte ...
- Netty 框架学习 —— Netty 组件与设计
Channel.EventLoop 和 ChannelFuture 这一节将对 Channel.EventLoop 和 ChannelFuture 类进行讨论,它们组合在一起,可以被认为是 Netty ...
随机推荐
- Python数模笔记-NetworkX(3)条件最短路径
1.带有条件约束的最短路径问题 最短路径问题是图论中求两个顶点之间的最短路径问题,通常是求最短加权路径. 条件最短路径,指带有约束条件.限制条件的最短路径.例如,顶点约束,包括必经点或禁止点的限制:边 ...
- [Java] HOW2J(Java中级)
异常 定义:导致程序正常流程被中断的事件 异常处理常见手段 try catch:将可能抛出异常的代码放在try的块中,一旦出现异常就跳转到catch的块中处理 throws/throw:不在本模块处理 ...
- 使用ps、top、ps_mem命令找出Linux中的最大内存消耗过程
使用ps.top.ps_mem命令找出Linux中的最大内存消耗过程 2020-02-08 16:06:59作者:自力稿源:云网牛站 您可能已经看到Linux系统多次消耗过多的内存,如果是这种情况,那 ...
- IT菜鸟之计算机硬件
现在的人们几乎无时无刻都会碰到计算机!不管是桌面计算机.笔记本电脑.平板计算机.智能型手机等等,这些东西都算计算机.虽然接触的怎么多,但是,我们一般很少会专门了解计算机内部的构成,下面就是自己在听课结 ...
- 8.10-11 mount、umount
8.10 mount:挂载文件系统 mount命令可以将指定的文件系统挂载到指定目录(挂载点),在Linux系统下必须先挂载所有的设备,然后才能被访问,挂载其实就是为要访问的设置开个门(开门才能访问) ...
- 9.11 strace:跟踪进程的系统调用 、ltrace:跟踪进程调用库函数
strace 是Linux环境下的一款程序调试工具,用于检查一个应用程序所使用的系统调用以及它所接收的系统信息.strace会追踪程序运行时的整个生命周期,输出每一个系统调用的名字.参数.返回值和执行 ...
- STM32关于多线程运行的疑问
我有一个疑问,如果 STM32在操作系统环境下 能够一个线程串口中断接收数据 另一个线程 继续进行其他的操作
- kylin优化的思想
kylin的核心优势在于使用额外的空间存储预计算的结果,以换取查询时间的缩减. 所以我们要对额外的空间进行优化,并且在空间优化之后,cuboid无法完全命中时,对查询时间进行优化. 空间优化的思路就是 ...
- 『动善时』JMeter基础 — 34、JMeter接口关联【XPath提取器】
目录 1.XPath提取器介绍 2.XPath提取器界面详解 3.XPath提取器的使用 (1)测试计划内包含的元件 (2)网易首页请求界面内容 (3)XPath提取器界面内容 (4)百度首页请求界面 ...
- [leetcode] 875. 爱吃香蕉的珂珂(周赛)
875. 爱吃香蕉的珂珂 这题时间要求比较严格... 首先,将piles排序,然后二分查找. 总之,答案K肯定位于piles[?]piles[?+1]或者1piles[0]之间 所以我们先二分把?找到 ...