自己动手写一个通信协议

什么是通信协议

我们常用的聊天软件比如:微信,都是基于一组通信协议进行服务端与客户端数据交互。协议指的就是客户端与服务端事先约定好的,每个二进制数据包中,每一段字节分别代表什么含义的规则。如下图所示一个简单的登录指令:
在这里插入图片描述
在这个数据包中,第一个字节为 1 表示这是一个登录指令,接下来是用户名和密码,这两个值以 \0 分割,客户端发送这段二进制数据包到服务端,服务端就能根据这个协议来取出用户名密码,进行登录逻辑。实际的通信协议设计中,我们会考虑更多细节,要比这个稍微复杂一些。
那么,协议设计好之后,客户端与服务端的通信过程又是怎样的呢?
在这里插入图片描述

通信协议的设计

在这里插入图片描述

  1. 第一个字段为魔数(magic_number),通常情况下都是几个字节(这里我们定义 4 个字节),魔数的作用类似于协议内的标识,通过客户端与服务端魔数对比,我们就知道这组二进制数据是否属于当前通信协议。这和 Java 字节码 中的魔数 0xCAFEBABE 用来标识这个文件,有着异曲同工之妙。
  2. 第二个字段为版本号,占用 1 个字节,通常情况下是预留字段。
  3. 第三部分是序列化算法,占用 1 个字节,表示如何把 Java 对象转换成二进制,二进制转换成 java 对象,比如 Java 自带的序列化,json,hessian 等序列化方式。
  4. 第四部分是指令,占用 1 个字节,服务端或者客户端每收到一种指令都会有相应的处理逻辑,最高支持256种指令,对于我们这个通信系统来说已经完全足够了。
  5. 第五部分是数据长度,占用 4 个字节。
  6. 最后一部分则是数据内容,每一个指令对应的数据是不一样的,比如登录的时候需要用户名密码,收消息的时候需要用户标识和具体消息内容等等。

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

通信协议的实现

我们把Java 对象封装成二进制的过程叫编码, 从二进制数据包解析成 Java 对象的过程叫解码,在学习如何使用 Netty 进行通信协议的编解码之前,我们先来定义一下客户端与服务端通信的 Java 对象。

Java 对象
public abstract class Packet {
    /**
     * 协议版本
     */
    private Byte version = 1;

    /**
    * 指令
    * /
    public abstract Byte getCommand();
}

接下来,我们拿客户端登录请求为例,定义登录请求数据包

public interface Command {
    Byte LOGIN_REQUEST = 1;
}
public class LoginRequestPacket extends Packet {
    private Integer userId;
    private String username;
    private String password;

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

Command 定义一些指令,不同的 command 指令分别对应不同的数据内容。
这里 Command 定义了 LOGIN_REQUEST 指令表示登录请求,相应的就会有与之对应的 Java 对象 LoginRequestPacket。

Java 对象定义完成之后,接下来我们就需要定义一种规则,如何把一个 Java 对象转换成二进制数据,这个规则叫做 Java 对象的序列化。

序列化

定义序列化接口:

public interface Serializer {

    /**
     * 序列化算法
     */
    byte getSerializerAlgorithm(); // (1)
    
    /**
     * java 对象转换成二进制
     */
    byte[] serialize(Object object); // (2)

    /**
     * 二进制转换成 java 对象
     */
    <T> T deserialize(Class<T> clazz, byte[] bytes); // (3)
}
  1. getSerializerAlgorithm() 获取具体的序列化算法标识。
  2. serialize() 将 Java 对象转换成字节数组。
  3. deserialize() 将字节数组转换成某种类型的 Java 对象。

序列化接口的实现,这里我们使用最简单的 json 序列化方式,使用阿里巴巴的 fastjson 作为序列化框架:

public interface SerializerAlgorithm {
    /**
     * json 序列化标识
     */
    byte JSON = 1;
}


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);
    }
}

这样我们就实现了序列化的相关逻辑。

序列化定义了 Java 对象与二进制数据的互转过程,接下来,我们就来学习一下,如何把这部分的数据编码到通信协议的二进制数据包中去。

编码
private static final int MAGIC_NUMBER = 0x12345678;

public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet){
    // 创建ByteBuf对象
    ByteBuf byteBuf = byteBufAllocator.ioBuffer(); // (1)

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

    // 实际编码过程 (3)
    // 魔数
    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;
}

分为三个步骤:

  1. 首先,我们需要创建一个 ByteBuf,这里我们调用 Netty 的 ByteBuf 分配器来创建,ioBuffer() 方法会返回适配 io 读写相关的内存,它会尽可能创建一个直接内存,直接内存可以理解为不受 jvm 堆管理的内存空间,写到 IO 缓冲区的效果更高。
  2. 将 Java 对象序列化成二进制数据包
  3. 编码过程

编码完成之后会发送给另一端,另一端就需要进行解码操作。

解码
public Packet decode(ByteBuf byteBuf) {
    // 跳过 magic number
    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;
}

解码过程刚刚好和编码过程相反。

总结

我们了解了什么是通信协议,并且自己动手实现一个简单的通信协议,还了解到了编码和解码的过程。
文章参考:https://juejin.im/book/5b4bc28bf265da0f60130116
代码下载:https://github.com/jeansTuo/-

  • 19
    点赞
  • 132
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值