• Netty入门指南之NIO 粘包与半包


    作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者!
    个人主页Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客
    当前专栏Netty应用专栏_Aomsir的博客-CSDN博客

    参考文献

    前言

    在之前的文章中,我们深入了解了NIO中的两个核心模块,ChannelBuffer,包括它们的结构、作用以及所解决的问题等。然而,虽然我们已经掌握了理论知识,但尚未经历实际的应用。在今天的这篇文章中,我们将以实战场景为例,探讨如何使用Channel和Buffer来解决一个常见的问题,即半包与粘包

    问题产生实际场景

    让我们考虑一个实际场景:客户端与服务端建立了连接,客户端需要向服务端发送三个句子:I'm AomsirI love youDo you love me?。然而,由于计算机无法理解文本的含义,它在接收这些句子时并不知道何时结束每句话。为了解决这个问题,我们通常在每个句子的末尾添加换行符\n。这样,在解析数据时,服务端可以根据换行符来确定每个句子的结束。实际上,这也是网络通信中协议概念。
    在这里插入图片描述

    问题出现

    在上面的场景中,我们假设了一个常见的情况,其中客户端和服务端之间使用NIO中的Channel进行通信。服务端将从Channel中读取的数据放入ByteBuffer中。然而,在确定Buffer的大小时,我们面临一个挑战:

    设置一个过大的Buffer可能会导致资源浪费,而设置一个过小的Buffer则可能导致半包和粘包问题。

    半包和粘包是通信中常见的问题,通常在数据读取和解析过程中引发。举例来说,如果我们将Buffer大小设置为15,并且客户端发送的第一句话(包括换行符)只有12个字符,没有超过15,那么第二句话会被读入Buffer。但是第二句话只读取了Buffer的前几个字符,然后Buffer就满了。此时,Buffer中包含第一句完整和第二句的开头,这就是粘包。接着,Buffer继续从Channel中读取第二句的剩余部分和第三句的开头,这导致Buffer中包含第二句的结尾和第三句的开头,这就是半包。半包和粘包问题可能会导致我们在处理接收到的数据时遇到一些困难
    在这里插入图片描述

    问题解决

    显然,我们不能允许我们的程序出现半包和粘包问题,因此我们需要采取措施来解决这个问题。我们可以借助ByteBuffer的compact方法来解决这一挑战。解决思路是在每个句子的末尾添加换行符\n的基础上,遍历原始Buffer,在遇到\n时将其之前的数据通过循环方式放入名为target的Buffer中,然后进行输出。如果原始Buffer中只有一个\n,后续的循环将不会进入if条件,最终将剩余的部分压缩到原始Buffer的最开始,以便继续接收数据。

    需要注意的是,为了避免原始Buffer中出现两个\n(即两个完整的句子),target的Buffer大小不能随意设置。我们可以使用i + 1 - buffer.position来确定target的长度,因为在ByteBuffer.get(i)的过程中,position不会移动,只有在ByteBuffer.get()时才会使position不断前进,所以我们就可以在程序中动态的计算长度(也就是 position - i)之间的长度。

    还有一个需要注意的问题是,如果原始Buffer中没有\n,整个程序可能会陷入死循环。为了解决这种情况,我们可以在else部分采取适当的处理措施。然而,这个具体处理方法超出了本文的范围,因为后续的Netty框架已经为我们提供了解决半包和粘包问题的更全面的解决方案

    public class TestNIO10 {
        public static void main(String[] args) {
            ByteBuffer buffer = ByteBuffer.allocate(50);
    
            // 假装buffer从channel里面读取到了第一次数据
            buffer.put("Hi Aomsir\n I love y".getBytes());
            doLineSplit(buffer);
    
            // 假装buffer从channel里面读取到了第二次数据
            buffer.put("ou\nDo you like me?\n".getBytes());
            doLineSplit(buffer);
        }
    
    
        private static void doLineSplit(ByteBuffer buffer) {
            // 读模式,让程序从buffer里面读取数据
            buffer.flip();
    
            // 循环会将整个buffer的数据都读取一遍
            for (int i = 0; i < buffer.limit(); i++) {
                
                // 在找到一行完整数据以后没有直接结束循环是因为可能会出现两个\n的情况
                if (buffer.get(i) == '\n') {
    
                    // 以免出现一行里面有多个\n的情况
                    // 注意:get(i)不会导致position的变化
                    int length = i + 1 - buffer.position();
    
                    // buffer的大小不能写死,每个句子的大小不一样,所以要动态分配
                    ByteBuffer target = ByteBuffer.allocate(length);
    
                    // 从buffer里面读取数据写入target
                    for (int j = 0; j < length; j++) {
                        target.put(buffer.get());
                    }
    
                    // 截取工作完成,将target切换为读模式,然后读取数据
                    target.flip();
                    System.out.println("StandardCharsets.UTF_8.decode(target) = " + StandardCharsets.UTF_8.decode(target));
    
                    target.clear();
                }
            }
            // buffer切换写模式,将未读完的数据移到最前面(position-limit之间)
            buffer.compact();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    总结

    今天的文章介绍和解决了半包和粘包的区别,这部分需要对Channel和Buffer的读写有一定的基础,如果没有看明白就先看看前面的文章打好基础,为本篇和以后的文章打基础。

  • 相关阅读:
    【Java】List
    Postgresql源码(68)virtualxid锁的原理和应用场景
    GBase 8c V3.0.0数据类型——窗口函数
    ukb进不去了是怎么回事?
    尚硅谷Vue系列教程学习笔记(6)
    初识进程~
    StoneDB社区答疑第二期
    pinia状态管理器使用
    (Golang) 牛客 在线编程 Go语言入门
    基于 MinIO 部署单实例 Databend | 新手篇(1)
  • 原文地址:https://blog.csdn.net/qq_43266723/article/details/134318113