在日常的网络开发当中,协议解析都是必须的工作内容,Netty中虽然内置了基于长度、分隔符的编解码器,但在大部分场景中我们使用的都是自定义协议,所以Netty提供了  MessageToByteEncoder<I>  与  ByteToMessageDecoder  两个抽象类,通过继承重写其中的encode与decode方法实现私有协议的编解码。这篇文章我们就对Netty中的自定义编解码器进行实践与分析。

一、编解码器的使用

下面是MessageToByteEncoder与ByteToMessageDecoder使用的简单示例,其中不涉及具体的协议编解码。

创建一个sever端服务

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final CodecHandler codecHandler = new CodecHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//添加编解码handler
p.addLast(new MessagePacketDecoder(),new MessagePacketEncoder());
//添加自定义handler
p.addLast(codecHandler);
}
}); // Start the server.
ChannelFuture f = b.bind(PORT).sync();

继承MessageToByteEncoder并重写encode方法,实现编码功能

public class MessagePacketEncoder extends MessageToByteEncoder<byte[]> {

    @Override
protected void encode(ChannelHandlerContext ctx, byte[] bytes, ByteBuf out) throws Exception {
//进行具体的编码处理 这里对字节数组进行打印
System.out.println("编码器收到数据:"+BytesUtils.toHexString(bytes));
//写入并传送数据
out.writeBytes(bytes);
}
}

继承ByteToMessageDecoder 并重写decode方法,实现解码功能

public class MessagePacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out){
try {
if (buffer.readableBytes() > 0) {
// 待处理的消息包
byte[] bytesReady = new byte[buffer.readableBytes()];
buffer.readBytes(bytesReady);
//进行具体的解码处理
System.out.println("解码器收到数据:"+ByteUtils.toHexString(bytesReady));
//这里不做过多处理直接把收到的消息放入链表中,并向后传递
out.add(bytesReady); }
}catch(Exception ex) { } } }

实现自定义的消息处理handler,到这里其实你拿到的已经是编解码后的数据

public class CodecHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("CodecHandler收到数据:"+ByteUtils.toHexString((byte[])msg));
byte[] sendBytes = new byte[] {0x7E,0x01,0x02,0x7e};
ctx.write(sendBytes);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}

运行一个客户端模拟发送字节0x01,0x02,看一下输出的执行结果

解码器收到数据:0102
CodecHandler收到数据:0102
编码器收到数据:7E01027E

根据输出的结果可以看到消息的入站与出站会按照pipeline中自定义的顺序传递,同时通过重写encode与decode方法实现我们需要的具体协议编解码操作。

二、源码分析

通过上面的例子可以看到MessageToByteEncoder<I>与ByteToMessageDecoder分别继承了ChannelInboundHandlerAdapter与ChannelOutboundHandlerAdapter,所以它们也是channelHandler的具体实现,并在创建sever时被添加到pipeline中, 同时为了方便我们使用,netty在这两个抽象类中内置与封装了一些其操作;消息的出站和入站会分别触发write与channelRead事件方法,所以上面例子中我们重写的encode与decode方法,也都是在父类的write与channelRead方法中被调用,下面我们就别从这两个方法入手,对整个编解码的流程进行梳理与分析。

1、MessageToByteEncoder

编码需要操作的是出站数据,所以在MessageToByteEncoder的write方法中会调用我们重写的encode具体实现, 把我们内部定义的消息实体编码为最终要发送的字节流数据发送出去。

    @Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
if (acceptOutboundMessage(msg)) {//判断传入的msg与你定义的类型是否一致
@SuppressWarnings("unchecked")
I cast = (I) msg;//转为你定义的消息类型
buf = allocateBuffer(ctx, cast, preferDirect);//包装成一个ByteBuf
try {
encode(ctx, cast, buf);//传入声明的ByteBuf,执行具体编码操作
} finally {
/**
* 如果你定义的类型就是ByteBuf 这里可以帮助你释放资源,不需要在自己释放
* 如果你定义的消息类型中包含ByteBuf,这里是没有作用,需要你自己主动释放
*/
ReferenceCountUtil.release(cast);//释放你传入的资源
} //发送buf
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
//类型不一致的话,就直接发送不再执行encode方法,所以这里要注意如果你传递的消息与泛型类型不一致,其实是不会执行的
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
buf.release();//释放资源
}
}
}

MessageToByteEncoder的write方法要实现的功能还是比较简单的,就是把你传入的数据类型进行转换和发送;这里有两点需要注意:

  • 一般情况下,需要通过重写encode方法把定义的泛型类型转换为ByteBuf类型, write方法内部自动帮你执行传递或发送操作;
  • 代码中虽然有通过ReferenceCountUtil.release(cast)释放你定义的类型资源,但如果定义的消息类中包含ByteBuf对象,仍需要主动释放该对象资源;

2、ByteToMessageDecoder

从命名上就可以看出ByteToMessageDecoder解码器的作用是把字节流数据编码转换为我们需要的数据格式

作为入站事件,解码操作的入口自然是channelRead方法

 @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {//如果消息是bytebuff
CodecOutputList out = CodecOutputList.newInstance();//实例化一个链表
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
callDecode(ctx, cumulation, out);//开始解码
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
if (cumulation != null && !cumulation.isReadable()) {//不为空且没有可读数据,释放资源
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
} int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);//向下传递消息
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}

callDecode方法内部通过while循环的方式对ByteBuf数据进行解码,直到其中没有可读数据

    protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {//判断ByteBuf是还有可读数据
int outSize = out.size();//获取记录链表大小 if (outSize > 0) {//判断链表中是否已经有数据
fireChannelRead(ctx, out, outSize);//如果有数据继续向下传递
out.clear();//清空链表 // Check if this handler was removed before continuing with decoding.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See:
// - https://github.com/netty/netty/issues/4635
if (ctx.isRemoved()) {
break;
}
outSize = 0;
} int oldInputLength = in.readableBytes();
decodeRemovalReentryProtection(ctx, in, out);//开始调用decode方法 // Check if this handler was removed before continuing the loop.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See https://github.com/netty/netty/issues/1664
if (ctx.isRemoved()) {
break;
} //这里如果链表为空且bytebuf没有可读数据,就跳出循环
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {//有可读数据继续读取
continue;
}
} if (oldInputLength == in.readableBytes()) {//beytebuf没有读取,但却进行了解码
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
} if (isSingleDecode()) {//是否设置了每条入站数据只解码一次,默认false
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}

decodeRemovalReentryProtection方法内部会调用我们重写的decode解码实现

    final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
decodeState = STATE_CALLING_CHILD_DECODE;//标记状态
try {
decode(ctx, in, out);//调用我们重写的decode解码实现
} finally {
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;
if (removePending) {//这里判断标记,防止handlerRemoved事件与解码操作冲突
handlerRemoved(ctx);
}
}
}

channelRead方法中接受到数据经过一系列逻辑处理,最终会调用我们重写的decode方法实现具体的解码功能;在decode方法中我们只需要ByteBuf类型的数据解析为我们需要的数据格式直接放入 List<Object> out链表中即可,ByteToMessageDecoder会自动帮你向下传递消息。

三、总结

通过上面的讲解,我们可以对Netty中内置自定义编解码器MessageToByteEncoder与ByteToMessageDecoder有一定的了解,其实它们本质上是Netty封装的一组专门用于自定义编解码的channelHandler实现类。在实际开发当中基于这两个抽象类的实现非常具有实用性,所以在这里稍作分析, 其中如有不足与不正确的地方还望指出与海涵。

关注微信公众号,查看更多技术文章。

转载说明:未经授权不得转载,授权后务必注明来源(注明:来源于公众号:架构空间, 作者:大凡)

Netty源码分析之自定义编解码器的更多相关文章

  1. Netty源码分析第4章(pipeline)---->第2节: handler的添加

    Netty源码分析第四章: pipeline 第二节: Handler的添加 添加handler, 我们以用户代码为例进行剖析: .childHandler(new ChannelInitialize ...

  2. Netty源码分析第6章(解码器)---->第1节: ByteToMessageDecoder

    Netty源码分析第六章: 解码器 概述: 在我们上一个章节遗留过一个问题, 就是如果Server在读取客户端的数据的时候, 如果一次读取不完整, 就触发channelRead事件, 那么Netty是 ...

  3. netty源码分析之揭开reactor线程的面纱(二)

    如果你对netty的reactor线程不了解,建议先看下上一篇文章netty源码分析之揭开reactor线程的面纱(一),这里再把reactor中的三个步骤的图贴一下 reactor线程 我们已经了解 ...

  4. Netty源码分析(前言, 概述及目录)

    Netty源码分析(完整版) 前言 前段时间公司准备改造redis的客户端, 原生的客户端是阻塞式链接, 并且链接池初始化的链接数并不高, 高并发场景会有获取不到连接的尴尬, 所以考虑了用netty长 ...

  5. Netty源码分析第6章(解码器)---->第4节: 分隔符解码器

    Netty源码分析第六章: 解码器 第四节: 分隔符解码器 基于分隔符解码器DelimiterBasedFrameDecoder, 是按照指定分隔符进行解码的解码器, 通过分隔符, 可以将二进制流拆分 ...

  6. Netty源码分析 (三)----- 服务端启动源码分析

    本文接着前两篇文章来讲,主要讲服务端类剩下的部分,我们还是来先看看服务端的代码 /** * Created by chenhao on 2019/9/4. */ public final class ...

  7. Netty源码分析 (七)----- read过程 源码分析

    在上一篇文章中,我们分析了processSelectedKey这个方法中的accept过程,本文将分析一下work线程中的read过程. private static void processSele ...

  8. Netty源码分析之NioEventLoop(三)—NioEventLoop的执行

    前面两篇文章Netty源码分析之NioEventLoop(一)—NioEventLoop的创建与Netty源码分析之NioEventLoop(二)—NioEventLoop的启动中我们对NioEven ...

  9. Netty 源码分析——ChannelPipeline

    Netty 源码分析--ChannelPipeline 通过前面的两章我们分析了客户端和服务端的流程代码,其中在初始化 Channel 的时候一定会看到一个 ChannelPipeline.所以在 N ...

随机推荐

  1. 深入了解机器学习决策树模型——C4.5算法

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是机器学习专题的第22篇文章,我们继续决策树的话题. 上一篇文章当中介绍了一种最简单构造决策树的方法--ID3算法,也就是每次选择一个特 ...

  2. [Chrome插件开发]001.入门

    Chrome插件开发入门 Chrome扩展文件 Browser Actions(扩展图标) Page Actions(地址栏图标) popup弹出窗口 Background Pages后台页面 实战讲 ...

  3. 03 . Redis集群

    Redis集群方案 Redis Cluster 集群模式通常具有 高可用.可扩展性.分布式.容错等特性.Redis分布式方案一般有两种 客户端分区方案 客户端 就已经决定数据会被 存储到哪个 redi ...

  4. 百度云百度网盘VIP不限速破解版绿色版-实测可用

    百度云百度网盘不限速VIP破解版绿色版-下载地址:https://www.90pan.com/b1548999

  5. 【jQuery】全功能轮播图的实现(本文结尾也有javascript版)

    轮播图 图片自动切换(定时器): 鼠标悬停在图片上图片不切换(清除定时器) 鼠标悬停在按钮上时显示对应的图片(鼠标悬停事件) 鼠标悬停在图片上是现实左右箭头 点击左键切换到上一张图片,但图片为第一张时 ...

  6. Jmeter 样例 之 JDBC请求-操作MySql数据库

    准备: 1.MySql的驱动jar包:mysql-connector-java-5.1.28.jar, 2.jmeter安装目录中修改编码格式:\bin\jmeter.properties   :sa ...

  7. Java实现 LeetCode 565 数组嵌套(没有重复值的数组)

    565. 数组嵌套 索引从0开始长度为N的数组A,包含0到N - 1的所有整数.找到并返回最大的集合S,S[i] = {A[i], A[A[i]], A[A[A[i]]], - }且遵守以下的规则. ...

  8. Java实现 LeetCode 481 神奇字符串

    481. 神奇字符串 神奇的字符串 S 只包含 '1' 和 '2',并遵守以下规则: 字符串 S 是神奇的,因为串联字符 '1' 和 '2' 的连续出现次数会生成字符串 S 本身. 字符串 S 的前几 ...

  9. Java实现 蓝桥杯VIP 算法提高 最长公共子序列

    算法提高 最长公共子序列 时间限制:1.0s 内存限制:256.0MB 问题描述 给定两个字符串,寻找这两个字串之间的最长公共子序列. 输入格式 输入两行,分别包含一个字符串,仅含有小写字母. 输出格 ...

  10. 第二届蓝桥杯C++B组国(决)赛真题

    以下代码仅供参考,解答部分来自网友,对于正确性不能保证,如有错误欢迎评论 四方定理. 数论中有著名的四方定理:所有自然数至多只要用四个数的平方和就可以表示. 我们可以通过计算机验证其在有限范围的正确性 ...