无论使用 Netty 还是原生 Socket 编程,都可以实现自定义的通信协议。

所谓协议就是:客户端和服务端商量好,每一个二进制数据包中的每一段字节分别代表什么含义的规则。

有了规则,在服务端和客户端就可以通过这个设置好的规则进行二进制和对象的转换。

通信协议格式可以参考如下格式

Netty 学习(三):通信协议和编解码_序列化

每个部分的说明如下

魔数:用来标识这个数据包是否遵循我们设计的通信协议,类似 Java 字节码开头的4字节:0xcafebabe

版本标识:用来标识这个协议是什么版本,用于后续协议的升级

序列化算法:用于标识这个协议的数据包使用什么序列化算法,比如:JSON,XML等

指令:用于标识这个数据在收到后应该使用什么处理逻辑。

数据长度&数据内容:不赘述

定好格式以后,

接下来我们可以约定双方的序列化方法,这里我们可以用 JSON 序列化/反序列化 为例,其他格式的类似。

使用 ​​Gson​​ 可以很方便将 JSON 字符串和对象进行互转:

private static final Gson gson = new Gson();
// 序列化
public byte[] serialize(Object object) {
return gson.toJson(object).getBytes(UTF_8);
}
// 反序列化
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
return gson.fromJson(new String(bytes, UTF_8), clazz);
}

实现了对象和字节数组的互转以后,我们需要实现字节数组和 Netty 通信载体 ByteBuf 的互转,包括如下两个方法

ByteBuf 编码(数据包)

上述编码方法需要做如下几个事情

  1. 分配 ByteBuf (分配一块内存区域,Netty 会直接创建一个堆外内存)
  2. 按照协议获取数据包对应的内容
  3. 严格按照协议规定的字节数填充到 ByteBuf 中
数据包 解码(ByteBuf byteBuf)

上述解码方法主要做如下几件事情

  1. 校验魔数
  2. 校验版本号
  3. 如果严格按照规范传输的 ByteBuf,上述两步校验一定是通过的,可以直接跳过。
  4. 获取序列化算法,指令和数据包长度,并将数据内容转换成字节数组
  5. 将字节数组转换成对应的数据包对象。

因为不同的数据包内容有所不一样,所以应该设置一个抽象类,由各个子类实现具体数据包的内容。

package protocol;

import lombok.Data;

/**
* 数据包抽象类
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/15
* @since
*/
@Data
public abstract class Packet {
/**
* 协议版本
*/
private Byte version = 1;

/**
* 指令,由子类实现
*
* @return
*/
public abstract Byte getCommand();
}

对于一个具体的操作,比如登录操作,它需要的数据包需要继承并实现这个抽象类的抽象方法。

package protocol;

import lombok.Data;

import static protocol.Command.LOGIN_REQUEST;

/**
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/15
* @since
*/
@Data
public class LoginRequestPacket extends Packet {
// 登录操作需要的数据内容包括如下三个
private Integer userId;
private String username;
private String password;

@Override
public Byte getCommand() {
return LOGIN_REQUEST;
}
}

对于调用者来说,只需要使用 ​​LoginRequestPacket​​ 即可,无须关注其底层的编码和解码工作。伪代码如下

func() {
LoginRequestPacket loginRequestPacket = new LoginRequestPacket();
loginRequestPacket.setVersion(((byte) 1));
loginRequestPacket.setUserId(123);
loginRequestPacket.setUsername("zhangsan");
loginRequestPacket.setPassword("password");
// 编码
ByteBuf byteBuf = 封装好的编解码工具类.编码(loginRequestPacket);
// 解码
Packet decodedPacket = 封装好的编解码工具类.解码(byteBuf);
// 序列化成我们需要的对象
序列化和反序列化工具类.序列化(decodedPacket);
}