概念
在网络编程中,无论使用netty还是其它的socket通讯框架,都是通过TCP或UDP传输二进制流。 发送方把要发送的对象转化成二进制流发送出去。 接收方把接收到的二进制流转化为对象进行处理。 为了让接收方和发送方能对同一个二进制流有相同的认识,双方必须提前约定好一个协议。即对象如何转化为二进制流,二进制流如何转化为对象,这样通信双方才不会产生误解。
定义通信协议:
魔数:4字节,通信双方协商的一个暗号,般为固定值,本项目中使用0x88888888。一般我们的应用于某个端口对外开放,为了防止该端口被意外调用,我们可以在收到报文后,取前4个字节与魔数比对,如果不相同,则直接拒绝并关闭连接。 魔数的思想在很多场景中都有体现,如 Java Class 文件开头就存储了魔数 OxCAFEBABE,在 JVM 加载 Class 文件时首先就会验证魔数对的正确性。
协议版本号: 1字节,一般是预留字段,不同版本的协议对应的解析方法也是不同的。所以在生产级项目中强烈建议预留协议版本这个字段。
序列化算法:1字节,表示如何将java对象转化为二进制数据,以及如何反序列化(接收方将接收的二进制流转换成对象)如 JSON、 Hessian、Java 自带序列化等
指令:1字节,即报文类型,表示该消息的意图。最多支持256种指令。 PC 框架中有请求、响应、心跳类型。IM 通讯场景中有登陆、创建群聊、发送消息、接收消息、退出群聊等类型。
数据长度:4字节,即:长度域字段, 表示该字段后数据部分的长度。长度域字段代表请求数据的长度,可以定义整个报文的长度,也可以是请求数据部分的长度。
数据:即:请求数据,具体数据的内容。每种指令对应的数据是不同的。通常为的业务对象信息序列化后的二进制流,是整个报文的主体。
状态
状态字段用于标识请求是否正常,一般由被调用方设置。例如一次 RPC 调用失败,状态字段可被服务提供方设置为异常状态。
校验字段
校验字段 存放某种校验算法计算报文校验码,校验码用于验证报文的正确性。
保留字段
保留字段是可选项,为了应对协议升级的可能性,可以预留若干字节的保留字段,以备不时之需。
协议结构示例
/*
+---------------------------------------------------------------+
| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte |
+---------------------------------------------------------------+
| 状态 1byte | 保留字段 4byte | 数据长度 4byte |
+---------------------------------------------------------------+
| 数据内容 (长度不定) | 校验字段 2byte |
+---------------------------------------------------------------+
*/
通信协议
所谓的协议,就是通信双方事先商量好的接口“暗语”, 在 TCP 网络编程中,发送方和接收方的数据包格式都是二进制,
发送方将对象转化成二进制流发送给接收方,接收方获得二进制数据后需要知道如何解析对象,所以协议是双方能够正常通行的基础。
市面上通用的协议,例如 HTTP、 HTTPS、JSON-RPC、FTP、IMAP、Protobuf等。 通用协议兼容性好,易于维护,各种异构系统间可以实现无缝对接等。如果满足业务场景及性能需求的前提下,推荐采用通用协议的方案。采用http、websocket等公有协议通信,netty提供了许多类可以实现步骤序列化算法,指令设计, 编码解码,无需我们编码实现,只需调用相应的类和方法即可。
自定义协议 优点
- 极致性能:通用协议考虑很多兼容性的因素,必然在性能有所损失。
- 扩展性:自定义的协议相比通用协议更好扩展,可以更好地满足自己的业务需求。
- 安全性:通用协议是公开的,可能存在很多漏洞。自定义协议通常是私有的,黑客需要先破解协议内容,才能攻破漏洞。
应用场景
物联网IOC :万物互联
红外测温 (RS-485)
www.renrendoc.com/paper/12995…
Netty——自定义协议通信
Netty 提供了非常丰富的编解码抽象基类来实现自定义协议。
编码解码分类:
分层解码分类:
一次解码:一次解码用于解决 TCP 拆包/粘包问题,按协议解析得到的字节数据。常用一次编解码器:MessageToByteEncoder / ByteToMessageDecoder。
二次解码:对一次解析后的字节数据做对象模型的转换,这时候需要二次解码器,同理编码器的过程是反过来的。常用二次编解码器:MessageToMessageEncoder / MessageToMessageDecoder
抽象编码类
通过抽象编码类的继承图可以看出,编码类是 ChanneOutboundHandler 的抽象类实现,具体操作的是 Outbound 出站数据。
自定义报文,协议头部包含了魔数、协议版本号、数据长度等固定字段。 ByteBuf 是否完整,需要通过消息长度 dataLength 字段来判断。
自定义编码器需要重写 ByteToMessageDecoder 的 encode 方法,具体代码如下所示:
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List\ list) throws Exception {
byteBuf.markReaderIndex();
int dataLength = byteBuf.readInt();
if (byteBuf.readableBytes() < dataLength) {
// 未校验最大长度,存在安全隐患
byteBuf.resetReaderIndex();
return;
}
int rpcMagicVal = byteBuf.readShort();
if (rpcMagicVal != Rpc.MAGIC_VALUE) {
// 此处直接close或抛异常后close将导致decode()被重复调用
ctx.close();
}
// DO ACTUAL CODEC
}
@Override
public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
// 判断 ByteBuf 可读取字节
if (in.readableBytes() < 14) {
return;
}
in.markReaderIndex(); // 标记 ByteBuf 读指针位置
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);
}
}