Netty-TCP服务端粘包、拆包问题(两种格式)
前言
最近公司搞了个小业务,需要使用TCP协议,我这边负责服务端。客户端是某个设备,客户端传参格式、包头包尾等都是固定的,不可改变,而且还有个蓝牙传感器,透传数据到这个设备,然后通过这个设备发送到服务端,数据格式也是不可变的。于是,相当于我这个TCP客户端会发送两种不同格式、不同长度的报文,且一种是ASCII 一种是HEX。
正常单发肯定是没问题的,但是,如果你业务卡顿,那么一定会有粘包、拆包的问题
请看:我在这里打个断点,模拟阻塞

然后一起发消息

放开断点

或者,睡个五秒

发现数据一起过来了,这就是粘包
还有种情况,如下


粘包了 但是下一次的数据包部分字节出现在了上次的数据包的尾部,把整个数据包给分开了,这种就是拆包(大概就是整个效果)
总结就是:
粘包,就是将多个小的包封装成一个大的包进行发送。(多次发送的数据到了服务端合并成了一个数据包)
拆包,即是将一个超过缓冲区可用大小的包拆分成多个包进行发送。(一个的数据包到了服务端变得不完整了,哪怕是粘包都没有完整的一段)
那么如何解决呢?
本篇就以netty来简单说下,第一次用,很多不足,希望各位大佬指点!
直接上代码:
先来个maven:
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.68.Final</version></dependency>
1. NettyTcpServerConfig TCP服务配置类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.annotation.PreDestroy;/*** @title: NettyTCPConfig* @description:* @date: 2024/10/14* @author: zwh* @copyright: Copyright (c) 2024* @version: 1.0*/
@Configuration
public class NettyTcpServerConfig {private final EventLoopGroup bossGroup = new NioEventLoopGroup();private final EventLoopGroup workerGroup = new NioEventLoopGroup();private ChannelFuture channelFuture;@Beanpublic ServerBootstrap serverBootstrap(NettyTcpServerHandler nettyTcpServerHandler) {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {// 添加自定义解码器ch.pipeline().addLast(new MyCustomDecoder());// 自带的解码器 上面数据拆包分包就是用的这个自带的// ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new StringEncoder());ch.pipeline().addLast(nettyTcpServerHandler);}}).childOption(ChannelOption.TCP_NODELAY, true);return bootstrap;}public void startServer(int port) throws Exception {channelFuture = serverBootstrap(new NettyTcpServerHandler()).bind(port).sync();System.out.println("服务器已启动,监听端口: " + port);channelFuture.channel().closeFuture().sync();}@PreDestroypublic void shutdown() {workerGroup.shutdownGracefully();bossGroup.shutdownGracefully();}
}
2. NettyTcpServer TCP服务端启动入口
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;/*** @title: NettyTcpServer* @description:* @date: 2024/10/14* @author: zwh* @copyright: Copyright (c) 2024* @version: 1.0*/@Component
public class NettyTcpServer implements CommandLineRunner {// 默认10067 可配置@Value("${nettyTcp.server.port:10067}")private int nettyTcpServerPort;private final NettyTcpServerConfig nettyTCPConfig;public NettyTcpServer(NettyTcpServerConfig nettyTcpServerConfig) {this.nettyTCPConfig = nettyTcpServerConfig;}@Overridepublic void run(String... args) throws Exception {nettyTCPConfig.startServer(nettyTcpServerPort);}
}
3. NettyTcpServerHandler 消息接收及回声(响应)处理
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.springframework.stereotype.Component;import java.net.InetSocketAddress;/*** @title: NettyTcpServerHandler* @description:* @date: 2024/10/14* @author: zwh* @copyright: Copyright (c) 2024* @version: 1.0*//** * @ChannelHandler.Sharable注解表示一个ChannelHandler实例可以被添加到多个ChannelPipeline中,并且该实例是线程安全的。 * 这意味着,如果一个ChannelHandler被标记为@Sharable,那么它可以在不同的ChannelPipeline中被共享使用,* 而不会出现竞争条件或线程安全问题。* */
@ChannelHandler.Sharable
@Component
public class NettyTcpServerHandler extends SimpleChannelInboundHandler<String> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println("接收到消息: " + msg);try {Thread.sleep(5000L);} catch (InterruptedException e) {throw new RuntimeException(e);}InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();String clientIp = remoteAddress.getAddress().getHostAddress();int clientPort = remoteAddress.getPort();System.out.println("来自客户端 (" + clientIp + ":" + clientPort + ") 的消息: " + msg);// 可根据需要发送响应String response = "Message processed: " + msg;ctx.writeAndFlush(response + "\r\n");
/*System.out.println("来自客户端 (" + clientIp + ":" + clientPort + ") 的消息: " + msg);if (msg.contains("重要")) {String responseMessage = "接收到重要数据: " + msg;ctx.writeAndFlush(responseMessage);System.out.println("发送响应到客户端 (" + clientIp + ":" + clientPort + "): " + responseMessage);} else {System.out.println("收到不重要数据,未发送响应。");}*/}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}
}
4. MyCustomDecoder 自定义解码器
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.springframework.stereotype.Component;import java.nio.charset.StandardCharsets;
import java.util.List;/*** @title: MyMessageDecoder* @description:* @date: 2024/10/15* @author: zwh* @copyright: Copyright (c) 2024* @version: 1.0*/
@Component
public class MyCustomDecoder extends ByteToMessageDecoder{@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {while (in.isReadable()) {// ASCII 消息处理(第一个字节是$)if (in.getByte(in.readerIndex()) == '$') {// 查找结束符 '*'int endIndex = in.indexOf(in.readerIndex(), in.writerIndex(), (byte) '*');if (endIndex == -1) {// 还没有找到结束符,等待更多数据 这是有结束位的break;}// 读取完整的 ASCII 消息ByteBuf messageBuffer = in.readBytes(endIndex + 1 - in.readerIndex()); // 包括结束符String message = messageBuffer.toString(StandardCharsets.US_ASCII);out.add(message+decToHex(calculateBcc(message)));}// 十六进制消息处理(第一个字节是0x2B)else if (in.getByte(0) == (byte) 0x2B) {// 读取原始的十六进制数据// 创建一个新的 ByteBuf 来保存 15 个字节// 读取15个字节 另一种格式就是15个字节 然后读取后,原始的 ByteBuf 中的数据会被更新 将其标记为已读取下次就读不到了ByteBuf byteBuf = in.readBytes(15);// 将 ByteBuf 中的数据转换为十六进制字符串StringBuilder hexMessage = new StringBuilder();for (int i = 0; i < byteBuf.readableBytes(); i++) {byte b = byteBuf.getByte(i);hexMessage.append(String.format("%02X ", b));}// 将十六进制消息发送到下一个处理器out.add(hexMessage.toString().trim());// 释放 ByteBuf,避免内存泄漏byteBuf.release();}else {in.skipBytes(1); // 跳过无效字节}}}/*** 计算给定数据的 BCC 校验值** @param data 输入的字节数组* @return BCC 校验值*/public static byte calculateBcc(byte[] data) {byte bcc = 0;for (byte b : data) {bcc ^= b; // 使用异或运算计算 BCC}return bcc;}/*** 计算给定字符串中 $ 和 * 之间的 BCC 校验值** @param input 输入的字符串* @return BCC 校验值*/public static byte calculateBcc(String input) {byte bcc = 0;// 找到 $ 和 * 的位置int start = input.indexOf('$') + 1; // 从 $ 后开始int end = input.indexOf('*');// 确保找到 $ 和 * 的位置if (start > 0 && end > start) {String data = input.substring(start, end); // 提取 $ 和 * 之间的部分// 计算 BCCfor (char c : data.toCharArray()) {bcc ^= c; // 使用异或运算计算 BCC}}return bcc;}/*** 十进制转HEX(16进制)* @date 2024/10/22 15:22* @return {@link }* @author zwh*/public static String decToHex(int dec) {return Integer.toHexString(dec).toUpperCase();}/*** 十六进制转HEX(10进制)* @date 2024/10/22 15:22* @return {@link }* @author zwh*/public static Integer hexToDec(String hexValue) {return Integer.parseInt(hexValue, 16);}public static void main(String[] args) {decToHex(107);}
}
这个自定义解码器就看需要处理的数据类型了。我这里是两种数据:
某蓝牙传感器:$TB,6300,D702,4700,,84C2E4DCEAAD,*6B ($ 固定头 * 结束符,ASCII)
6B是 BCC异或校验,取 $ 和 * 之间所有值 (包括逗号)的异或校验 参考BCC测试
I/O控制的器:2B 50 00 00 00 09 11 44 00 20 00 01 02 00 00 15个字节(HEX)
以这两种为示例来测试我们自定义的解码是否正确,如果需要别的数据自行修改开头和结尾以及长度啥的
Let's give it a try
就直接按照我们开头说的那种方法看看效果:

让我们换另外的格式:

两种格式混合:

没有再出现拆包、粘包现象,但是要注意一点发送的数据的格式一定要是我们预定好的。
比如:$TB,6300,D702,4700,,84C2E4DCEAAD,*6B 这个数据要是ASCII;
2B 50 00 00 00 09 11 44 00 20 00 01 02 00 00 这个数据要是HEX;不然我们自定义解码器的规则就对不上了。
如果要复现开头的,就很简单,换上内置的解码器就行

当然,以上问题 UDP不会出现!因为UDP是一种面向报文的协议,每个UDP段都是一条消息,所以不会出现粘包、拆包问题;TCP是面向流的,不知道数据的界限,会把构成整条消息的数据段排序完成后才呈现在内核缓冲区,容易造成拆包、粘包问题。
用哪种协议看自己的需求,实时性高的优先UDP,数据可靠性优先TCP
另外附上文中提到的网络调试助手(NetAssist)
网络调试助手
放资源了,0积分,但是不知道能不能审核过,如果被吞了 直接百度搜索就行能搜到

---------------
欢迎大佬指出问题!
end

相关文章:
Netty-TCP服务端粘包、拆包问题(两种格式)
前言 最近公司搞了个小业务,需要使用TCP协议,我这边负责服务端。客户端是某个设备,客户端传参格式、包头包尾等都是固定的,不可改变,而且还有个蓝牙传感器,透传数据到这个设备,然后通过这个设备…...
centos安装指定版本的jenkins
打开jenkins镜像包官网,找到自己想要安装的版本,官网地址:https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable 下载指定版本安装包: wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/jenkins-2.452.…...
QT 周期性的杀死一个进程(软件),一分钟后自动退出
1.原因:某软件开机自启动很烦,搞一个程序干掉这个自启动的软件 2.QT代码 main.cpp #include "KillXXX.h" #include <QtWidgets/QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);KillXXX k;return a.exec…...
MySQL任意版本安装卸载和数据库原理图绘制
MYSQL任意版本安装和卸载 安装: 1、解压文件 --- 不能出现中文路径 2、在解压目录(安装目录)下: 1>.创建data文件夹 2>.创建配置文件my.txt 然后修改成ini格式 3、修改配置文件 basedirD:\\mysql\\mysql-5.7.28-winx64…...
技术成神之路:设计模式(二十三)解释器模式
相关文章:技术成神之路:二十三种设计模式(导航页) 介绍 解释器模式(Interpreter Pattern)是一种行为设计模式,用于定义一种语言的文法表示,并提供一个解释器来处理这种文法。它用于处理具有特定语法或表达…...
2024软考《软件设计师》-Python专题知识(含历年真题解析)
自2020年之后,软考软件设计师考试在综合知识部分开始增加Python编程语言相关考点,每年会考2~3分的样子。本文将结合近几年常考的内容,扩展一下Pyhton的基础知识!考前看一看,或许有所帮助。 一、基础语法 标识符 第一…...
基于大数据 Python+Vue 旅游推荐可视化系统(源码+LW+部署讲解+数据库+ppt)
!!!!!!!!! 会持续一直更新下去 有问必答 一键收藏关注不迷路 源码获取:https://pan.baidu.com/s/1aRpOv3f2sdtVYOogQjb8jg?pwdjf1d 提取码: jf1d &#…...
使用虚拟机搭建环境:CentOS7 Docker、MySQL、Redis 安装与配置
创作灵感 项目实践总结:记录了在虚拟机中安装与配置CentOS7环境下的Docker、MySQL、Redis的全过程,帮助理解和应用各项技术。技术笔记与问题总结:详细梳理了每一步安装的关键点与常见问题,并给出了解决方案。职业感悟与心得&…...
[分享] Docker容器可视化管理工具 - WGCLOUD
WGCLOUD是新一代运维监测平台,它可以监控Docker容器的各种性能数据,比如内存,cpu,Image,运行时间,运行状态,端口映射等信息 WGCLOUD也支持在页面启动,重启,停止Docker容…...
保存网页中 canvas 的内容
在开发人员工具中,保存网页中 canvas 的内容,可以用这个方法: 1. 在 dom 中创建一个下载按钮 <button id="save">保存</button>2. 控制台中运行: const gCanvas = document.querySelector(#page_1);function onSave() {gCanvas.toBlob((blob) =&g…...
PID控制原理
PID控制原理 PID控制器是一种经典且广泛应用于工业控制领域的反馈控制器,它由比例(P)、积分(I)和微分(D)三个部分组成。通过对这三个部分的综合调节,PID控制器能够实现对被控对象的…...
python 使用 企微机器人发送消息
import requestswecom_bot_webhook ""msg_text "" # 要发送的消息内容""" mentioned_mobile_list : 手机号列表 , 提醒手机号对应的群成员(某个成员) """ res requests.post(wecom_bot_webhook,json{"msgtype"…...
ARM/Linux嵌入式面经(五二):华为
文章目录 一面技术面相关问题1. **硬件改进的具体内容是什么?**硬件改进的具体内容深入询问及回答2. **在维护前任师兄的代码时,你遇到了哪些挑战?**问题回答面试官追问及回答3. **在嵌入式系统中,内存泄漏通常有哪些原因?**一、内存泄漏的主要原因二、内存泄漏的具体场景…...
[旧日谈]高清画面撕裂问题考
背景 无边框透明背景透明的窗口,在随着缩放比例非整数倍数放大时的画面发生了露底、撕裂问题。 当我们在使用Qt开发的时候,遇到了一个结构性问题。因为我们的软件是自己做的,所以要自己定义标题栏,所以我们设置了软件为FrameLess…...
Nginx反向代理-域名代理前后端项目部署流程
一、下载Nginx 地址:https://nginx.org/en/download.html 1、稳定版本下载 二、Nginx配置 1、下载文件完成后,解压文件 2、打开文件目录下conf目录,打开找到nginx.conf 3、文件配置 注意:.conf 文件使用文本编辑器编辑后&…...
代码随想录(十二)——图论
并查集 并查集主要有三个功能。 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上判断两个节点是否在…...
如何通过 Service Mesh 构建高效、安全的微服务系统
1. 引言 1.1.什么是 Service Mesh? Service Mesh 是一种基础架构层,负责处理微服务之间的通信,它通过在每个服务旁边部署代理(通常称为 Sidecar)来捕获和管理服务间的网络流量。这种方式解耦了微服务的业务逻辑和基础…...
MySQL 临时表详解
在 MySQL 中,临时表(Temporary Table)是一种非常有用的工具,可以帮助我们在执行复杂查询时存储临时数据。临时表的存在时间仅限于会话期,当会话结束后,临时表自动销毁。本文将详细讲解 MySQL 临时表的创建、…...
Kafka系列之:Kafka集群新增节点后实现数据均衡
Kafka系列之:Kafka集群新增节点后实现数据均衡 一、背景二、Kafka集群快速负载均衡方案三、按照Topic负载均衡Kafka系列之:使用Kafka Manager实现leader分区平衡和broker节点上分区平衡一、背景 Kafka集群新增节点,要使得每个节点数据均衡,在增加完kafka topic分区后,要进…...
实验:使用Oxygen发布大型手册到Word格式
此前,我曾发表过一篇文章《结构化文档发布的故事和性能调优》,文中讨论了在将大型DITA手册转换为PDF格式时可能遇到的性能挑战及相应的优化策略。 近日,有朋友咨询,若将同样的大型手册输出为MS Word格式,是否也会面临…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...
从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...
【HarmonyOS 5】鸿蒙中Stage模型与FA模型详解
一、前言 在HarmonyOS 5的应用开发模型中,featureAbility是旧版FA模型(Feature Ability)的用法,Stage模型已采用全新的应用架构,推荐使用组件化的上下文获取方式,而非依赖featureAbility。 FA大概是API7之…...
