首页 > 其他分享 >【Netty】「优化进阶」(一)粘包半包问题及解决方案

【Netty】「优化进阶」(一)粘包半包问题及解决方案

时间:2023-07-13 15:31:54浏览次数:46  
标签:Netty 进阶 buffer ctx 粘包 发送 半包 数据包

前言

本篇博文是《从0到1学习 Netty》中进阶系列的第一篇博文,主要内容是介绍粘包半包出现的现象和原因,并结合应用案例来深入讲解多种解决方案,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;

粘包现象

粘包是指多个独立的数据包被粘合在一起发送,接收端无法区分每个数据包的边界。例如,发送端要发送三个数据包 A、B 和 C,但它们被粘合在一起发送了,接收端收到的数据可能是 AB 或 ABC 等,需要额外的处理才能区分出每个数据包。

比如,通过下述代码客户端将向服务端发送十次消息:

for (int i = 0; i < 10; i++) {  
    ByteBuf buffer = ctx.alloc().buffer();  
    buffer.writeBytes("sidiot.".getBytes());  
    ctx.writeAndFlush(buffer);  
}

但是服务端最终收到的结果却是如下所示:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_netty

按照理论来说,客户端发送了十次数据也就是十个包,服务端理应也收到十个包,但是,实际情况却是服务端只收到了一个包,并且十次数据都包含在其中。

半包现象

半包是指一个完整的数据包被拆分成了多个数据包进行发送,接收端只收到了部分数据包,无法还原完整的数据包。例如,发送端要发送一个数据包 D,但它被拆分成了两个数据包 D1 和 D2 进行发送,接收端只收到了 D1 或 D2,无法还原完整的数据包。

比如,通过下述代码客户端将向服务端一次性发送70字节的数据:

ByteBuf buffer = ctx.alloc().buffer();  
for (int i = 0; i < 10; i++) {  
    buffer.writeBytes("sidiot.".getBytes());  
}  
ctx.writeAndFlush(buffer);

服务端设置一个名为 SO_RCVBUF 的 TCP/IP 协议选项,SO_RCVBUF 是一个用于指定接收缓冲区大小的选项。接收缓冲区是操作系统内核用来存储接收到的数据的内存区域。当应用程序接收数据时,数据首先被写入到接收缓冲区中,然后应用程序再从缓冲区中读取数据进行处理。:

serverBootstrap.option(ChannelOption.SO_RCVBUF, 7);

在这行代码中,选项的值被设置为7,这意味着接收缓冲区的大小被限制为7字节,但这仅仅决定了 Netty 读取的最小单位,Netty 实际每次读取的字节大小一般是它的整数倍。并且这个值非常小,在实际情况下并不会使用这个选项设置这么小的缓冲区大小。如果接收缓冲区太小,那么可能会导致网络拥塞、丢包等问题。

运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_后端_02

从上述结果中,我们可以获知原先的70字节的数据包被拆分成了两个数据包,其大小分别为14字节和56字节,也都恰好是7的倍数。

分析原因

粘包半包是因为数据在网络传输过程中被拆分成多个数据块进行传输,但是接收端无法确定每个数据块的大小和边界,从而导致的问题。

具体来说,粘包现象发生是因为发送方将两个或多个数据包连续地发送到网络中,而接收方一次性读取了多个数据包,从而把它们看作一个数据包处理,造成了粘包的现象。

举个例子,假设发送方一个完整报文的大小为52字节,接收方的滑动窗口大小为256字节,由于接收方处理不及时且滑动窗口空闲大小足够大,这52字节的报文就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文,就会发生粘包现象。

同时,如果在 Netty 中接收方将 ByteBuf 设置过大(默认为1024)以及 Nagle 算法都会造成粘包现象。

半包现象则是指发送方将一个数据包分割成多个数据块进行传输,在接收方接收到部分数据块时就开始处理数据,从而只处理了部分数据信息,无法还原完整的数据包。

举个例子,假设接收方的滑动窗口大小只剩下128字节,而发送方一个完整报文的大小为256字节,这时,接收方的滑动窗口无法容纳发送方的全部报文,发送方只能先发送前128字节,等待 ACK 确认应答后才能发送剩余部分,这就造成了半包现象。

同时,如果在 Netty 中接收方将 ByteBuf 设置过小以及当发送的数据超过 MSS 的大小限制后,系统都会将数据切分发送,这就会造成半包现象。

这些问题通常由底层协议不正确或者网络拥塞等原因引起。为了解决这些问题,可以采用各种方法,如使用固定长度的数据包、在数据包中添加长度头等方式进行控制。

发生粘包与半包现象的本质是TCP 是流式协议,消息无边界,想要进一步了解,可以阅读博主往期的博文:

解决方案

短链接

短链接是指客户端和服务器之间只建立一次连接,发送完数据后立即断开连接的通信方式。

在短链接通信中,每次发送的数据都与一个完整的消息边界对应,不需要使用滑动窗口等技术来缓冲数据,因此不会出现粘包现象。

然而,如果一次性发送的数据过多,接收方无法一次性容纳全部数据,仍然可能会出现半包现象。因此,短链接并不能完全解决半包问题

修改客户端的代码:

// 短链接:每次发送完毕就断开连接
@Override  
public void channelActive(ChannelHandlerContext ctx) throws Exception {  
    log.debug("sending...");  
    ByteBuf buffer = ctx.alloc().buffer();  
    buffer.writeBytes("sidiot.".getBytes());  
    ctx.writeAndFlush(buffer);  
    ctx.channel().close();  
}

然后将全部代码封装成一个方法,这里取名为 send(),并运行十次:

public static void main(String[] args) {  
    for (int i = 0; i < 10; i++) {  
        send();  
    }  
}  
  
private static void send() { ... }

运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_netty_03

定长解码器

定长解码器指的是客户端和服务器之间进行数据传输时,双方事先约定一个最大长度。

客户端在发送数据时必须保证每次发送的数据长度都不会超过该最大长度,如果发送的数据长度不足,则需要进行补齐。

当服务器接收到数据时,会按照约定的最大长度进行拆分,即使在传输过程中出现了粘包的情况,也可以通过定长解码器将数据正确地拆分开来。这样可以保证数据在传输过程中的完整性和准确性,防止数据因为长度问题而被截断或拼接错误。

服务端需要用到 FixedLengthFrameDecoder 函数对数据进行定长解码,代码如下:

ch.pipeline().addLast(new FixedLengthFrameDecoder(10));

FixedLengthFrameDecoder 是一个解码器,它的作用是将接收到的 ByteBuf 按照固定长度进行拆分,并将每个拆分出来的数据封装成一个新的 ByteBuf 对象。这样,无论原始数据包的长度如何,都可以保证每个新的数据包的长度是一致的。

举个例子,假设我们想要接收长度为10的固定长度数据包。那么当接收到一个长度为20的数据包时,FixedLengthFrameDecoder 会将其拆分成两个长度为10的数据包。而当接收到一个长度为5的数据包时,FixedLengthFrameDecoder 会暂存这个数据包,直到接收到下一个数据包,然后将这两个数据包拼接在一起再进行拆分。总之,只要接收到的数据包长度不足固定长度,FixedLengthFrameDecoder 就会等待更多数据的到来,直到达到固定长度为止。

根据上述例子,我们先测试拆分数据包,修改客户端代码如下:

@Override  
public void channelActive(ChannelHandlerContext ctx) throws Exception {  
    for (int i = 0; i < 5; i++) {  
        ByteBuf buffer = ctx.alloc().buffer();  
        buffer.writeBytes("sidiot.".getBytes());  
        ctx.writeAndFlush(buffer);  
    }  
}

运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_java_04

根据上述例子,我们再测试合并数据包,修改客户端代码如下:

@Override  
public void channelActive(ChannelHandlerContext ctx) throws Exception {  
    for (int i = 0; i < 5; i++) {  
        ByteBuf buffer = ctx.alloc().buffer();  
        buffer.writeBytes("sidiot.".getBytes());  
        ctx.writeAndFlush(buffer);  
    }  
    Thread.sleep(1000);  
    ByteBuf buffer = ctx.alloc().buffer();  
    buffer.writeBytes("Next......".getBytes());  
    ctx.writeAndFlush(buffer);  
}

运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_netty_05


行解码器

行解码器指的是通过分隔符对数据进行拆分。

客户端在每个数据包的末尾添加一个特定的分隔符,比如回车换行符 \r\n,表示该数据包已经结束;而服务端则根据分隔符将接收到的数据进行拆分,以此恢复原始的数据包。

需要注意的是,分隔符的选取应当与数据本身的格式相适应,并且需要考虑到特殊字符的转义等问题,以避免出现误解析的情况。同时,行解码器只适用于传输文本数据,对于二进制数据需要采用其他的解决方案。

服务端通过 LineBasedFrameDecoder 函数来拆分以换行符 \n 为分隔符的数据,代码如下:

ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

LineBasedFrameDecoder 是一个解码器,它的作用是将基于行的文本协议中的数据流分成一系列的帧。它会扫描缓冲区中的字节,直到找到行结束符号(例如 \n\r\n),然后将这一段数据作为一个完整的帧返回。使用 LineBasedFrameDecoder 解码器时,每个帧都被视为一个字符串对象,其中包含了行结束符以前的所有数据。

客户端代码如下:

public static StringBuilder makeString(char ch, int len) {  
    StringBuilder sb = new StringBuilder(len + 2);  
    for (int i = 0; i < len; i++) {  
        sb.append(ch);  
    }  
    sb.append("\n");  
    return sb;  
}

@Override  
public void channelActive(ChannelHandlerContext ctx) {  
    ByteBuf buffer = ctx.alloc().buffer();  
    char ch = 's';  
    Random r = new Random();  
    for (int i = 0; i < 5; i++) {  
        StringBuilder sb = makeString(ch, r.nextInt(52)+1);  
        ch++;  
        buffer.writeBytes(sb.toString().getBytes());  
    }  
    ctx.writeAndFlush(buffer);  
}

客户端运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_netty_06

服务端运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_java_07

当然,我们也可以使用函数 DelimiterBasedFrameDecoder 来自定义分割符,代码如下所示:

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, false, Unpooled.wrappedBuffer("\\s".getBytes())));

客户端运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_netty_08

服务端运行结果:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_netty_09

如果想要在结果中显示分割符,将 true 改为 false 就可以了,结果如下:

【Netty】「优化进阶」(一)粘包半包问题及解决方案_后端_10


长度字段解码器

【Netty】「优化进阶」(一)粘包半包问题及解决方案_后端_11

详见博主的另一篇博文 浅谈 LengthFieldBasedFrameDecoder:如何实现可靠的消息分割?

后记

虽然粘包半包问题是一个非常普遍的现象,但是我们可以通过多种方式来解决这个问题。其中最常见的方法是使用消息长度分隔符来标记每个消息的边界,以确保每个消息都可以独立处理。此外,我们还可以使用固定长度的消息来防止粘包和半包问题的发生。最后,我们还可以通过自定义协议来解决粘包半包问题,例如使用特殊字符来分隔不同的消息。

需要注意的是,不同的应用场景可能需要不同的解决方案。因此,在实际应用中,我们应该根据具体情况选择最适合的解决方案,以确保网络通信的稳定和可靠。

以上就是 粘包半包问题及解决方案 的所有内容了,希望本篇博文对大家有所帮助!

参考:


标签:Netty,进阶,buffer,ctx,粘包,发送,半包,数据包
From: https://blog.51cto.com/sidiot/6692824

相关文章

  • 【Netty】「优化进阶」(二)浅谈 LengthFieldBasedFrameDecoder:如何实现可靠的消息分割?
    前言本篇博文是《从0到1学习Netty》中进阶系列的第二篇博文,主要内容是通过不同的应用案例来了解LengthFieldBasedFrameDecoder是如何处理不同的消息,实现自动分割,往期系列文章请访问博主的Netty专栏,博文中的所有代码全部收集在博主的GitHub仓库中;介绍LengthFieldBasedFrameDe......
  • Git小白到老鸟的进阶之路
    小白:师兄,师兄,上次你教我的操作,我傻乎乎的执行了一遍,可是那个Git究竟是什么那?师兄:小白莫慌,Git就是一种版本控制,小白,你平时写论文,是不是也按日期保存成许多的版本那。小白:对呀,对呀。师兄:那开发项目的时候,每个人开发的部分都不一样,需要记录多个版本,这个就是我们伟大的Git做得。小白:师......
  • 文本格式进阶
    day1描述型列表链接 注意的点:一个术语可以同时有多个描述,如:<dl><dt>旁白</dt><dd>戏剧中,为渲染幽默或戏剧性效果而进行的场景之外的补充注释念白,只面向观众,内容一般都是角色的感受、想法、以及一些背景信息等。</dd><dd>写作中,指与当前主题相关的一段......
  • [第三章 web进阶]Python里的SSRF
    一、运行靶机 发现没有有效界面信息,这个时候查看靶机说明信息,在说明信息里面明确提到访问容器内部的8000端口和urlpath/api/internal/secret即可获取flag 二、根据提示信息访问url由于提示信息中提到urlpath,则可以尝试设置参数名为url?url=http://127.0.0.1:8000/......
  • SpringBoot中使用Netty开发WebSocket服务-netty-websocket-spring-boot-starter开源项
    场景SpringBoot+Vue整合WebSocket实现前后端消息推送:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/114392573SpringCloud(若依微服务版为例)集成WebSocket实现前后端的消息推送:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/114480731若依前后......
  • HCIP-进阶实验07-高可靠性园区网部署
    HCIP-进阶实验07-高可靠性园区网部署1实验需求1.1实验拓扑1.2实验环境说明设备接口IP地址备注AR1G0/0/0172.16.1.1/24G0/0/1172.16.2.1/24loopback0202.22.2.2/24ISPSW1VLANIF10192.168.1.254/24VLANIF20192.168.2.253/24......
  • 【Netty】「源码解析」(三)设置连接超时:深入分析 ChannelFuture.sync() 的执行过程
    前言本篇博文是《从0到1学习Netty》中源码系列的第三篇博文,主要内容是深入分析连接超时的实现原理,包括了connect方法的源码解析和ChannelFuture.sync()执行过程的解析。,往期系列文章请访问博主的Netty专栏,博文中的所有代码全部收集在博主的GitHub仓库中;介绍在实际应用中,当......
  • ssh进阶
    1、ssh客户端工具查看参数和帮助方法==ssh--help====manssh==常见参数windowslinuxmacos提供的ssh命令,会有些区别,查看帮助后使用即可。linux下ssh远程登录简单用法[root@web-7~]#[email protected]@10.0.0.41'spassword:Lastlogin:FriApr......
  • Hadoop on k8s 快速部署进阶精简篇
    目录一、概述二、快速部署步骤如下1)安装git2)部署mysql3)创建存储目录(所有节点)4)下载hadoop-on-k8s部署包5)修改hadoopconfigmap6)修改hiveconfigmap(MySQL配置)7)安装helm8)开始部署9)测试验证一、概述前面一篇文章已经很详细的介绍了Hadooponk8s部署了,这里主要针对部署时可......
  • 面试进阶齐飞!Github一天万赞的阿里Java系统性能优化有多牛?
    前两天在知乎上看到一个问答,说的是:一个Java程序员具备什么样的素质和能力才可以称得上高级工程师?这个问题也引发了我的一些思考,可能很多人会说,“作为高级工程师,基础得过硬、得熟练掌握一门编程语言、至少看过一个优秀开源项目的源代码、有过高并发/性能优化的工作经验、沟通能力......