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 ...
随机推荐
- Linux 通过 UUID 在 fstab 中自动挂载分区
Linux 通过 UUID 在 fstab 中自动挂载分区 summerm6关注 2019.10.17 16:29:00字数 1,542阅读 605 https://xiexianbin.cn/lin ...
- Linux基本原则
Bash特性 Shell shell(外壳),广义的shell可以理解为是用户的工作环境,在windows看来桌面就是一个shell,在linux看来终端就是shell 常见的shell有两种,一种是 ...
- 052.Python前端Django框架路由层和视图层
一.路由层(URLconf) 1.1 路由层简单配置 URL配置(URLconf)就像Django 所支撑网站的目录.它的本质是URL与要为该URL调用的视图函数之间的映射表:你就是以这种方式告诉Dj ...
- pre -regulator 前端稳压器
regulator
- 2.7循环_while
循环 目标 程序的三大流程 while 循环基本使用 break 和 continue while 循环嵌套 01. 程序的三大流程 在程序开发中,一共有三种流程方式: 顺序 -- 从上向下,顺序执行 ...
- Redis(3)- 数据结构
一.Redis数据结构 Redis数据结构:Redis在数据类型上常用的有5种数据类型,而底层实现拥有种.可以使用命令OBJECT ENCODING K1查询底层数据结构. # 查询key的底层数据类 ...
- nignx反向代理web服务器的配置与使用
一.为什么要用nignx反向代理 1.负载均衡 当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃.为了避免服务器崩溃,让用户有更好的体验,我们通过负载均 ...
- java并发编程工具类JUC第四篇:LinkedBlockingQueue链表队列
在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue. LinkedBlockingQueue 队列是Blo ...
- 通过Maven打jar包&运行
运行命令:java -jar [包名] https://www.cnblogs.com/jinjiyese153/p/9374015.html
- DHCP与DHCP中继
DHCP原理与配置 1. DHCP应用场景 2. DHCP报文类型 3. DHCP工作原理 4. IP地址获取与释放 5. DHCP中继配置 1. DHCP应用场景 在大型企业网络中,会有大量的主机或 ...