李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
05.Netty进阶之自定义协议
Leefs
2022-06-16 PM
1610℃
0条
[TOC] ### 一、网络协议基本要素 ##### 魔数 + 魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。 + 魔数的作用是**防止任何人随便向服务器的端口上发送数据**。 + 服务端在接收到数据时会解析出前几个固定字节的魔数,然后做正确性比对。如果和约定的魔数不匹配,则认为是非法数据,可以直接关闭连接或者采取其他措施以增强系统的安全防护。 + 魔数的思想在压缩算法、Java Class 文件等场景中都有所体现,例如 Class 文件开头就存储了魔数 `0xCAFEBABE`,在加载 Class 文件时首先会验证魔数的正确性。 ##### 协议版本号 + 随着业务需求的变化,协议可能需要对结构或字段进行改动,不同版本的协议对应的解析方法也是不同的。所以在生产级项目中强烈建议预留**协议版本号**这个字段。 ##### 序列化算法 + 序列化算法字段表示数据发送方应该采用何种方法将请求的对象转化为二进制,以及如何再将二进制转化为对象,如 JSON、Hessian、Java 自带序列化等。 ##### 报文类型 + 在不同的业务场景中,报文可能存在不同的类型。 + 例如在 RPC 框架中有请求、响应、心跳等类型的报文,在 IM 即时通信的场景中有登陆、创建群聊、发送消息、接收消息、退出群聊等类型的报文。 ##### 长度域字段 + 长度域字段代表**请求数据**的长度,接收方根据长度域字段获取一个完整的报文。 ##### 请求数据 + 请求数据通常为序列化之后得到的**二进制流**,每种请求数据的内容是不一样的。 ##### 状态 + 状态字段用于标识**请求是否正常**。一般由被调用方设置。 + 例如一次 RPC 调用失败,状态字段可被服务提供方设置为异常状态。 ##### 保留字段 + 保留字段是可选项,为了应对协议升级的可能性,可以预留若干字节的保留字段,以备不时之需。 通过以上协议基本要素的学习,可以得到一个较为通用的协议示例: ``` +---------------------------------------------------------------+ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | +---------------------------------------------------------------+ | 状态 1byte | 保留字段 4byte | 数据长度 4byte | +---------------------------------------------------------------+ | 数据内容 (长度不定) | +---------------------------------------------------------------+ ``` ### 二、Netty编解码器分类 Netty 作为一个非常优秀的网络通信框架,提供了非常丰富的编解码抽象基类来实现自定义协议。 #### 2.1 编/解码分类 ![05.Netty进阶之自定义协议01.png](https://lilinchao.com/usr/uploads/2022/06/1519841982.png) #### 2.2 分层解码分类 + **一次解码**:一次解码用于解决TCP拆包/粘包问题,按协议解析得到的字节数据。常用一次编解码器:`MessageToByteEncoder / ByteToMessageDecoder`。 + **二次解码**:对一次解析后的字节数据做对象模型的转换,这时候需要二次解码器,同理编码器的过程是反过来的。常用二次编解码器:`MessageToMessageEncoder / MessageToMessageDecoder`。 #### 2.3 抽象编码类 ![05.Netty进阶之自定义协议02.png](https://lilinchao.com/usr/uploads/2022/06/1780591062.png) 通过抽象编码类的继承图可以看出,编码类是 `ChanneOutboundHandler` 的抽象类实现,具体操作的是 Outbound 出站数据。 #### 2.4 抽象解码类 ![05.Netty进阶之自定义协议03.png](https://lilinchao.com/usr/uploads/2022/06/3398141879.png) 解码类是 `ChanneInboundHandler` 的抽象类实现,操作的是 Inbound 入站数据。解码器的主要难度在于拆包和粘包问题,由于接收方可能没有接受到完整的消息,所以编码框架还要对入站数据做缓冲处理,直到获取到完整的消息。 ### 三、示例 + **编解码器** 根据上面的要素,设计一个登录请求消息和登录响应消息,并使用 Netty 完成收发 ```java import com.lilinchao.netty.message.Message; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageCodec; import lombok.extern.slf4j.Slf4j; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.List; /** * @author lilinchao * @date 2022/6/16 * @description 自定义编解码器 **/ @Slf4j public class MessageCodec extends ByteToMessageCodec
{ @Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { //1. 4字节的魔数 out.writeBytes(new byte[]{1,2,3,4}); //2. 1字节的版本 out.writeByte(1); //3. 1字节的序列化方式 jdk 0,json 1 out.writeByte(0); //4. 1字节的指令类型 out.writeByte(msg.getMessageType()); //5. 4个字节 out.writeInt(msg.getSequenceId()); // 无意义,对齐填充 out.writeByte(0xff); //6. 获取内容的字节数组 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(msg); byte[] bytes = bos.toByteArray(); //7. 长度 out.writeInt(bytes.length); //8. 写入内容 out.writeBytes(bytes); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
out) throws Exception { // 获取魔数 int magicNum = in.readInt(); // 获取版本号 byte version = in.readByte(); // 获得序列化方式 byte serializerType = in.readByte(); // 获得指令类型 byte messageType = in.readByte(); // 获得请求序号 int sequenceId = in.readInt(); // 移除补齐字节 in.readByte(); // 获得正文长度 int length = in.readInt(); // 获得正文 byte[] bytes = new byte[length]; in.readBytes(bytes,0,length); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); Message message = (Message)ois.readObject(); log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length); log.debug("{}", message); // 将信息放入List中,传递给下一个handler out.add(message); } } ``` + **测试** ```java import com.lilinchao.netty.message.LoginRequestMessage; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.logging.LoggingHandler; /** * @author lilinchao * @date 2022/6/16 * @description 测试类 **/ public class MessageDemo { public static void main(String[] args) throws Exception { EmbeddedChannel channel = new EmbeddedChannel( new LoggingHandler(), // 添加解码器,避免粘包半包问题 new LengthFieldBasedFrameDecoder( 1024, 12, 4, 0, 0), new MessageCodec() ); // encode LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三"); // channel.writeOutbound(message); // decode // 测试编码与解码 ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); new MessageCodec().encode(null, message, buf); ByteBuf s1 = buf.slice(0, 100); ByteBuf s2 = buf.slice(100, buf.readableBytes() - 100); s1.retain(); // 引用计数 2 channel.writeInbound(s1); // release 1 channel.writeInbound(s2); } } ``` - 测试类中用到了`LengthFieldBasedFrameDecoder`,避免粘包半包问题 - 通过`MessageCodec`的encode方法将附加信息与正文写入到ByteBuf中,通过channel执行入站操作。入站时会调用decode方法进行解码。 **运行结果** ![05.Netty进阶之自定义协议04.png](https://lilinchao.com/usr/uploads/2022/06/3107498446.png) *附参考文章链接* [*《如何利用 Netty 实现自定义协议通信》*](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/07%20%20%E6%8E%A5%E5%A4%B4%E6%9A%97%E8%AF%AD%EF%BC%9A%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%20Netty%20%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE%E9%80%9A%E4%BF%A1%EF%BC%9F.md)
标签:
Netty
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2174.html
上一篇
04.Netty进阶之协议设计与解析
下一篇
06.Netty进阶之Sharable注解
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
设计模式
Typora
数据结构
Livy
随笔
二叉树
锁
BurpSuite
Elasticsearch
正则表达式
Spark
MySQL
Python
Spark Core
Redis
人工智能
递归
Flink
Sentinel
高并发
JavaSE
工具
机器学习
Eclipse
Scala
VUE
哈希表
JavaWeb
线程池
链表
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭