章节索引 :

Netty 粘包和拆包

1. 前言

前面几个章节主要解析了 Netty 的编码、解码问题,那么是否有了编解码器,我们的 Netty 通信就能正常了呢?

TCP 协议在传输数据时没有办法判断数据是什么时候结束的,它无法识别一段完整的信息,因此可能会导致接受到的数据和发送时的数据不一致的情况。因此需要人为的指定一种规范的协议,从而保证数据的安全性,比如:我们所熟悉的 HTTP 协议。

本节内容,我们主要需要以下两点知识

  1. TCP 拆包、粘包的原因;
  2. TCP 拆包、粘包的解决方案。

2. 学习目的

拆包、粘包在 TCP 协议当中,或者说 Netty 开发当中必须需要去解决的问题。在开发当中,你会发现你不需要解决拆包、粘包问题,数据也是能正常发送和接受,那么为什么需要去解决呢?

原因是,数据量比较小,TCP 发送之前它是有个缓冲池的,根据缓冲池的大小来把数据包拆分成多个小包进行发送。在高并发的情况下,拆包、粘包问题是经常会发生的,因此需要去 解决,否则接收方将获取不到正确的数据。

3. 粘包和拆包问题解析

3.1 模拟拆包粘包问题

开始,之前我们先看一个简单的案例,具体如下所示:

客户端: 客户端使用 for 循环,连续向服务端发送 hello world1000 遍(使用 StringEncoder 编码器)。

public class ClientTestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {     
        for(int i=0;i<1000;i++){
            ctx.channel().writeAndFlush(
                Unpooled.copiedBuffer("hello world 世界你好,Netty技术学习".getBytes())
            );
        }
    }
}

服务端: 正常输出客户端的信息(使用 StringDecoder 解码器)。

public class ServerTestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String str=msg.toString();
        System.out.println(str);
    }
}

输出结果:
图片描述

总结:
通过以上的输出结果,我们发现,客户端发送过来的数据,有时候能正确打印,有时候数据粘在了一起。以上输出结果有乱码想象、有多个信息输出到一行,就是 ByteBuf 粘包和 ByteBuf 半包。

通过上面的简单案例,我们发现 TCP 协议下会产生数据安全性问题,其实在 TCP 中粘包和拆包是不可避免的,因为在 TCP 协议中,数据流向水流一样,根本不知道应该从哪里截取才是完整的数据包。TCP 并不了解上层业务的数据含义,它会根据 TCP 缓冲区的实际情况进行包的划分,因此一个完整的业务包可能会被 TCP 拆分成多个包进行发送,也可能会把多个小包封装成一个大包进行发送,这就是 TCP 粘包和拆包问题。

3.2 常见的原因分析

粘包和拆包其实是客户端和服务端之间都会发生的事情,并不是说只是在客户端产生或者服务端产生,具体分析如下:

发送方的粘包和拆包问题

  1. 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包,也就是拆分几次发送;
  2. 要发送数据大于最大报文长度,TCP 在传输前将进行拆包,也就是拆分几次发送;
  3. 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。

接收方的粘包和拆包问题

  1. 服务端分两次读取到独立的数据包,那么解析出来的数据正常,没有粘包和拆包问题;
  2. 服务端一次读取两个数据包,那么这些数据包就会粘合在一起,因此称为粘包;
  3. 服务端分两次读取两个数据包,第一次读到数据 1 和数据 2 部分内容,第二次读取数据 2 剩余内容,这被成为 TCP 拆包。

粘包和拆包的示意图

图片描述

总结,拆包和粘包问题并不是某一方的问题,可能是发送的粘包和拆包导致接收方读取数据出错,也可能是发送方正常,但是接收方读取出错。但是我们只需要了解,发送方和接收方什么情况下会拆包和粘包。

4. Netty 提供的粘包拆包解决方案

虽然,在 Netty 当中是基于 ByteBuf 字节容器去编程,但是底层还是会被转换成字节流进行传输, 数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf。如果为了数据的完整性,通常的解决方案如下:

  1. 每次读取完都需要判断是否是一个完整数据包 ;
  2. 如果当前读取的数据不足以拼接成一个完整数据包,那就保留该数据,继续从 TCP 缓冲器读取,直到拼接成一个完整数据包为止;
  3. 如果拼接成了完整的数据包,但是有多余的数据,则仍然保留,以便和下次读取的数据进行拼接。

思考:那么应该如何去判断一个业务数据的完整结束呢?

方案一: 固定数据长度,客户端在发送数据的时候,每个数据包的长度固定(比如:1024 个字节),如果发送数据不足 1024 字节时,以空格补齐;服务端则每次读取固定长度是数据;
方案二: 分隔符,每个数据包的结尾加一个特殊分隔符,服务端则读取到特殊分隔符则认为数据包结束;如果一次读取的数据没有结束符,则保留当前数据,等待下次读取;
方案三: 将数据分为消息头和消息体,在头部保存了消息的数据长度,只有读取指定长度的数据就算完整数据包;
方案四: 自定义协议,通过协议的规范进行发送和接受数据。

当然,以上的方案 Netty 官方也考虑到了,并且为了简化开发人员的工作量,Netty 内置了常见的拆包器,具体如下:

1. 固定长度的拆包器 FixedLengthFrameDecoder

每个数据包的长度都是固定的,比如 1024,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 1024 的数据包 (ByteBuf) 传递到下一个 channelHandler。

2. 行拆包器 LineBasedFrameDecoder

它是一个特殊的分隔符拆包器,以换行符作为结束符。

3. 分隔符拆包器 DelimiterBasedFrameDecoder

可以自定义自己的分隔符。

4. 基于长度域拆包器 LengthFieldBasedFrameDecoder

是最通用的一种拆包器,有一个存放数据长度的字段,读到该字段之后,往后面的数据读取一定长度的数据即可,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。

5. 小结

本节内容需要掌握的知识点

  1. 什么是拆包、粘包问题,以及它的产生原因是什么?
  2. 解决拆包、粘包问题的思路以及常见解决方案是什么?