netty源码解解析(4.0)-19 ChannelHandler: codec--常用编解码实现
数据包编解码过程中主要的工作就是:在编码过程中进行序列化,在解码过程中从Byte流中分离出数据包然后反序列化。在MessageToByteEncoder中,已经解决了序列化之后的问题,ByteToMessageDecoder中已经部分第解决了从Byte流中分离出数据包的问题。实现具体的数据包编解码,只需要实现MessageToByteEncoder的encode和ByteToMessageDecoder的decode方法即可。
为了方便开发者使用Netty,在io.netty.handler.codec包中已经实现了一些开箱即用的编解码ChannelHandler,这些Handler包括:
- FixedLengthFrameDecoder : 固定长度的数据包解码。
- LengthFieldPrepender, LengthFieldBasedFrameDecoder: 带有长度字段的数据包编解码。
- LineBasedFrameDecoder: 以行字符串为一个数据包解码。
- StringEncoder, StringDecoder: 字符集转换器。
- Base64Encoder, Base64Decoder: Base64编码转换器。
- ProtobufEncoder, ProtobufDecoder: protoBuf序列化格式数据包的编解码。
- ZlibEncoder,ZlibDecoder: zlib压缩格式数据包的编解码。
- SnappyFramedEncoder,SnappyFramedDecoder: Snappy压缩格式数据包的编解码。
接下来让我们来分析几个关键ChannelHandler的实现代码。
带有长度字段的数据解码: LengthFieldBasedFrameDecoder
之所以先从LengthFieldBasedFrameDecoder开始,是因为这个类的实现非常典型,它向我们展示了解码二进制数据包的一些常用方法:
- 从Byte流中反序列化出一个整数。
- 利用整数表示一个数据包的长度。
- 使用偏移量得到数据包的开始位置。
- 使用数据包的开始位置和长度从Byte流中提取数据包。
这个类解码的数据包可能有三中格式:
| length | content | : 其中length是长度字段,它表示content的长度。但是这样格式无法明确地找到数据包的开始位置,需要一个表示开始位置的字段。
| header | length | conent | : 通过header字段确定数据包的开始位置。
| header | length | header1 | content |: header2是一个固定长度的字段,它可能包含一些子字段。
有一些必要的属性用来辅助解码数据包:
byteOrder: 反序列化整数使用的字节序。
lengthFieldOffset: 长度字段的偏移量,也是header的长度。
lengthFieldLength: 长度字段的长度。长度字段是一个整数,它的长度可能是: 1, 2, 3, 4, 8。
lengthAdjustment: header1的长度。
initialBytesToStrip: 从Byte流中提取数据包时去掉的数据长度。
通过覆盖decode方法从Byte流中提取数据包:
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
代码比较简单,调用内部的decode方法完成数据包的提取,干货都在这个内部方法中。
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (discardingTooLongFrame) {
discardingTooLongFrame(in);
}
if (in.readableBytes() < lengthFieldEndOffset) {
return null;
}
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);
if (frameLength < 0) {
failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
}
frameLength += lengthAdjustment + lengthFieldEndOffset;
if (frameLength < lengthFieldEndOffset) {
failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
}
if (frameLength > maxFrameLength) {
exceededFrameLength(in, frameLength);
return null;
}
// never overflows because it's less than maxFrameLength
int frameLengthInt = (int) frameLength;
if (in.readableBytes() < frameLengthInt) {
return null;
}
if (initialBytesToStrip > frameLengthInt) {
failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
}
in.skipBytes(initialBytesToStrip);
// extract frame
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);
return frame;
}
调用extractFrame(42行)从Byte流中提取数据包之前的关键环节有:
- 找到数据包的开始位置: 10行。
- 确定数据包的长度: 11行,调用getUnadjustedFrameLength方法反序列化到的length字段值。此时frameLength值是content的长度,还不是真正的包长度。17行,content长度加上header1的长度(lengthAjustment)再加上header和length的长度(lengthFieldEndOffset),得到的结果才是真正的包长度。 41行,最后减去需要丢掉的那部分数据的长度(initialBytesStrip),得到期望的数据包长度。
- 处理in中数据不足一个数据包的情况: 6-7行,30-31行,返回null。
- 清理掉in中超过最大数据包长度限制的数据: 23-24行如果in中的数据大于frameLength丢掉这个数据包,否则丢掉in中现有的所有数据,记录下还需要丢弃的数据长度,下次在 2-3行丢掉剩下长度的数据。
- 处理包长度错误的情况: 13-14行,19-20行丢掉header和length。34-35行丢掉frameLength长度的数据。
最后得到得到数据包的开始位置和长度之和从in缓冲区中提取出一个完整数据包,并修改in的读位置, 43-44行。
getUnadjustedFrameLength中反序列环length的值是计算数据包长度的关键,这个方法的实现如下:
protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
buf = buf.order(order);
long frameLength;
switch (length) {
case 1:
frameLength = buf.getUnsignedByte(offset);
break;
case 2:
frameLength = buf.getUnsignedShort(offset);
break;
case 3:
frameLength = buf.getUnsignedMedium(offset);
break;
case 4:
frameLength = buf.getUnsignedInt(offset);
break;
case 8:
frameLength = buf.getLong(offset);
break;
default:
throw new DecoderException(
"unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
}
return frameLength;
}
2行设置ByteBuf反序列化整数时使用的字节序,默认BIG_ENDIAN。这个值可以在调用构造方法时设置。
4-24行,根据length字段的不同长度,使用ByteBuf的不同方法反序列化length的值。length字段的长度只能是: 1,2,3,4,8之一。
以上是LengthFieldBasedFrameDecoder实现的核心代码的分析。这个类把一个比较关键的问题留给子类实现,就是如何处理header或header1字段的处理, header1可以没有,但一定要有header。他们内部可以有比较复杂的结构,而且长度是约定好的,不会变。这个就给子类留下了比较大的扩展空间。通过扩展这个类,我们可以实现任意格式数据包的提取功能。
带有长度字段的数据包编码: LengthFieldPrepender
这个类实现的功能和LengthFieldBasedFrameDecoder相反,也比它要简单很多,只是简单地在已经序列化好的数据前面加上长度字段。
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
int length = msg.readableBytes() + lengthAdjustment;
if (lengthIncludesLengthFieldLength) {
length += lengthFieldLength;
} if (length < 0) {
throw new IllegalArgumentException(
"Adjusted frame length (" + length + ") is less than zero");
} switch (lengthFieldLength) {
case 1:
if (length >= 256) {
throw new IllegalArgumentException(
"length does not fit into a byte: " + length);
}
out.writeByte((byte) length);
break;
case 2:
if (length >= 65536) {
throw new IllegalArgumentException(
"length does not fit into a short integer: " + length);
}
out.writeShort((short) length);
break;
case 3:
if (length >= 16777216) {
throw new IllegalArgumentException(
"length does not fit into a medium integer: " + length);
}
out.writeMedium(length);
break;
case 4:
out.writeInt(length);
break;
case 8:
out.writeLong(length);
break;
default:
throw new Error("should not reach here");
} out.writeBytes(msg, msg.readerIndex(), msg.readableBytes());
}
3-11行,计算并检查长度字段的值。
13-43行,在数据前面加上长度字段。
45行,把数据追加到长度长度字段之后。
行数据包和固定长度数据包的提取
LineBasedFrameDecoder: 用于从字符串流中按行提取数据包。用"\n"或"\r\n"作为前一个数据包的结束标志和下一个数据包的开始标志,也是根据这个标志算出数据包的长度。
FixedLengthFrameDecoder: 用于从Byte流中安固定长度提取数据包。数据包没有明确的开始标志或结束标志,只是简单地根据约定的长度提取数据包。
这个中数据包提取方式对数据源可靠性要求较高,实际应用中要多加小心。
字符集转换
StringEncoder, StringDecoder这两个类用于把字符串从一种字符集转换成另外一种字符集,不涉及数据包的提取。如:GBK和UTF-8之间的转换。
Base64编解码
Base64Encoder, Base64Decoder这个两个类用于base64的编解码,不涉及数据包的提取。
ProtoBuf编解码
ProtobufEncoder, ProtobufDecoder用ProtoBuf格式数据包的编解码,不涉及数据包的的提取。
压缩和解压缩
ZlibEncoder,ZlibDecoder: 对zlib和gzip压缩和解压缩算法的支持,不涉及数据包的提取。
SnappyFramedEncoder,SnappyFramedDecoder: 最Snappy压缩和解压算法的支持,不涉及数据包的提取。
本章小结
本章分析了分析了Netty使用编解码框架实现特定用途的编解码工具,其中最重要的是LengthFieldPrepender, LengthFieldBasedFrameDecoder类,这两个类展示了对任意二进制数据包打包,和从Byte流中提取二进制数据包的常用方法,理解了这两个类的实现,就能针对任意类型数据包实现自己的编解码Handler。其他的类比较简单,大部分不涉及数据包的提取,只涉及到一些常用的算法和编码格式,Netty在这里只是提供了一些开箱即用的工具。
前讲过LengthFieldBasedFrameDecoder把对header字段的处理留给子类,这意味着子类可以通过设计自己的header实现更加健壮的,安全的同时兼顾性能的数据包,关于这个话题将在下一章详细讲解。
netty源码解解析(4.0)-19 ChannelHandler: codec--常用编解码实现的更多相关文章
- netty源码解解析(4.0)-17 ChannelHandler: IdleStateHandler实现
io.netty.handler.timeout.IdleStateHandler功能是监测Channel上read, write或者这两者的空闲状态.当Channel超过了指定的空闲时间时,这个Ha ...
- netty源码解解析(4.0)-18 ChannelHandler: codec--编解码框架
编解码框架和一些常用的实现位于io.netty.handler.codec包中. 编解码框架包含两部分:Byte流和特定类型数据之间的编解码,也叫序列化和反序列化.不类型数据之间的转换. 下图是编解码 ...
- netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端
本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...
- netty源码解解析(4.0)-16 ChannelHandler概览
本章开始分析ChannelHandler实现代码.ChannelHandler是netty为开发者提供的实现定制业务的主要接口,开发者在使用netty时,最主要的工作就是实现自己的ChannelHan ...
- netty源码解解析(4.0)-10 ChannelPipleline的默认实现--事件传递及处理
事件触发.传递.处理是DefaultChannelPipleline实现的另一个核心能力.在前面在章节中粗略地讲过了事件的处理流程,本章将会详细地分析其中的所有关键细节.这些关键点包括: 事件触发接口 ...
- netty源码解解析(4.0)-11 Channel NIO实现-概览
结构设计 Channel的NIO实现位于io.netty.channel.nio包和io.netty.channel.socket.nio包中,其中io.netty.channel.nio是抽象实 ...
- netty源码解解析(4.0)-15 Channel NIO实现:写数据
写数据是NIO Channel实现的另一个比较复杂的功能.每一个channel都有一个outboundBuffer,这是一个输出缓冲区.当调用channel的write方法写数据时,这个数据被一系列C ...
- netty源码解解析(4.0)-14 Channel NIO实现:读取数据
本章分析Nio Channel的数据读取功能的实现. Channel读取数据需要Channel和ChannelHandler配合使用,netty设计数据读取功能包括三个要素:Channel, Eve ...
- netty源码解解析(4.0)-12 Channel NIO实现:channel初始化
创建一个channel实例,并把它register到eventLoopGroup中之后,这个channel然后处于inactive状态,仍然是不可用的.只有在bind或connect方法调用成功之后才 ...
随机推荐
- c++ 动态规划(数塔)
c++ 动态规划(dp) 题目描述 观察下面的数塔.写一个程序查找从最高点到底部任意位置结束的路径,使路径经过数字的和最大. 每一步可以从当前点走到左下角的点,也可以到达右下角的点. 输入 5 13 ...
- WPF依赖属性的正确学习方法
前言 我在学习WPF的早期,对依赖属性理解一直都非常的不到位,其恶果就是,我每次在写依赖属性的时候,需要翻过去的代码来复制黏贴. 相信很多朋友有着和我相同的经历,所以这篇文章希望能帮助到那些刚刚开始学 ...
- WPF音乐电台
最近一两年都没写过wpf相关的项目了,本来就不太熟的一些技巧全忘光啦,为了重新拾起这点东西,就花了几天时间做了个小demo,大致功能就是读取豆瓣电台api,获取歌单列表听歌.最开始是参考网上现有的例子 ...
- .net持续集成sonarqube篇之sonarqube基本操作(二)
系列目录 Activity界面操作 Activity界面主要是对多次构建管理界面,主要是帮助管理员快速了解项目每次构建与以往构建相比问题是增加了还是减少了等指标.由于目前我们仅进行了一次构建,因此没有 ...
- .NET领域驱动设计—初尝(一:疑问、模式、原则、工具、过程、框架、实践)
.NET领域驱动设计—初尝(一:疑问.模式.原则.工具.过程.框架.实践) 2013-04-07 17:35:27 标签:.NET DDD 驱动设计 原创作品,允许转载,转载时请务必以超链接形式标明 ...
- Java简单公式计算器
最近给公司开发业务代码时,碰到一个场景,简单描述是这样的: 客户要向咱们公司定制一件产品,这个产品呢,有很多属性,那公司得根据这些属性报价呀,怎么报价呢?公司针对某种类型的产品有一个基准价,在同类产品 ...
- 用maven工具管理web项目的错误记录:org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException
运行异常报告日志: 严重: Context initialization failedorg.springframework.beans.factory.xml.XmlBeanDefinitionSt ...
- Maven重新下载未下载完成的jar包
使用maven下载jar包,经常会遇到下载失败的情况,如果失败的jar包过多,或是不清楚到底有那些jar包在下载过程中出现了问题.可通过maven命令重新批量下载未成功的jar包. 1,打开cmd , ...
- Jmeter脚本录制--HTTP代理服务器
Jmeter脚本录制功能依赖第三方工具Badboy,所以在安装了Jmeter之后,还需要再安装一个工具. Badboy本身自带浏览器,相关操作只能在Badboy上进行操作,偶尔可能会遇到浏览器兼容的问 ...
- 在 Windows 上使用 Python 进行 web 开发
本文由葡萄城技术团队于原创并首发 转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 上一篇我们介绍了在Windows 10下进行初学者入门开发Python的指 ...