netty系列之:netty中的自动解码器ReplayingDecoder
简介
netty提供了一个从ByteBuf到用户自定义的message的解码器叫做ByteToMessageDecoder,要使用这个decoder,我们需要继承这个decoder,并实现decode方法,从而在这个方法中实现ByteBuf中的内容到用户自定义message对象的转换。
那么在使用ByteToMessageDecoder的过程中会遇到什么问题呢?为什么又会有一个ReplayingDecoder呢?带着这个问题我们一起来看看吧。
ByteToMessageDecoder可能遇到的问题
要想实现自己的解码器将ByteBuf转换成为自己的消息对象,可以继承ByteToMessageDecoder,然后实现其中的decode方法即可,先来看下decode方法的定义:
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception
输入的参数中buf是要解码的ByteBuf,out是解码过后的对象列表,我们需要把ByteBuf中的数据转换成为我们自己的对象加入out的list中。
那么这里可能会遇到一个问题,因为我们在调用decode方法的时候buf中的数据可能还没有准备好,比如我们需要一个Integer,但是buf中的数据不够一个整数,那么就需要一些buf中数据逻辑的判断,我们以一个带有消息长度的Buf对象来描述一下这个过程。
所谓带有消息长度的Buf对象,就是说Buf消息中的前4位,构成了一个整数,这个整数表示的是buf中后续消息的长度。
所以我们读取消息进行转换的流程是,先读取前面4个字节,得到消息的长度,然后再读取该长度的字节,这就是我们真正要获取的消息内容。
来看一下如果是继承自ByteToMessageDecoder应该怎么实现这个逻辑呢?
public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception {
if (buf.readableBytes() < 4) {
return;
}
buf.markReaderIndex();
int length = buf.readInt();
if (buf.readableBytes() < length) {
buf.resetReaderIndex();
return;
}
out.add(buf.readBytes(length));
}
}
在decode中,我们首先需要判断buf中可读的字节有没有4个,没有的话直接返回。如果有,则先读取这4个字节的长度,然后再判断buf中的可读字节是否小于应该读取的长度,如果小于,则说明数据还没有准备好,需要调用resetReaderIndex进行重置。
最后,如果所有的条件都满足,才真正进行读取工作。
有没有一个办法可以不提前进行判断,可以直接按照自己想要的内容来读取buf的方式呢?答案就是ReplayingDecoder。
我们先来看一下上面的例子用ReplayingDecoder重写是什么情况:
public class IntegerHeaderFrameDecoder
extends ReplayingDecoder<Void> {
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception {
out.add(buf.readBytes(buf.readInt()));
}
}
使用ReplayingDecoder,我们可以忽略buf是否已经接收到了足够的可读数据,直接读取即可。
相比之下ReplayingDecoder非常的简单。接下来,我们来探究一下ReplayingDecoder的实现原理。
ReplayingDecoder的实现原理
ReplayingDecoder实际上是ByteToMessageDecoder的一个子类,它的定义如下:
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
在ByteToMessageDecoder中,最重要的方法是channelRead,在这个方法中实际调用了callDecode(ctx, cumulation, out);
来实现cumulation到out的解码过程。
ReplayingDecoder的秘密就在于对这个方法的重写,我们来看下这个方法的具体实现:
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
replayable.setCumulation(in);
try {
while (in.isReadable()) {
int oldReaderIndex = checkpoint = in.readerIndex();
int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
S oldState = state;
int oldInputLength = in.readableBytes();
try {
decodeRemovalReentryProtection(ctx, replayable, out);
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes() && oldState == state) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) + ".decode() must consume the inbound " +
"data or change its state if it did not decode anything.");
} else {
continue;
}
}
} catch (Signal replay) {
replay.expect(REPLAY);
if (ctx.isRemoved()) {
break;
}
// Return to the checkpoint (or oldPosition) and retry.
int checkpoint = this.checkpoint;
if (checkpoint >= 0) {
in.readerIndex(checkpoint);
} else {
}
break;
}
if (oldReaderIndex == in.readerIndex() && oldState == state) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) + ".decode() method must consume the inbound data " +
"or change its state if it decoded something.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
这里的实现和ByteToMessageDecoder不同的是ReplayingDecoder中定义了一个checkpoint,这个checkpint是在尝试进行数据解码之初设置的:
int oldReaderIndex = checkpoint = in.readerIndex();
如果是在解码的过程中出现了异常,则使用checkpoint重置index:
int checkpoint = this.checkpoint;
if (checkpoint >= 0) {
in.readerIndex(checkpoint);
} else {
}
这里捕获的异常是Signal,Signal是什么呢?
Signal是一个Error对象:
public final class Signal extends Error implements Constant<Signal>
这个异常是从replayable中抛出来的。
replayable是一个特有的ByteBuf对象,叫做ReplayingDecoderByteBuf:
final class ReplayingDecoderByteBuf extends ByteBuf
在ReplayingDecoderByteBuf中定义了Signal属性:
private static final Signal REPLAY = ReplayingDecoder.REPLAY;
这个Signal异常是从ReplayingDecoderByteBuf中的get方法中抛出的,这里以getInt为例,看一下异常是如何抛出的:
public int getInt(int index) {
checkIndex(index, 4);
return buffer.getInt(index);
}
getInt方法首先会去调用checkIndex方法进行buff中的长度检测,如果小于要读取的长度,则会抛出异常REPLAY:
private void checkIndex(int index, int length) {
if (index + length > buffer.writerIndex()) {
throw REPLAY;
}
}
这就是Signal异常的由来。
总结
以上就是对ReplayingDecoder的介绍,虽然ReplayingDecoder好用,但是从它的实现可以看出,ReplayingDecoder是通过抛出异常来不断的重试,所以在某些特殊的情况下会造成性能的下降。
也就是说在减少我们代码量的同时,降低了程序的执行效率。看来要想马儿跑又想马儿不吃草,这样的好事是不可能的了。
本文已收录于 http://www.flydean.com/14-4-netty-replayingdecoder/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
netty系列之:netty中的自动解码器ReplayingDecoder的更多相关文章
- Netty 系列之 Netty 高性能之道 高性能的三个主题 Netty使得开发者能够轻松地接受大量打开的套接字 Java 序列化
Netty系列之Netty高性能之道 https://www.infoq.cn/article/netty-high-performance 李林锋 2014 年 5 月 29 日 话题:性能调优语言 ...
- 【读后感】Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ?
[读后感]Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ? 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商 ...
- netty系列之:netty中的核心解码器json
目录 简介 java中对json的支持 netty对json的解码 总结 简介 程序和程序之间的数据传输方式有很多,可以通过二进制协议来传输,比较流行的像是thrift协议或者google的proto ...
- netty系列之:netty实现http2中的流控制
目录 简介 http2中的流控制 netty对http2流控制的封装 Http2FlowController Http2LocalFlowController Http2RemoteFlowContr ...
- netty系列之:netty中各不同种类的channel详解
目录 简介 ServerChannel和它的类型 Epoll和Kqueue AbstractServerChannel ServerSocketChannel ServerDomainSocketCh ...
- 【转】Netty系列之Netty编解码框架分析
http://www.infoq.com/cn/articles/netty-codec-framework-analyse/ 1. 背景 1.1. 编解码技术 通常我们也习惯将编码(Encode)称 ...
- Netty系列之Netty编解码框架分析
1. 背景 1.1. 编解码技术 通常我们也习惯将编码(Encode)称为序列化(serialization),它将对象序列化为字节数组,用于网络传输.数据持久化或者其它用途. 反之,解码(Decod ...
- Netty 系列之 Netty 高性能之道
1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用 Netty4 + Thrift 压缩二进制编解码技术,他们实现了 10 W TPS(1 K 的复杂 POJO 对象)的跨 ...
- Netty系列之Netty高性能之道
转载自http://www.infoq.com/cn/articles/netty-high-performance 1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Ne ...
随机推荐
- String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有?
没有.因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象.在这段代码中,s原先指向一个String对象,内容是 "Hello",然后我们对s进行 ...
- springBoot 多配置文件切换之profile
说明: 我们平时工作,有开发环境,和生产环境,利用springboot的多profile配置,可以很轻松切换配置. 实现方式1(推荐): 配置文件命名遵循:application-{开发模式}.pro ...
- pyinstaller打包Django项目
系统:ubuntu18.04 / Centos 7自带Python3.61.安装pip3 apt-get install -y python3-pip pip3 install --u ...
- 简述在 MySQL 数据库中 MyISAM 和 InnoDB 的区别 ?
MyISAM: 不支持事务,但是每次查询都是原子的: 支持表级锁,即每次操作是对整个表加锁: 存储表的总行数: 一个 MYISAM 表有三个文件:索引文件.表结构文件.数据文件: 采用菲聚集索引,索引 ...
- jsp技术之隐藏域
隐藏域 hidden:隐藏域属性,不显示到页面上,但是会提交的表单项 注意:表单中增加了一个隐藏域,是用户的id.稍后修改联系人信息,提交表单时需要使用到 <!-- hidden:隐藏域,不显示 ...
- linux命令手册
常⻅系统服务命令 常用命令 作用 chkconfig --list 列出系统服务 service <服务名> status 查看某个服务 service <服务名> start ...
- Simulink仿真时间、步长、精度和解法器设置
在Simulink模型中Configuration Parameters里的Solver页设置仿真时间.步长.精度和解法器. 一.仿真时间:注意这里的时间概念与真实的时间并不一样,只是计算机仿真中对时 ...
- Pycharm使用 Ctrl+滚轮 调整字体大小
首先,打开File中的Settings 然后,点开Editor内的General 最后,在3 指向的位置勾选:Change font size (Zoom)with Ctrl+Mouse Whel 这 ...
- ES6-11学习笔记--Promise
Promise是ES6异步编程解决方案之一,简化以前ajax的嵌套地狱,增加代码可读性. 基本用法: resolve,成功 reject,失败 let p = new Promise((resol ...
- linux系统引导过程
linux系统引导过程 linux-0.11引导时,将依次运行BIOS程序.bootsect.s.setup.s和head.s,完成引导过程后进入到main函数运行.BIOS完成硬件的检查与初始化等工 ...