怎么使用Netty解码自定义通信协议

网络协议的基本要素

一个完备的网络协议需要具备哪些基本要素

  1. 魔数:魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据
  2. 协议版本号:随着业务需求的变化,协议可能需要对结构或字段进行改动,不同版本的协议对应的解析方法也是不同的。所以在生产级项目中强烈建议预留协议版本号这个字段。
  3. 序列化算法:表示数据发送方应该采用何种方法将请求的对象转化为二进制,以及如何再将二进制转化为对象
  4. 报文类型:报文可能存在不同的类型。例如在 RPC 框架中有请求、响应、心跳等类型的报文,在 IM 即时通信的场景中有登陆、创建群聊、发送消息、接收消息、退出群聊等类型的报文。
  5. 长度域字段:代表请求数据的长度,接收方根据长度域字段获取一个完整的报文。
  6. 请求数据:通常为序列化之后得到的二进制流
  7. 状态:状态字段用于标识请求是否正常。一般由被调用方设置。例如一次 RPC 调用失败,状态字段可被服务提供方设置为异常状态。
  8. 保留字段:保留字段是可选项,为了应对协议升级的可能性,可以预留若干字节的保留字段,以备不时之需。
+---------------------------------------------------------------+
​
| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte  |
​
+---------------------------------------------------------------+
​
| 状态 1byte |        保留字段 4byte     |      数据长度 4byte     | 
​
+---------------------------------------------------------------+
​
|                   数据内容 (长度不定)                          |
​
+---------------------------------------------------------------+

举例如下: image.png

如何实现自定义通信协议

Netty 作为一个非常优秀的网络通信框架,已经为我们提供了非常丰富的编解码抽象基类,帮助我们更方便地基于这些抽象基类扩展实现自定义协议。 Netty 常用编码器类型:

  • MessageToByteEncoder 对象编码成字节流;

  • MessageToMessageEncoder 一种消息类型编码成另外一种消息类型。

Netty 常用解码器类型:

  • ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象;

  • MessageToMessageDecoder 将一种消息类型解码为另外一种消息类型。

编解码器可以分为一次解码器和二次解码器,一次解码器用于解决 TCP 拆包/粘包问题,按协议解析后得到的字节数据。如果你需要对解析后的字节数据做对象模型的转换,这时候便需要用到二次解码器,同理编码器的过程是反过来的。 一次编解码器:MessageToByteEncoder/ByteToMessageDecoder。 二次编解码器:MessageToMessageEncoder/MessageToMessageDecoder。

抽象编码类

ChannelOutboundHandler.png 通过抽象编码类的继承图可以看出,编码类是 ChanneOutboundHandler 的抽象类实现,具体操作的是 Outbound 出站数据。

MessageToByteEncoder

MessageToByteEncoder 用于将对象编码成字节流,MessageToByteEncoder 提供了唯一的 encode 抽象方法,我们只需要实现encode 方法即可完成自定义编码。 编码器实现非常简单,不需要关注拆包/粘包问题。如下例子,展示了如何将字符串类型的数据写入到 ByteBuf 实例,ByteBuf 实例将传递给 ChannelPipeline 链表中的下一个 ChannelOutboundHandler。

public class StringToByteEncoder extends MessageToByteEncoder<String> {@Overrideprotected void encode(ChannelHandlerContext channelHandlerContext, String data, ByteBuf byteBuf) throws Exception {byteBuf.writeBytes(data.getBytes());}
}

encode什么时候被调用的

MessageToByteEncoder 重写了 ChanneOutboundHandler 的 write() 方法,其主要逻辑分为以下几个步骤:

  1. acceptOutboundMessage 判断是否有匹配的消息类型,如果匹配需要执行编码流程,如果不匹配直接继续传递给下一个 ChannelOutboundHandler;

  2. 分配 ByteBuf 资源,默认使用堆外内存;

  3. 调用子类实现的 encode 方法完成数据编码,一旦消息被成功编码,会通过调用 ReferenceCountUtil.release(cast) 自动释放;

  4. 如果 ByteBuf 可读,说明已经成功编码得到数据,然后写入 ChannelHandlerContext 交到下一个节点;如果 ByteBuf 不可读,则释放 ByteBuf 资源,向下传递空的 ByteBuf 对象。

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {ByteBuf buf = null;try {if (acceptOutboundMessage(msg)) { // 1. 消息类型是否匹配@SuppressWarnings("unchecked")I cast = (I) msg;buf = allocateBuffer(ctx, cast, preferDirect); // 2. 分配 ByteBuf 资源try {encode(ctx, cast, buf); // 3. 执行 encode 方法完成数据编码} finally {ReferenceCountUtil.release(cast);}if (buf.isReadable()) {ctx.write(buf, promise); // 4. 向后传递写事件} else {buf.release();ctx.write(Unpooled.EMPTY_BUFFER, promise);}buf = null;} else {ctx.write(msg, promise);}} catch (EncoderException e) {throw e;} catch (Throwable e) {throw new EncoderException(e);} finally {if (buf != null) {buf.release();}}
}

MessageToMessageEncoder

MessageToMessageEncoder 与 MessageToByteEncoder 类似,同样只需要实现 encode 方法。

MessageToMessageEncoder常用的实现子类StringEncoderLineEncoderBase64Encoder等。

StringEncoder为例看下MessageToMessageEncoder 的用法。

源码示例如下:将 CharSequence 类型(String、StringBuilder、StringBuffer 等)转换成 ByteBuf 类型,结合 StringDecoder 可以直接实现 String 类型数据的编解码。

@Override
protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {if (msg.length() == 0) {return;}out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset));
}

抽象解码类

解码类是 ChanneInboundHandler 的抽象类实现,操作的是 Inbound 入站数据。解码器实现的难度要远大于编码器,因为解码器需要考虑拆包/粘包问题。

由于接收方有可能没有接收到完整的消息,所以解码框架需要对入站的数据做缓冲操作,直至获取到完整的消息。 ChannelOutboundHandler.png

ByteToMessageDecoder

使用 ByteToMessageDecoder,Netty 会自动进行内存的释放,我们不用操心太多的内存管理方面的逻辑。 首先,我们看下 ByteToMessageDecoder 定义的抽象方法:

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {if (in.isReadable()) {decodeRemovalReentryProtection(ctx, in, out);}}
}

我们只需要实现一下decode()方法,这里的 in 大家可以看到,传递进来的时候就已经是 ByteBuf 类型,所以我们不再需要强转,第三个参数是List类型,我们通过往这个List里面添加解码后的结果对象,就可以自动实现结果往下一个 handler 进行传递,这样,我们就实现了解码的逻辑 handler。 d966e7714f17f19cf3606ddae7a0b6ed.png

为什么存取解码后的数据是用List

由于 TCP 粘包问题,ByteBuf 中可能包含多个有效的报文,或者不够一个完整的报文。

Netty 会重复回调 decode() 方法,直到没有解码出新的完整报文可以添加到 List 当中,或者 ByteBuf 没有更多可读取的数据为止。

如果此时 List 的内容不为空,那么会传递给 ChannelPipeline 中的下一个ChannelInboundHandler。

static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {for (int i = 0; i < numElements; i ++) {//循环传播  有多少调用多少ctx.fireChannelRead(msgs.getUnsafe(i));}
}

decodeLast

ByteToMessageDecoder 还定义了 decodeLast() 方法。为什么抽象解码器要比编码器多一个 decodeLast() 方法呢?

因为 decodeLast 在 Channel 关闭后会被调用一次,主要用于处理 ByteBuf 最后剩余的字节数据。Netty 中 decodeLast 的默认实现只是简单调用了 decode() 方法。如果有特殊的业务需求,则可以通过重写 decodeLast() 方法扩展自定义逻辑。

ReplayingDecoder

ByteToMessageDecoder 还有一个抽象子类是 ReplayingDecoder。它封装了缓冲区的管理,在读取缓冲区数据时,你无须再对字节长度进行检查。因为如果没有足够长度的字节数据,ReplayingDecoder 将终止解码操作。ReplayingDecoder 的性能相比直接使用 ByteToMessageDecoder 要慢,大部分情况下并不推荐使用 ReplayingDecoder。

MessageToMessageDecoder

与 ByteToMessageDecoder 不同的是 MessageToMessageDecoder 并不会对数据报文进行缓存,它主要用作转换消息模型。 比较推荐的做法是使用 ByteToMessageDecoder 解析 TCP 协议,解决拆包/粘包问题。解析得到有效的 ByteBuf 数据,然后传递给后续的 MessageToMessageDecoder 做数据对象的转换,具体流程如下图所示: image.png 案例如下:

public class MyTcpDecoder extends ByteToMessageDecoder {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {// 检查ByteBuf数据是否完整if (in.readableBytes() < 4) {return;}// 标记ByteBuf读取索引位置in.markReaderIndex();// 读取数据包长度int length = in.readInt();// 如果ByteBuf中可读字节数不足一个数据包长度,则将读取索引位置恢复到标记位置,等待下一次读取if (in.readableBytes() < length) {in.resetReaderIndex();return;}// 读取数据ByteBuf data = in.readBytes(length);// 将数据传递给下一个解码器进行转换,转换后的数据对象添加到out中ctx.fireChannelRead(data);}
}public class MyDataDecoder extends MessageToMessageDecoder<ByteBuf> {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {// 将读取到的ByteBuf数据转换为自定义的数据对象MyData data = decode(msg);if (data != null) {// 将转换后的数据对象添加到out中,表示解码成功out.add(data);}}private MyData decode(ByteBuf buf) {// 实现自定义的数据转换逻辑// ...return myData;}
}

实战案例

如何判断 ByteBuf 是否存在完整的报文? 最常用的做法就是通过读取消息长度 dataLength 进行判断。如果 ByteBuf 的可读数据长度小于 dataLength,说明 ByteBuf 还不够获取一个完整的报文。在该协议前面的消息头部分包含了魔数、协议版本号、数据长度等固定字段,共 14 个字节。 固定字段长度和数据长度可以作为我们判断消息完整性的依据,具体编码器实现ByteToMessageDecoder逻辑示例如下:

/*
+---------------------------------------------------------------+
| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte  |
+---------------------------------------------------------------+
| 状态 1byte |        保留字段 4byte     |      数据长度 4byte     | 
+---------------------------------------------------------------+
|                   数据内容 (长度不定)                          |
+---------------------------------------------------------------+*/
@Override
public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {// 判断 ByteBuf 可读取字节if (in.readableBytes() < 14) { return;}// 标记 ByteBuf 读指针位置in.markReaderIndex();// 跳过魔数in.skipBytes(2);// 跳过协议版本号in.skipBytes(1);byte serializeType = in.readByte();// 跳过报文类型in.skipBytes(1);// 跳过状态字段in.skipBytes(1);// 跳过保留字段in.skipBytes(4);// 验证报文长度,不对的话就重置指针位置int dataLength = in.readInt();if (in.readableBytes() < dataLength) {in.resetReaderIndex(); // 重置 ByteBuf 读指针位置,这一步很重要return;}byte[] data = new byte[dataLength];in.readBytes(data);// 方式一:在解码器中就将数据解码成具体的对象SerializeService serializeService = getSerializeServiceByType(serializeType);Object obj = serializeService.deserialize(data);if (obj != null) {out.add(obj);}// 方式二:这一步可以不在解码器中处理,将请求数据读取到一个新的byteBuf然后丢给handler处理// 创建新的 ByteBuf 对象来存储有效负载数据ByteBuf payload = Unpooled.buffer((int) dataSize);// 读取有效负载数据并写入到 payload 中in.readBytes(payload);if (payload.isReadable()) {out.add(payload);}
}

扩展

什么是字节序

字节顺序,是指数据在内存中的存放顺序 使用16进制表示:0x12345678。在内存中有两种方法存储这个数字, 不同在于,对于某一个要表示的值,是把值的低位存到低地址,还是把值的高位存到低地址。

字节顺序分类

字节的排列方式有两种。例如,将一个多字节对象的低位放在较小的地址处,高位放在较大的地址处,则称小端序;反之则称大端序。 典型的情况是整数在内存中的存放方式(小端/主机字节序)和网络传输的传输顺序(大端/网络字节序)

1. 网络字节序(Network Order):TCP/IP各层协议将字节序定义为大端(Big Endian) ,因此TCP/IP协议中使用的字节序通常称之为网络字节序。

  • 所以当两台主机之间要通过TCP/IP协议进行通信的时候就需要调用相应的函数进行主机序列(Little Endian)和网络序(Big Endian)的转换。这样一来,也就达到了与CPU、操作系统无关,实现了网络通信的标准化。

2. 主机字节序(Host Order): 整数在内存中保存的顺序,它遵循小端(Little Endian)规则(不一定,要看主机的CPU架构,不过大多数都是小端)。

  • 同型号计算机上写的程序,在相同的系统上面运行是没有问题的。

结论

Java中虚拟机屏蔽了大小端问题,如果是Java之间通信则无需考虑,只有在跨语言通信的场景下才需要处理大小端问题。

回到本文的重点,我们在编解码时也要注意大小端的问题,一般来说如果是小端序的话,我们用Netty取值的时候都要用LE结尾的方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/779.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV(图像处理)-图片搜索

图片搜索 1.知识介绍2.实现流程2.1 计算特征点与描述子2.2 描述子的匹配2.3 求出单应性矩阵并画出轮廓2.4 将特征点标出 此篇博客作者仍在探索阶段&#xff0c;还有一些模糊的概念没有弄懂&#xff0c;请读者自行分辨。 1.知识介绍 Opencv进行图片搜索需要的知识有&#xff1…

Leetcode-每日一题【147.对链表进行插入排序】

题目 给定单个链表的头 head &#xff0c;使用 插入排序 对链表进行排序&#xff0c;并返回 排序后链表的头 。 插入排序 算法的步骤: 插入排序是迭代的&#xff0c;每次只移动一个元素&#xff0c;直到所有元素可以形成一个有序的输出列表。 每次迭代中&#xff0c;插入排序…

青岛大学_王卓老师【数据结构与算法】Week05_09_顺序栈的操作3_学习笔记

本文是个人学习笔记&#xff0c;素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享&#xff0c; 另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权&#xff0c;请留言作删文处理。 课程视频链接&#xff1a; 数据结构与算法基础…

ELK搭建

ELK介绍&#xff1a; ELK是一组开源工具的缩写&#xff0c;它由Elasticsearch、Logstash和Kibana三个组件组成&#xff0c;用于处理、分析和可视化大量日志数据。 入门级ELK搭建&#xff08;无Docker环境&#xff09; 安装前准备 1.获取安装包 https://artifacts.elastic…

Objective-C 父元素和子元素的点击事件

场景&#xff1a; &#xff08;需求1&#xff09;pageA一开始是【默认模式】&#xff0c;点击父元素view&#xff08;包括【搜索】文字&#xff09;&#xff0c;进入【搜索模式】&#xff1b; &#xff08;需求2&#xff09;在pageA中&#xff0c;点击【取消】文字时&#xff…

如何快速制作一个奶茶店小程序商城

如果你是一个奶茶店的老板&#xff0c;你可能会考虑开设一个小程序商城来增加销售渠道和提升品牌形象。那么&#xff0c;如何快速制作一个奶茶店小程序商城呢&#xff1f;下面我们将介绍一个简单的步骤供你参考。 首先&#xff0c;你需要登录乔拓云平台进入商城后台管理页面。在…

用ChatGPT解析Wireshark抓取的数据包样例

用Wireshark抓取的数据包&#xff0c;常用于网络故障排查、分析和应用程序通信协议开发。其抓取的分组数据结果为底层数据&#xff0c;看起来比较困难&#xff0c;现在通过chatGPT大模型&#xff0c;可以将原始抓包信息数据提交给AI进行解析&#xff0c;本文即是进行尝试的样例…

互联网行业真的不行了吗?

文章目录 前言一、起因二、互联网真的完了吗&#xff1f;三、是不是要转行&#xff1f;四、十年磨一剑五、统一回复 前言 英雄算法联盟 - 七月集训 已经开始 16 天&#xff0c;八月算法集训 将于 08月01日 正式开始&#xff0c;目前已经提前开始报名&#xff0c;报名方式参见&a…

OpenCv之滤波器

目录 一、卷积 二、方盒滤波与均值滤波 三、高斯滤波 四、中值滤波 五、双边滤波 一、卷积 图像卷积就是卷积核在图像上按行华东遍历像素时不断的相乘求和的过程 相关知识点: 步长:就是卷积核在图像上移动的步幅.(为充分扫描图片&#xff0c;步长一般为1)padding:指在图片…

matlab学习指南(1):matlab初步入门详细介绍

&#x1f305;*&#x1f539;** φ(゜▽゜*)♪ **&#x1f539;*&#x1f305; 欢迎来到馒头侠的博客&#xff0c;该类目主要讲数学建模的知识&#xff0c;大家一起学习&#xff0c;联系最后的横幅&#xff01; 喜欢的朋友可以关注下&#xff0c;私信下次更新不迷路&#xff0…

sqlite3交叉编译

1、交叉编译sqllite3可以先从官网下载最新最新的源码进行编译。sqlite3下载sqlite3有两种版本的源代码&#xff0c;sqlite-amalgamation-3420000.zip这种是将所有的操作放到sqlite3中进行使用的。虽然官方推荐使用这种方法。但是对于嵌入式移植还是使用sqlite-autoconf-3420000…

探索基于300W-LP的3D人脸关键点检测

目录 前言一、&#xff13;D 关键点可视化二、使用步骤1.300W-LP转为YOLO数据格式2.修改数据入口3.开始训练 总结 前言 300WLP数据集提供来丰富的人脸线索&#xff0c;包括&#xff12;D或&#xff13;D的关键点信息&#xff0c;Head Angle和&#xff13;DMM的参数等&#xff…

OpenCV中的RGB与YUV转换

1 基本概念 YUV 颜色空间从模拟电视时代开始就被广泛应用于彩色图像的转换与处理。其基于一个 3x3 的矩阵&#xff0c;通过线性变换将 RGB 像素转换为一个亮度&#xff08;Luma&#xff09;分量 Y 以及两个色度&#xff08;Chroma&#xff09;分量 U 和 V。由于模拟电视存在着多…

K8s 为什么要弃用 Docker

K8s 为什么要弃用 Docker 最近在学习容器技术的过程中&#xff0c;看到有关于Kubernetes“弃用 Docker”的事情&#xff0c;担心现在学 Docker 是否还有价值&#xff0c;是否现在就应该切换到 containerd 或者是其他 runtime。 随着深入了解&#xff0c;这些疑虑的确是有些道理…

git 工具使用--分支管理

git 工具使用–分支管理 文章目录 git 工具使用--分支管理理解分支创建分支切换分支合并分支删除分支合并冲突分支管理策略分支策略bug分支删除临时分支总结 理解分支 分支管理是Git的杀手级功能之一。分支&#xff1a;就是科幻中的平行宇宙&#xff0c;当你正在电脑面前学习C…

设计模式-单例模式

面向对象语言讲究的是万物皆对象。通常流程是先定义可实例化类&#xff0c;然后再通过各种不同的方式创建对象&#xff0c;因此类一般可以实例化出多个对象。但是实际项目开发时&#xff0c;我们还是希望保证项目运行时有且仅包含一个实例对象。这个需求场景的出发点包括但不限…

变压器试验介质损耗

试验目的 介质损耗因数 tanδ (% ) 是判断变压器绝缘状态的一种较有效的手段, 主要用来检 查变压器整体受潮、 油质劣化及严重的局部缺陷等, 但不一定能发现变压器局部受潮 等集中性局部缺陷。 试验设备 异频介质损耗测试仪 厂家&#xff1a; 湖北众拓高试 试验接线 (1) 介…

搜索引擎elasticsearch :安装elasticsearch (包含安装组件kibana、IK分词器、部署es集群)

文章目录 安装elasticsearch1.部署单点es1.1.创建网络1.2.加载镜像1.3.运行 2.部署kibana2.1.部署2.2.DevTools2.3 分词问题(中文不友好) 3.安装IK分词器3.1.在线安装ik插件&#xff08;较慢&#xff09;3.2.离线安装ik插件&#xff08;推荐&#xff09;1&#xff09;查看数据卷…

数据结构--哈夫曼树

数据结构–哈夫曼树 带权路径长度 结点的 权 \color{red}权 权:有某种现实含义的数值&#xff08;如:表示结点的重要性等) 结点的带权路径长度 \color{red}结点的带权路径长度 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数&#xff09;与该结点上权值的乘积 树的…

小程序MobX创建store并实现全局数据共享

查看小程序根目录中是否存在package.json文件 在项目根目录运行cmd 没有package.json文件输入npm init -y初始化一下,初始化一个包管理 安装MobX npm install --save mobx-miniprogram4.13.2 mobx-miniprogram-bindings1.2.1 小程序菜单栏工具–构建npm 根目录创建store文…