【Netty学习】5.Netty中的粘包和拆包


1 什么是粘包和半包?

粘包问题是指数据在传输时,在一条消息中读取到了另一条消息的部分数据,这种现象就叫做粘包

半包问题是指接收端只收到了部分数据,而非完整的数据的情况就叫做半包

粘包半包问题

2 为什么 TCP 应用中会出现粘包和半包现象?

2.1 粘包的主要原因

  • 发送方每次写入数据 < 套接字缓冲区大小;

    由于 TCP 协议本身的机制(面向连接的可靠的协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用 Nagle 算法对较小的数据包进行合并,然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包。

  • 接收方读取套接字缓冲区数据不够及时

    服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。

2.2 半包的主要原因

  • 发送方写入数据 > 套接字缓冲区大小;
  • 进行 MSS 大小的 TCP 分段。

    MSS=TCP 报文段长度-TCP 首部长度

  • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包。

2.3 换个角度看

  • 收发:一个发送可能被多次接收,多个发送可能被一次接收。
  • 传输:一个发送可能占用多个传输包,多个发送可能公用一个传输包。

2.4 根本原因

TCP 是流式协议,消息无边界。

Tips: UDP本身作为无连接的不可靠的传输协议,不会对数据包进行合并发送(也就没有 Nagle 算法之说了),它直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP 头+IP 头等等,即每个数据包均有界),也就没有粘包一说了。

2.5 示例

2.5.1 服务端

/**
 * 服务器端(只负责接收消息)
 */
class ServSocket {
    // 字节数组的长度
    private static final int BYTE_LENGTH = 20;  
    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器
        ServerSocket serverSocket = new ServerSocket(8888);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 得到客户端发送的流对象
        try (InputStream inputStream = clientSocket.getInputStream()) {
            while (true) {
                // 循环获取客户端发送的信息
                byte[] bytes = new byte[BYTE_LENGTH];
                // 读取客户端发送的信息
                int count = inputStream.read(bytes, 0, BYTE_LENGTH);
                if (count > 0) {
                    // 成功接收到有效消息并打印
                    System.out.println("接收到客户端的信息是:" + new String(bytes));
                }
                count = 0;
            }
        }
    }
}

2.5.2 客户端

/**
 * 客户端(只负责发送消息)
 */
static class ClientSocket {
    public static void main(String[] args) throws IOException {
        // 创建 Socket 客户端并尝试连接服务器端
        Socket socket = new Socket("127.0.0.1", 8888);
        // 发送的消息内容
        final String message = "Hi,Java."; 
        // 使用输出流发送消息
        try (OutputStream outputStream = socket.getOutputStream()) {
            // 给服务器端发送 10 次消息
            for (int i = 0; i < 10; i++) {
                // 发送消息
                outputStream.write(message.getBytes());
            }
        }
    }
}

2.5.3 结果

粘包半包

3 解决粘包和半包问题的几种常用方法

解决问题的根本手段:找出消息的边界

3.1 消息定长

发送方和接收方固定发送数据的大小,当字符长度不够时用空字符弥补,有了固定大小之后就知道每条消息的具体边界了,这样就没有粘包的问题了。

3.2 分隔符

以特殊的字符结尾,比如以“\n”结尾,这样我们就知道数据的具体边界了,从而避免了粘包问题。

3.3 消息头+消息体

在 TCP 协议的基础上封装一层自定义数据协议,在自定义数据协议中,包含数据头(存储数据的大小)和 数据的具体内容,这样服务端得到数据之后,通过解析数据头就可以知道数据的具体长度了,也就没有粘包的问题了。

解决方案及各自优缺点

4 Netty 对三种常用封帧方式的支持

方式解码编码
固定长度FixedLengthFrameDecoder简单
分割符DelimiterBasedFrameDecoder, LineBasedFrameDecoder(换行符)简单
固定长度字段存个内容的长度信息LengthFieldBasedFrameDecoderLengthFieldPrepender

下一章,具体了解下Netty的编解码技术。

参考文章:
面试突击70:什么是粘包和半包?怎么解决?
TCP粘包/半包Netty全搞定 - 极客时间


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录