Netty进阶之自定义协议
一、网络协议基本要素
魔数
- 魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。
- 魔数的作用是防止任何人随便向服务器的端口上发送数据。
- 服务端在接收到数据时会解析出前几个固定字节的魔数,然后做正确性比对。如果和约定的魔数不匹配,则认为是非法数据,可以直接关闭连接或者采取其他措施以增强系统的安全防护。
- 魔数的思想在压缩算法、Java Class 文件等场景中都有所体现,例如 Class 文件开头就存储了魔数
0xCAFEBABE
,在加载 Class 文件时首先会验证魔数的正确性。
协议版本号
- 随着业务需求的变化,协议可能需要对结构或字段进行改动,不同版本的协议对应的解析方法也是不同的。所以在生产级项目中强烈建议预留协议版本号这个字段。
序列化算法
- 序列化算法字段表示数据发送方应该采用何种方法将请求的对象转化为二进制,以及如何再将二进制转化为对象,如 JSON、Hessian、Java 自带序列化等。
报文类型
- 在不同的业务场景中,报文可能存在不同的类型。
- 例如在 RPC 框架中有请求、响应、心跳等类型的报文,在 IM 即时通信的场景中有登陆、创建群聊、发送消息、接收消息、退出群聊等类型的报文。
长度域字段
- 长度域字段代表请求数据的长度,接收方根据长度域字段获取一个完整的报文。
请求数据
- 请求数据通常为序列化之后得到的二进制流,每种请求数据的内容是不一样的。
状态
- 状态字段用于标识请求是否正常。一般由被调用方设置。
- 例如一次 RPC 调用失败,状态字段可被服务提供方设置为异常状态。
保留字段
- 保留字段是可选项,为了应对协议升级的可能性,可以预留若干字节的保留字段,以备不时之需。
通过以上协议基本要素的学习,可以得到一个较为通用的协议示例:
+---------------------------------------------------------------+
| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte |
+---------------------------------------------------------------+
| 状态 1byte | 保留字段 4byte | 数据长度 4byte |
+---------------------------------------------------------------+
| 数据内容 (长度不定) |
+---------------------------------------------------------------+
二、Netty编解码器分类
Netty 作为一个非常优秀的网络通信框架,提供了非常丰富的编解码抽象基类来实现自定义协议。
2.1 编/解码分类
2.2 分层解码分类
- 一次解码:一次解码用于解决TCP拆包/粘包问题,按协议解析得到的字节数据。常用一次编解码器:
MessageToByteEncoder / ByteToMessageDecoder
。 - 二次解码:对一次解析后的字节数据做对象模型的转换,这时候需要二次解码器,同理编码器的过程是反过来的。常用二次编解码器:
MessageToMessageEncoder / MessageToMessageDecoder
。
2.3 抽象编码类
通过抽象编码类的继承图可以看出,编码类是 ChanneOutboundHandler
的抽象类实现,具体操作的是 Outbound 出站数据。
2.4 抽象解码类
解码类是 ChanneInboundHandler
的抽象类实现,操作的是 Inbound 入站数据。解码器的主要难度在于拆包和粘包问题,由于接收方可能没有接受到完整的消息,所以编码框架还要对入站数据做缓冲处理,直到获取到完整的消息。
三、示例
- 编解码器
根据上面的要素,设计一个登录请求消息和登录响应消息,并使用 Netty 完成收发
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;
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@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<Object> 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);
}
}
- 测试
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;
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方法进行解码。
运行结果