Netty-5-客户端与服务端通信协议编解码

客户端与服务端的通信协议,就是客户端与服务端事先商量好的,每个二进制数据包中的每一段字节分别代表什么含义

看一下下面这个图:
image

如图,就是一个数据包,这个数据包中,第一个字节是1,表示是一个指令,比如是登录还是注册,然后下面是用户名,密码,中间有分隔符做分割

服务端收到这个数据包后,就能按这个格式取出用户名密码,然后执行对应的操作,在实际的通信协议设计中,会比这个要复杂

编解码呢,就是客户端按照协议去组装数据发送给服务端,然后服务端也按照协议去解析数据

客户端服务端通信过程

如图:
image
客户端服务端的通信过程就是.首先客户端把Java对象按照通信协议转换成二进制数据包,然后通过网络把这段二进制数据包发送到服务端,数据的传输过程由TCP/IP协议负责数据的传输

服务端接收到数据按照协议截取二进制数据包的相应字段包装成Java对象交给应用逻辑处理,服务端处理完毕如果需要返回响应给客户端按照相同的流程进行.

通信协议设计

image

  • 第一部分: 魔数,通常情况为固定的几个字节
  • 第二部分: 版本号
  • 第三部分: 序列化算法,表示通过哪种序列化方式,把Java对象转为二进制对象,二进制对象又如何转为Java对象
  • 第四部分: 指令,表示具体的操作,这里用一个字节最高能有256种指令
  • 第五部分: 数据部分的长度
  • 第六部分: 具体的数据内容

通常情况下,这样一套标准的协议能够适配大多数情况下的服务端与客户端的通信场景,接下来我们就来看一下我们如何使用 Netty 来实现这套协议.

通信协议实现

编码

首先调用ByteBuf分配器ByteBufAllocator,创建ByteBuf对象,使用ioBuffer()方法返回适配io读写相关的内存,它会尽可能创建一个直接内存,直接内存可以理解为不受 jvm 堆管理的内存空间,写到 IO 缓冲区的效果更高

第二步,把Java对象序列化成二进制数据包

最后,按照通信协议逐个往ByteBuf对象写入字段即实现编码.

解码

解码就是根据通信协议,将上面传过来的ByteBuf对象解析,读取到我们需要的数据

具体实现代码

通信数据包

首先,我们的服务端客户端都是通过数据包去通信的,然后我们可以把这个数据包抽象出来,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public abstract class Packet {

/**
* 协议版本号
*/
private Byte version = 1;

/**
* 获取指令
* @return 指令
*/
public abstract Byte getCommand();

}

然后,以后用到的数据包就都继承这个抽象类就好了,比如说我们下面用到的登录请求的数据包,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class LoginRequestPacket extends Packet{

private Integer userId;

private String username;

private String password;

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

这里有一个getCommand()方法,是重写的父类的方法,用来标识是什么命令,Command里面就放了个常量,如下:

1
2
3
4
5
public interface Command {

Byte LOGIN_REQUEST = 1;

}

序列化工具类

我们编解码需要把Java对象转成二进制数据,二进制数据转成Java对象,所以,还需要一个序列化的类,序列化可以有多种实现方式,所以先搞一个接口出来,Serializer,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface Serializer {

/**
* json 序列化
*/
byte JSON_SERIALIZER = 1;


Serializer DEFAULT = new JSONSerializer();

/**
* 序列化算法
*/
byte getSerializerAlgorithm();

/**
* java 对象转换成二进制
*/
byte[] serialize(Object object);

/**
* 二进制转换成 java 对象
*/
<T> T deserialize(Class<T> clazz, byte[] bytes);
}

然后这里我们默认就用阿里巴巴的fastjson去序列化,这里给了个默认的DEFAULT,是实例化了一个JSONSerializer,JSONSerializer的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JSONSerializer implements Serializer {
@Override
public byte getSerializerAlgorithm() {
return SerializerAlgorithm.JSON;
}

@Override
public byte[] serialize(Object object) {
return JSON.toJSONBytes(object);
}

@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
return JSON.parseObject(bytes, clazz);
}
}

这个类就是实现了上面序列化接口,然后一个序列化方法,一个反序列化方法.

最后还有个SerializerAlgorithm,代码如下:

1
2
3
4
5
6
7
8
public interface SerializerAlgorithm {

/**
* Json序列化标识
*/
byte JSON = 1;

}

编解码

序列化和数据包的都搞完了,然后就是编解码的方法了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public class PacketCodeC {

private static final int MAGIC_NUMBER = 0x12345678;

private static final Map<Byte, Class<? extends Packet>> packetTypeMap;
private static final Map<Byte, Serializer> serializerMap;

static {
packetTypeMap = new HashMap<>();
packetTypeMap.put(LOGIN_REQUEST, LoginRequestPacket.class);

serializerMap = new HashMap<>();
Serializer serializer = new JSONSerializer();
serializerMap.put(serializer.getSerializerAlgorithm(), serializer);
}

/**
* 编码
* @param packet
* @return ByteBuf
*/
public ByteBuf encode(Packet packet) {
// 创建ByteBuf对象
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();

// 序列化java对象
byte[] bytes = Serializer.DEFAULT.serialize(packet);

// 实际编码过程
byteBuf.writeInt(MAGIC_NUMBER);
byteBuf.writeByte(packet.getVersion());
byteBuf.writeByte(Serializer.DEFAULT.getSerializerAlgorithm());
byteBuf.writeByte(packet.getCommand());
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);

return byteBuf;
}

/**
* 解码
* @param byteBuf
* @return Packet
*/
public Packet decode(ByteBuf byteBuf) {
// 跳过魔数
byteBuf.skipBytes(4);

// 跳过版本号
byteBuf.skipBytes(1);

// 序列化算法标识
byte serializeAlgorithm = byteBuf.readByte();

// 指令
byte command = byteBuf.readByte();

// 数据包长度
int length = byteBuf.readInt();

byte[] bytes = new byte[length];
byteBuf.readBytes(bytes);

// 具体数据内容
Class<? extends Packet> requestType = getRequestType(command);
Serializer serializer = getSerializer(serializeAlgorithm);

if (requestType != null && serializer != null) {
return serializer.deserialize(requestType, bytes);
}

return null;
}

private Serializer getSerializer(byte serializeAlgorithm) {
return serializerMap.get(serializeAlgorithm);
}

private Class<? extends Packet> getRequestType(byte command) {

return packetTypeMap.get(command);
}
}

这里的packetTypeMap,是放了指令和对应的数据包类,然后serializerMap是存放了序列化的标识和具体的类

然后编解码就ok了

上面这些类的类图如下:

image

image

具体实现编解码的完整代码已经上传到了github