基于 Netty 实现自定义协议

举报
大明哥 发表于 2024/11/19 22:58:20 2024/11/19
【摘要】 在上篇文章[Netty 进阶 — Netty 的编解码器]大明哥阐述了 Netty 的常用解码器,这些解码器都是开箱即用的,它提供了 TCP 拆包/粘包的通用解决方案。但是只解决拆包/粘包问题还不够,因为我们在实际开发过程使用的 Java 对象,我们需要将发送端传递过来的消息解析成 Java 对象才能使用,所以这篇文章我们将在这个基础上来探讨如何利用 Netty 实现自定义通信协议。 通用协...

在上篇文章[Netty 进阶 — Netty 的编解码器]大明哥阐述了 Netty 的常用解码器,这些解码器都是开箱即用的,它提供了 TCP 拆包/粘包的通用解决方案。但是只解决拆包/粘包问题还不够,因为我们在实际开发过程使用的 Java 对象,我们需要将发送端传递过来的消息解析成 Java 对象才能使用,所以这篇文章我们将在这个基础上来探讨如何利用 Netty 实现自定义通信协议。

通用协议设计

所谓**协议是通信双方为了实现通信而设计的约定或通话规则。**在 TCP 网络编程中,发放方和接收方双方需要约定以什么样的协议来约定,进行有效的通信。

在网络编程中,协议其本质是将对象转换为字节流,或者将字节流转换为对象的一个规范。对于发送方而言,它需要依照规范将对象转换为字节流,对于接收方而言,则需要知道如果将接收的字节流转换为对象,这就是协议。

目前市面上有不少较为优秀的通用协议,如 JSON、Hessian、Protobuf 等,他们性能高、兼容性较好,且经过大规模的验证,在各种异构系统之间可以实现无缝对接。如果不是有特殊业务需求的话,我们一般建议不建议采用自定义协议。

一款自定义协议一般都需要包括两个部分:消息头和消息体。消息头长度固定,主要用于定义一些公有的信息,如魔数、版本号、序列化类型、消息类型等等通用型信息。而消息体则是此次需要发送的消息。下面是一个较为通用的协议规范:

+---------------------------------------------------------------+
| 魔数 4byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte  |
+---------------------------------------------------------------+
| 状态 1byte |        保留字段 4byte     |      数据长度 4byte     | 
+---------------------------------------------------------------+
|                   数据内容 (长度不定)                          |
+---------------------------------------------------------------+
  • 魔数

魔数一般是一个固定数字,主要用于防止任何随便向服务器的端口发送数据。接收方接收到消息后,解析出魔数,然后做对比,如果发现魔数不匹配则可认为是非法数据,丢弃即可。

Java 生成的 class 文件起始就使用 0xCAFEBABE 作为其标识符。

  • 协议版本号

随着业务的发展,发送方和接受放可能会对协议进行调整,不同的版本对应不同的解析方法。所以在实际生成环境中一般都建议保留版本号。通过版本号,接收方可自行对协议进行调整升级,然后通知发送方进行迭代调整,待其所有发送方都接入新版本后可根据业务需要对旧版本下线。

  • 序列化算法

对象要进行传输就需要进行序列化,将对象转换为二进制字节流,发送方通过某种序列化算法将对象转化为二进制,那么接收方就需要使用同样的序列化算法才能将二进制还原为对象。目前较为流行的序列化算法有:JSON、Hessian、Protobuf,Java 自带的序列化算法就算了吧,笨、重、还不好用。大明哥在【死磕 Java 基础】中深入分析了这几种序列化算法,有兴趣的小伙伴去看看。

  • 报文类型

不同的业务场景,报文类型就有所不同。比如在 RPC 中有请求、响应、PING、PONG 等等。

  • 状态

用于标识请求是否正常。用于标识一次通信请求是否已处理成功。

  • 保留字段

附加字段,备用。

  • 数据长度

记录消息体的长度

  • 数据内容

服务之间交互所发送的数据。

基于 Netty 的自定义协议

在了解如果用 Netty 实现自定义协议之前,我们需要先了解 Netty 的编解码核心原理。

Netty 的编解码器

Netty 提供了一套完整的编解码器,我们只需要继承相对应的抽象类就可以实现自己的编解码规则了。接收端的数据在 ChannelInboundHandler 中处理的,发送端的数据是在ChannelOutboundHandler中处理的。

编码器

对于编码器,Netty 提供了两个抽象类供我们使用。

  • MessageToByteEncoder:对象编码为字节流。
  • MessageToMessageEncoder: 一个对象编码为另外一个对象。

整体结构如下:

MessageToByteEncoder

MessageToByteEncoder 是将一个对象编码成 Byte 字节流, 定义如下:

public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {
    ...
    protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
}

MessageToByteEncoder 是一个泛型类,I 表示需要编码的对象的类型。它有一个抽象方法 encode() ,该方法有三个参数,msg 表示需要编码的对象,out 则是编码的结果,我们需要将对象的信息写入到 out 实例中来。子类通过实现该方法来实现编码操作,如:

public class StringToByteEncoder extends MessageToByteEncoder<String> {
    
    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
        out.writeBytes(msg.getBytes());
    }
}

该实例演示了讲 String 对象转换为 Byte 的编码过程。

MessageToMessageEncoder

MessageToMessageEncoder 是将一个对象编码成另外一个对象,其定义如下:

public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerAdapter {
    protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
}

与 MessageToByteEncoder 一样,泛型 I 是我们需要编码的对象,但是与 MessageToByteEncoder 不一样的地方在于,它是将编码的结果放到 List 集合中。一般来说我们都只是将 MessageToMessageEncoder 当做一个中间的编码器,编码后的结果属于中间对象,最终依然会转换为 ByteBuf 进行传输。下面用一个案例来演示下如何使用的:

public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
        if (msg != null) {
            out.add(msg.toString());
        }
    }
}

一般来说编码器相对而言比较简单,因为编码器只需要按照相对应的协议将对象转换为字节流即可,无须考虑拆包/粘包问题。

解码器

对于解码器,Netty 也提供了两个解码器:

  • ByteToMessageDecoder:将字节流解码为对象。
  • MessageToMessageDecoder:将一个对象解码为另一个 对象。

结构如下:

ByteToMessageDecoder

ByteToMessageDecoder 用于将字节流转换为对象的解码器,它的定义如下:

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
  ...
      protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
}

它提供了一个抽象方法 decode(),我们自定义解码器只需要实现该抽象方法即可。decode() 方法提供了三个参数:

  • ByteBuf in:需要解码的字节流
  • List<Object> out:解码后的对象集合。这里之所以使用一个 List 集合是因为考虑到粘包问题,我们传入的 in 可能包含有多个有效报文。当然,也有可能是拆包,in 中一个有效报文都没有,这个时候我们只需要不往 List 中添加对象即可。

考虑到粘包/拆包问题,我们在使用 ByteToMessageDecoder 的时候需要格外注意:不能对传入的字节流直接解码,而是首先判断该字节流能否构成一个有效的报文,如下:

public class ByteToIntegerDecoder extends ByteToMessageDecoder {
    
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() >= 4) {
            // int 占用四个字节
            out.add(in.readInt());
        }
    }
}

这段代码是将 Byte 解码成 Integer,我们知道一个 Integer 占四个字节,所以只有可读字节数 ≥ 4 的时候,我们才能进行一次解码 ,才能读取一个 有效的 Integer。

那可读字节数 < 4 呢?我们不用做任何处理,可能有小伙伴担心,不做任何处理,未处理的数据不会丢失吗?其实是不会的,你可以认为 ByteToMessageDecoder 会对这些字节流进行缓存(具体如何处理,在源码篇会深入分析),等到下一次补齐后,在来调用 ByteToIntegerDecoder.decode() 进行解码。

那如果可读字节数 > 4 呢?由于我们这里只读取了 4 个字节,那剩余的字节数怎么办?其实我们也不用担心,ByteToMessageDecoder 它是通过 while(in.isReadable()) 的方式来判断当前 ByteBuf 是否还有剩余字节可读,如果有,则继续调用 decode() ,直到 List 中的元素个数没有发生变化才会停止,List 元素个数没有变化则意味着解码器已经无法从剩余的字节中读取一个有效的报文了。

所以,依照这个特性,大明哥建议,我们一般一次只解码一个有效报文,没有必要一次性全部解码出来,这样会使解码程序变得复杂,复杂就意味着不可控。

MessageToMessageDecoder

MessageToMessageDecoder 是将一个对象解码成另一个对象的解码器。其定义如下:

public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter {
    ...
    protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
}

与 ByteToMessageDecoder 一样,他也需要子类实现它的 decode()。它传入的参数是泛型 I,也就是我们需要解码的对象。

MessageToMessageDecoder 主要用于二次解码,比如我们利用 ByteToMessageDecoder 将 ByteBuf 解码成一个 Java 对象后,该对象可能是一个中间对象,这个时候我们可以再次利用 MessageToMessageDecoder 对其进行二次解码,将其解码成我们真正需要的对象。

一个比较容易理解的例子:相信各位小伙伴都写过 Spring MVC,在 Spring MVC 中我们都是利用 Java 对象来接收参数的。浏览器发送过来的数据是二进制数据,进入 Web 容器后,Web 容器会将其解析一个 HttpServletRequest 对象,然后我们再次对 HttpServletRequest 对象的数据进行加工提取,封装成我们 Java 对象。所以这里就有两次解码了,第一次是将二进制解码成 HttpServletRequest 对象(对应 ByteToMessageDecoder),第二次是将 HttpServletRequest 对象解码成 POJO 对象(对应 MessageToMessageDecoder)。

编解码器

除了编解码之外,Netty 还提供了一套即可编码又可解码的编解码器 Codec,如同编码器和解码器,它也有两个抽象类:

  • ByteToMessageCodec:字节流 < — > 对象的编解码器
  • MessageToMessageCodec:一个对象转换为另一个对象的编解码器

结构图如下:

从图中可以看出,ByteToMessageCodec 和 MessageToMessageCodec 都继承 ChannelDuplexHandler,而 ChannelDuplexHandler 又同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler 接口,它能够同时处理入站和出站。

ByteToMessageCodec 是一个字节流 < — > 对象 的编解码器,其内部封装了 ByteToMessageDecoder 和 MessageToByteEncoder ,然后实现了编码和解码的功能。

public abstract class ByteToMessageCodec<I> extends ChannelDuplexHandler {
    private final MessageToByteEncoder<I> encoder;
    private final ByteToMessageDecoder decoder = ...;
    ...
    protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
    
    ...
}

实现自定义协议

上面大明哥已经分析了 Netty 的编解码器,现在我们利用这些编解码器来实现自定义协议。协议规范如下:

+---------------------------------------------------------------+
| 魔数 4byte | 协议版本号 1byte | 数据长度 4byte                   |
+---------------------------------------------------------------+
|                   数据内容 (长度不定)                          |
+---------------------------------------------------------------+

为了充分演示 Netty 编解码的使用,对于一些信息无法设置进去,比如序列化方式(我们直接采用 JSON),指令类型等等。

消息定义

我们先定义一个 abstract 的 message,它是所有消息的抽象,所有消息都需要继承他。


public abstract class Message {
}

定义完抽象消息体后,我们在定义两个消息:

public class ChatMessage extends Message{

    /**
     * 消息内容
     */
    private Object content;
}

这里只简单地定义了两个消息:ChatMessage 和 PingMessage,由于我们只演示 Netty 的编解码,知道如何利用 Netty 来自定义编解码即可,所以就不需要定义多且复杂的消息。

消息发送端 :自定义编码器

定义完消息体后,我们就写消息发送方法部分,我们用客户端来当做发送方,发送方则是编码,他需要将消息对象转换为二进制。为了更好的演示上面的编码器,我们将消息编码分为几个步骤:

  1. 将消息转换为 JSON 格式的 String 类型,即利用 MessageToMessageEncoder。
  2. 将 String 转换 Byte ,即利用 MessageToByteEncoder。
  • ObjectToJsonEncoder

ObjectToJsonEncoder 则是将 Java 对象转换为 Json 格式的 String。

public class ObjectToJsonEncoder extends MessageToMessageEncoder<Message> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List out) throws Exception {
        if (msg != null) {
            out.add(JSONObject.toJSONString(msg));
        }
    }
}
  • JsonStringToByteEncoder

JsonStringToByteEncoder 则是将 String 对象转换为 Byte。

public class JsonStringToByteEncoder extends MessageToByteEncoder<String> {

    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
        //1. 4 字节的魔数
        out.writeBytes(new byte[]{1,3,1,4});

        //2. 1 字节的版本协议号
        out.writeByte(1);

        byte[] bytes = msg.getBytes();

        //3 4 字节的消息长度
        out.writeInt(bytes.length);

        //4 消息内容
        out.writeBytes(bytes);
    }
}

这里我们需要填充下内容,由于需要充分验证 Netty的编解码器,所以在这里无法构造一个比较完整的协议。JsonStringToByteEncoder 的实现很简单,将魔数、版本号、消息长度和消息写入 ByteBuf 即可。

  • 发送端
public class Client {
    public static void main(String[] args) {
        new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,5,4,0,0));
                        ch.pipeline().addLast(new JsonStringToByteEncoder());
                        ch.pipeline().addLast(new ObjectToJsonEncoder());
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                             @Override
                             public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                 ChatMessage chatMessage = new ChatMessage("hello, boy!!!");
                                 ctx.writeAndFlush(chatMessage);
                             }
                         });
                    }
                }).connect("127.0.0.1",8081);
    }
}

发送端要注意几个地方:

  1. 各个 Handler 的添加顺序,小伙们一定要注意出站的调度顺序,如果还有不清楚的,先到这篇文章去研究研究:Netty 入门 — ChannelPipeline,Netty 的核心编排组件
  2. LengthFieldBasedFrameDecoder。为了解决拆包/粘包的问题,这里使用了 LengthFieldBasedFrameDecoder 的编码器。注意它的五个参数
    • maxFrameLength:1024,表示消息最大长度为 1024 字节。
    • lengthFieldOffset:消息长度偏移量 5 ,因为我们前面有 4 个字节的魔数 + 1 个字节的协议版本号,所以 为 5。
    • lengthFieldLength:消息长度占用字节 4,规定了消息长度占用 4 个字节。
    • lengthAdjustment:不需要调整,所以为 0。
    • initialBytesToStrip:不需要跳过,所以为 0。

在发送端连接服务端的时候就会往服务端发送消息:hello,boy!!!

消息接收端:自定义解码器

由于发送端将消息的编码分为了两个,所以接收端也需要将解码器分为两个,且顺序恰好相反

  1. 将 Byte 转换为 String,需要使用到 ByteToMessageDecoder。
  2. 将 String 转换为 Java 对象,需要使用到 MessageToMessageDecoder。
  • ByteToStringDecoder

ByteToStringDecoder 是将 Byte 转换为 String,这里我们需要和发送端保持一样的步调,发送端有 4 个字节的魔数,1个字节的协议版本号,4 个字节的消息长度。所以我们也需要按照这样的步调进行解码。

public class ByteToStringDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //1. 4 字节魔数
        in.readInt();

        //2. 1 字节版本号
        in.readByte();

        //3. 4 字节消息长度
        int length = in.readInt();

        //4. 读取消息
        byte[] bytes = new byte[length];
        in.readBytes(bytes,0,length);

        String message = new String(bytes,Charset.defaultCharset());
        log.info("message :{}", message);
        out.add(message);
    }
}

这个类就是操作 ByteBuf,有对 ByteBuf API 不熟悉的小伙伴,可以复习复习这篇文章:Netty 入门 — ByteBuf,Netty 数据传输的载体

  • StringToObjectDecoder

获取到 String 对象后,我们只需要利用 JSON 将其还原为 Java 对象就可以了。

public class StringToObjectDecoder extends MessageToMessageDecoder<String> {

    @Override
    protected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
        if (msg != null) {
            ChatMessage chatMessage = JSONObject.parseObject(msg,ChatMessage.class);
            out.add(chatMessage);
        }
    }
}
  • 接收端
public class Server {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup(),new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,5,4,0,0));
                        ch.pipeline().addLast(new ByteToStringDecoder());
                        ch.pipeline().addLast(new StringToObjectDecoder());
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                             @Override
                             public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ChatMessage chatMessage = (ChatMessage) msg;
                                System.out.println("接受内容:" + chatMessage.getContent());
                            }
                        });
                    }
                }).bind(8081);
    }
}

接收端也是要注意 Handler 的顺序哈。发送端使用 new LengthFieldBasedFrameDecoder(1024,5,4,0,0) 编码,所以我们也需要使用同样的规则来进行解码。

下面我们来测试。

测试

测试1:发送一个消息

发送端正常发送一个消息,结果如下:

// 发送端日志
2022-08-17 23:59:36.370 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x661e7dc1, L:/127.0.0.1:51092 - R:/127.0.0.1:8081] WRITE: 36B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 03 01 04 01 00 00 00 1b 7b 22 63 6f 6e 74 65 |.........{"conte|
|00000010| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|00000020| 21 21 22 7d                                     |!!"}            |
+--------+-------------------------------------------------+----------------+

// 接收端日志
2022-08-17 23:59:36.391 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x746b4f3e, L:/127.0.0.1:8081 - R:/127.0.0.1:51092] READ: 36B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 03 01 04 01 00 00 00 1b 7b 22 63 6f 6e 74 65 |.........{"conte|
|00000010| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|00000020| 21 21 22 7d                                     |!!"}            |
+--------+-------------------------------------------------+----------------+
2022-08-17 23:59:36.399 [nioEventLoopGroup-3-1] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!"}
接受内容:hello, boy!!!

客户端发送 36B 的消息给服务端,服务端也接收到了 36B 的数据,同时也正确解析了报文,得到了正确的结果。这是客户端发送一个消息给服务端,所以不存在拆包/粘包的问题,那如果发送多个呢?

测试2:发送多个消息

我们重新定义一个 Client2,代码和上面的 Client 保存一样,只不过发送消息的时候,发 500 次。

ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    for(int i = 1 ; i <= 500 ; i++) {
      ChatMessage chatMessage = new ChatMessage("hello, boy!!!--" + i);
      ctx.writeAndFlush(chatMessage);
    }
  }
});

那服务端呢?大明哥尝试了好多次,终于找到了一个符合要求的日志(不一定会发送拆包/粘包情况,需要多试几次):

2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf27362ce, L:/127.0.0.1:8081 - R:/127.0.0.1:51288] READ: 160B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|00000010| 21 21 2d 2d 37 32 22 7d 01 03 01 04 01 00 00 00 |!!--72"}........|
|00000020| 1f 7b 22 63 6f 6e 74 65 6e 74 22 3a 22 68 65 6c |.{"content":"hel|
|00000030| 6c 6f 2c 20 62 6f 79 21 21 21 2d 2d 37 33 22 7d |lo, boy!!!--73"}|
|00000040| 01 03 01 04 01 00 00 00 1f 7b 22 63 6f 6e 74 65 |.........{"conte|
|00000050| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|00000060| 21 21 2d 2d 37 34 22 7d 01 03 01 04 01 00 00 00 |!!--74"}........|
|00000070| 1f 7b 22 63 6f 6e 74 65 6e 74 22 3a 22 68 65 6c |.{"content":"hel|
|00000080| 6c 6f 2c 20 62 6f 79 21 21 21 2d 2d 37 35 22 7d |lo, boy!!!--75"}|
|00000090| 01 03 01 04 01 00 00 00 1f 7b 22 63 6f 6e 74 65 |.........{"conte|
+--------+-------------------------------------------------+----------------+
2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--72"}
接受内容:hello, boy!!!--72
2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--73"}
接受内容:hello, boy!!!--73
2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--74"}
接受内容:hello, boy!!!--74
2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--75"}
接受内容:hello, boy!!!--75
2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf27362ce, L:/127.0.0.1:8081 - R:/127.0.0.1:51288] READ: 224B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|00000010| 21 21 2d 2d 37 36 22 7d 01 03 01 04 01 00 00 00 |!!--76"}........|
|00000020| 1f 7b 22 63 6f 6e 74 65 6e 74 22 3a 22 68 65 6c |.{"content":"hel|
|00000030| 6c 6f 2c 20 62 6f 79 21 21 21 2d 2d 37 37 22 7d |lo, boy!!!--77"}|
|00000040| 01 03 01 04 01 00 00 00 1f 7b 22 63 6f 6e 74 65 |.........{"conte|
|00000050| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|00000060| 21 21 2d 2d 37 38 22 7d 01 03 01 04 01 00 00 00 |!!--78"}........|
|00000070| 1f 7b 22 63 6f 6e 74 65 6e 74 22 3a 22 68 65 6c |.{"content":"hel|
|00000080| 6c 6f 2c 20 62 6f 79 21 21 21 2d 2d 37 39 22 7d |lo, boy!!!--79"}|
|00000090| 01 03 01 04 01 00 00 00 1f 7b 22 63 6f 6e 74 65 |.........{"conte|
|000000a0| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 62 6f 79 21 |nt":"hello, boy!|
|000000b0| 21 21 2d 2d 38 30 22 7d 01 03 01 04 01 00 00 00 |!!--80"}........|
|000000c0| 1f 7b 22 63 6f 6e 74 65 6e 74 22 3a 22 68 65 6c |.{"content":"hel|
|000000d0| 6c 6f 2c 20 62 6f 79 21 21 21 2d 2d 38 31 22 7d |lo, boy!!!--81"}|
+--------+-------------------------------------------------+----------------+
2022-08-18 00:19:31.825 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--76"}
接受内容:hello, boy!!!--76
2022-08-18 00:19:31.826 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--77"}
接受内容:hello, boy!!!--77
2022-08-18 00:19:31.826 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--78"}
接受内容:hello, boy!!!--78
2022-08-18 00:19:31.826 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--79"}
接受内容:hello, boy!!!--79
2022-08-18 00:19:31.826 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--80"}
接受内容:hello, boy!!!--80
2022-08-18 00:19:31.826 [nioEventLoopGroup-3-2] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToStringDecoder - message :{"content":"hello, boy!!!--81"}
接受内容:hello, boy!!!--81

从日志中我们可以看到第一个是 160B,第二个是 224B,我们一个消息的总大小为 40B,第二个消息根本就不是 40 的整数倍,所以它一定发生了粘包/拆包。第一个虽然可以整除,但是我们从打印的日志来看,它完完整整打印了 10 条消息。

测试3:调整 LengthFieldBasedFrameDecoder

在上面的例子中,我们只是简单使用了 LengthFieldBasedFrameDecoder,经过 LengthFieldBasedFrameDecoder 解码后,它传递给下一个 Handler 的消息还是包含了消息头 (魔数、版本号、消息长度),可能有小伙伴说,我不要这个消息头,我希望他传给 ByteToStringDecoder 就是一个干干净净的消息体。怎么做呢?

原来 4 个核心参数如下:

  • lengthFieldOffset:消息长度偏移量 5 ,因为我们前面有 4 个字节的魔数 + 1 个字节的协议版本号,所以 为 5
  • lengthFieldLength:消息长度占用字节 4,规定了消息长度占用 4 个字节。
  • lengthAdjustment:不需要调整,所以为 0。
  • initialBytesToStrip:不需要跳过,所以为 0。

现在呢?

  • lengthFieldOffset:5,消息长度偏移量不变。
  • lengthFieldLength:4,消息长度占用字节不变。
  • lengthAdjustment:0,消息长度只包含了消息的长度,所以不需要调整。
  • initialBytesToStrip:9,为什么是 9?4 + 1 + 4,即我们需要跳过 4 字节魔数 + 1 字节版本协议号 + 4 字节长度。

故而接收端的 LengthFieldBasedFrameDecoder 为 new LengthFieldBasedFrameDecoder(1024,5,4,0,9)。验证下。

首先我们需要调整我们的 ByteToString2Decoder,因为返回给他的 ByteBuf 一整个消息了,它是一个只包含了消息体的 ByteBuf ,所以我们直接转换为 String 就可以了。

public class ByteToString2Decoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        byte[] bytes = new byte[in.readableBytes()];
        in.readBytes(bytes);

        String message = new String(bytes,Charset.defaultCharset());
        log.info("message :{}", message);
        out.add(message);
    }
}

运行结果如下:·

2022-08-18 00:45:55.728 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x98445a1c, L:/127.0.0.1:8081 - R:/127.0.0.1:51543] READ: 38B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 03 01 04 01 00 00 00 1d 7b 22 63 6f 6e 74 65 |.........{"conte|
|00000010| 6e 74 22 3a 22 68 65 6c 6c 6f 2c 20 67 69 72 6c |nt":"hello, girl|
|00000020| 6c 21 21 21 22 7d                               |l!!!"}          |
+--------+-------------------------------------------------+----------------+
2022-08-18 00:45:55.736 [nioEventLoopGroup-3-1] INFO  com.sike.netty.jinjie.codec.protocol.decoder.ByteToString2Decoder - message :{"content":"hello, girll!!!"}
接受内容:hello, girll!!!

一样完美解决问题!!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。