19.Netty源码之粘包/拆包

news2024/12/24 9:42:07

本节课开始我们将学习 Netty 通信过程中的编解码技术。

编解码技术这是实现网络通信的基础,让我们可以定义任何满足业务需求的应用层协议。

在网络编程中,我们经常会使用各种网络传输协议,其中 TCP 是最常用的协议。

我们首先需要了解的是 TCP 最基本的拆包/粘包问题以及常用的解决方案,才能更好地理解 Netty 的编解码框架。

为什么会出现拆包/粘包现象呢?

UDP没有拆包半包

提醒:UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收, 所以无粘包、半包问题。

☆为什么TCP有拆包/粘包

根本原因:TCP传输协议是面向流的,是流式协议,没有数据包界限。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。因此就有了拆包和粘包。

在网络通信的过程中,每次可以发送的数据包大小受多种因素限制,如 MTU 传输单元大小、MSS 最大分段大小、滑动窗口等。

如果一次传输的网络包数据大小超过传输单元大小,那么我们的数据可能会拆分为多个数据包发送出去。

如果每次请求的网络包数据都很小,一共请求了 10000 次,TCP 并不会分别发送 10000 次。

因为 TCP 采用的 Nagle 算法对此作出了优化。

tcp为什么会粘包

粘包的根本原因: TCP 传输协议是面向流的,是流式协议,没有数据包界限。 粘包的主要原因: • 发送方每次写入数据小于套接字缓冲区大小,需要多次发送 • 接收方读取套接字缓冲区数据不够及时 ​ 半包的主要原因: • 发送方写入数据大于套接字缓冲区大小 • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包 ​ 换个角度看: • 收发 一个发送可能被多次接收,多个发送可能被一次接收 • 传输 一个发送可能占用多个传输包,多个发送可能公用一个传输包 ​

MTU 最大传输单元和 MSS 最大分段大小

MTU(Maxitum Transmission Unit) 是链路层一次最大传输数据的大小。MTU 一般来说大小为 1500 byte。

MSS(Maximum Segement Size)是指 TCP 最大报文段长度,它是传输层一次发送最大数据的大小。MSS一般大小是1460

如下图所示,MTU 和 MSS 一般的计算关系为:MSS = MTU - IP 首部 - TCP首部,如果 MSS + TCP 首部 + IP 首部 > MTU,那么数据包将会被拆分为多个发送。这就是拆包现象。

image.png

滑动窗口:限制发送方单次发送数据大小

滑动窗口是 TCP 传输层用于流量控制的一种有效措施,也被称为通告窗口

滑动窗口是数据接收方设置的窗口大小,数据接收方会把窗口大小告诉发送方,以此限制发送方每次发送数据的大小,从而达到流量控制的目的。

这样数据发送方不需要每发送一组数据就阻塞等待接收方确认,允许发送方同时发送多个数据分组,每次发送的数据都会被限制在窗口大小内。

由此可见,滑动窗口可以大幅度提升网络吞吐量。

那么 TCP 报文是怎么确保数据包按次序到达且不丢数据呢?

首先,所有的数据帧都是有编号的,TCP 并不会为每个报文段都回复 ACK 响应,它会对多个报文段回复一次 ACK。假设有三个报文段 A、B、C,发送方先发送了B、C,接收方则必须等待 A 报文段到达,如果一定时间内仍未等到 A 报文段,那么 B、C 也会被丢弃,发送方会发起重试。如果已接收到 A 报文段,那么将会回复发送方一次 ACK 确认。

Nagle 算法:批量发送

Nagle 算法于 1984 年被福特航空和通信公司定义为 TCP/IP 拥塞控制方法。它主要用于解决频繁发送小数据包而带来的网络拥塞问题。试想如果每次需要发送的数据只有 1 字节,加上 20 个字节 IP Header 和 20 个字节 TCP Header,每次发送的数据包大小为 41 字节,但是只有 1 字节是有效信息,这就造成了非常大的浪费。Nagle 算法可以理解为批量发送,也是我们平时编程中经常用到的优化思路,它是在数据未得到确认之前先写入缓冲区,等待数据确认或者缓冲区积攒到一定大小再把数据包发送出去。

Linux 在默认情况下是开启 Nagle 算法的,在大量小数据包的场景下可以有效地降低网络开销。 但如果你的业务场景每次发送的数据都需要获得及时响应,那么 Nagle 算法就不能满足你的需求了,因为 Nagle 算法会有一定的数据延迟。

你可以通过 Linux 提供的 TCP_NODELAY 参数禁用 Nagle 算法。

Netty 中为了使数据传输延迟最小化,就默认禁用了 Nagle 算法,这一点与 Linux 操作系统的默认行为是相反的。

拆包/粘包解决方案:消息边界

image.png

在客户端和服务端通信的过程中,服务端一次读到的数据大小是不确定的。如上图所示,拆包/粘包可能会出现以下五种情况:

  • 服务端恰巧读到了两个完整的数据包 A 和 B,没有出现拆包/粘包问题;
  • 服务端接收到 A 和 B 粘在一起的数据包,服务端需要解析出 A 和 B;
  • 服务端收到完整的 A 和 B 的一部分数据包 B-1,服务端需要解析出完整的 A,并等待读取完整的 B 数据包;
  • 服务端接收到 A 的一部分数据包 A-1,此时需要等待接收到完整的 A 数据包;
  • 数据包 A 较大,服务端需要多次才可以接收完数据包 A。

由于拆包/粘包问题的存在,数据接收方很难界定数据包的边界在哪里,很难识别出一个完整的数据包。所以需要提供一种机制来识别数据包的界限,这也是解决拆包/粘包的唯一方法:定义应用层的通信协议

下面我们一起看下主流协议的解决方案。

消息长度固定

每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。

当发送方发送的数据小于固定长度时,则需要将空位补齐再发送。

``` +----+------+------+---+----+

| AB | CDEF | GHIJ | K | LM |

+----+------+------+---+----+ ```

假设我们的固定长度为 4 字节,那么如上所示的 5 条数据一共需要发送 4 个报文:

+------+------+------+------+ | ABCD | EFGH | IJKL | M000 | +------+------+------+------+

长度如何设置是问题

消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。

特定分隔符:redis

既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符 \n 按行解析,即可得到 AB、CDEF、GHIJ、K、LM 五条原始报文。

``` +-------------------------+

| AB\nCDEF\nGHIJ\nK\nLM\n |

+-------------------------+ ```

由于在发送报文时尾部需要添加特定分隔符,所以对于分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。

比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。

base64编码中的64 指代的是64个字符

A-Z a-z 0-9 + / 26 + 26 + 10 + 2 = 64

特定分隔符法在消息协议足够简单的场景下比较高效,例如大名鼎鼎的 Redis 在通信过程中采用的就是换行分隔符。

消息长度+变长消息:dubbo&rocketMQ

消息头 消息体 +--------+----------+ | Length | Content | +--------+----------+

消息长度 + 消息内容是项目开发中最常用的一种协议,如上展示了该协议的基本格式。

消息头中存放消息的总长度,例如使用 4 字节的 int 值记录消息的长度,消息体实际的二进制的字节数据。

接收方在解析数据时,首先读取消息头的长度字段 Len,然后紧接着读取长度为 Len 的字节数据,该数据即判定为一个完整的数据报文。

依然以上述提到的原始字节数据为例,使用该协议进行编码后的结果如下所示:

+-----+-------+-------+----+-----+ | 2AB | 4CDEF | 4GHIJ | 1K | 2LM | +-----+-------+-------+----+-----+

消息长度 + 消息内容的使用方式非常灵活,且不会存在消息定长法和特定分隔符法的明显缺陷。当然在消息头中不仅只限于存放消息的长度,而且可以自定义其他必要的扩展字段,例如消息版本、算法类型等。

使用JSON

每种都不同,例如JSON 可以看{}是否应已经成对.

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

| 方式 | 解码 | 编码 | | --------------- | ---------------------------- | -------------------- | | 固定长度 | FixedLengthFrameDecoder | 简单 | | 分割符 | DelimiterBasedFrameDecoder | 简单 | | 固定长度字段存个内容的长度信息 | LengthFieldBasedFrameDecoder | LengthFieldPrepender | | JSON | 可以看{}是否应已经成对. | 简单 |

总结

本节课我们详细讨论了 TCP 中的拆包/粘包问题,以及如何通过应用层的通信协议来解决拆包/粘包问题。

其中基于消息长度 + 消息内容的变长协议是项目开发中最常用的一种方法,需要我们重点掌握,例如开源中间件 Dubbo、RocketMQ 等都基于该方法自定义了自己的通信协议。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/826018.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手机pdf转换成word免费版?看看这几个转换方法

手机pdf转换成word免费版?在当今信息化的时代,PDF文档已经成为公文交流、资料分享、学术论文等领域中最常用的文件格式之一。然而,PDF文档的固化特性也使其在文本编辑、内容修改等方面存在不便。因此,将PDF文档转换为Word文档已成…

离线部署 python 环境

本机 启动命令行 保存本机python安装的库 pip3 freeze > packet.txt。这样可以在当前目录下生成packet.txt。注意不要随意删除其中行,除非你清楚依赖项 获取库文件,pip3 download -r packet.txt -d ./pip_packages。从当前环境的网络中下载packet.…

springboot项目如何自动重启(使用Devtools检测修改并自动重启springboot)

1. 问题: 我们在项目开发阶段,可能经常会修改代码,修改完后就要重启Spring Boot。经常手动停止再启动,比较麻烦。 所以我们引入一个Spring Boot提供的开发工具; 只要源码或配置文件发生修改,Spring Boot应用…

突破游戏行业天花板,“技术外溢”成趋势

文 | 螳螂观察 作者 | 余一 受游戏版号发放的“放缓”、人口结构的调整,过去两年国内游戏行业过得并不算好。前不久据相关机构发布的数据显示,2022年中国游戏市场实际销售收入2658.84亿元,同比减少306.29亿元,下降10.33%。且游戏…

Maven-学习笔记

文章目录 1. Maven简介2.Maven安装和基础配置3.Maven基本使用4.Maven坐标介绍 1. Maven简介 概念 Maven是专门用于管理和构建Java项目的工具 主要功能有: 提供了一套标准化的项目结构提供了一套标准化的构建流程(编译,测试,打包,…

Vulmap和struts-scan联合使用

介绍 Vulmap 是一款 web 漏洞扫描和验证工具, 可对 webapps 进行漏洞扫描, 并且具备漏洞利用功能, 目前支持的 webapps 包括 activemq, flink, shiro, solr, struts2, tomcat, unomi, drupal, elasticsearch, fastjson, jenkins, nexus, weblogic, jboss, spring, thinkphp Vul…

尝试多数据表 sqlite

C 唯一值得骄傲的地方就是 通过指针来回寻址 😂 提高使用的灵活性 小脚本buff 加成

【基础理论】了解点过程

Maximum tsunami wave height generated by the 16 Sept. 2015 Chile earthquake, from the International Tsunami Information Center. Posted by Austin Elliott 一、说明 在这个世界上,会发生许多事件,其趋势可能遵循一种模式。在这篇博客中&#…

手把手教你制作春节微信集卡抽奖活动

春节是中国传统的重要节日,商家们纷纷希望能够通过一系列的营销活动吸引更多的用户参与。而微信集福字游戏活动成为了一个赢得春节营销的关键。在本文中,我们将介绍如何通过第三方平台/工具,如乔拓云平台,来注册并登录后台&#x…

漏洞复现--原型链污染、沙箱逃逸绕过

目录 一、原型链污染 1.prototype和__proto__区别??? 2.原型链污染是什么??? 3.哪些情况原型链会被污染??? 4.原型链污染例题 二、沙箱逃逸绕过 1.如何实现沙箱逃逸…

nsq的目录锁,源码分析

文章目录 前言 nsqd启动加锁流程及源码分析 总结 前言 前面几篇博客我们讲了nsq是什么,nsq的安装等,大家想过下面这样的问题没有,就是 问题:一个目录下能启动多个nsqd进程吗? 答案:不能 问题&#xff1…

Maven项目解决cannot resolve plugin maven-deploy-plugin:2.7

导入maven项目后,编辑的时候提示一些插件加载失败!大概率是你的网络有问题,插件下载失败。 如下图:(网络突然好了,我想截图但是没有复现,用网上找到的截图代替,明白意思就行&#x…

Docker Compose 安装与使用(常用指令)

一、简介 Docker Compose 是一个编排多容器分布式部署的工具,提供命令集管理容器化应用的完整开发周期,包括服务构建、启动和停止。使用步骤:1. 利用 Dockerfile 定义运行环境镜像 2. 使用 docker-compose.yml 家义组成应用的各服务 3. 运行 …

python制作超高难度走迷宫游戏,你要来挑战嘛~(赶紧收藏)

前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 走迷宫,是一项充满智慧的挑战~ 作为经常刷短视频的我们,见识过不少迷宫小游戏 当然印象深刻的当然是小动物走迷宫 这里有几组挑战走迷宫的小可爱。先来看看吧! (1&#xff…

Java Maven 构建项目里面有个聚合的概念

Java 项目里面有个聚合的概念,它没有.net里面解决方案(solution)的能力,可以统一的编译项目下的所有包,或设置统一的打包路径,使用maven编译后的产物也不会像.net那样编译到当前项目的bin文件夹下面,而是统一的生成到配…

MySQL 其他数据库日志

我们了解数据库事务时,知道两种日志:重做日志,回滚日志。 对于线上数据库应用系统,突然遭遇 数据库宕机 怎么办?在这种情况下,定位宕机的原因 就非常关键。我们可以查看数据库的 错误日志。因为日志中记录…

保修管理系统

保修管理系统密切关注IT资产的保修信息,以便在发生任何故障或损坏时,供应商可以及时更换和修复任何IT硬件。自动化保修管理软件跟踪各种供应商提供的维修和服务期限的有效性,以便任何硬件都可以在保修到期之前升级,从而降低成本并…

企业项目中md-loader项目组件文档实现

背景需求: 随着业务增多,公共组件变多,无法直观知道其中的方法使用和业务场景,轻量级不需要重新新建一个项目 技术基础: 1.仿造element-ui md-loader 需求场景: 当前项目公共组件比较多,需…

在AndroidStudio中如何查看Gradle的版本

以Android Studio Giraffe | 2022.3.1为例 File -> Project Structure -> Project Android Gradle Plugin Version - Android Gradle插件版本号 Gradle Version - Gradle的版本号 Gradle 版本 (gradle version): Gradle 是一种通用的构建工具,用于构建各种类…

蓝牙、GPS定位学习

启动状态(APP) 冷启动 指在启动应用时,后台没有应用的进程或者进程被杀死的情况下,系统会重新创建一个新的进程,并按照一定的顺序创建和初始化Application类和MainActivity类,最后显示在界面上。这个过程需…