通用协议及自定义通信协议。

589 阅读5分钟

概念

在网络编程中,无论使用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…

image.png

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