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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pandas快速入门

pandas快速入门 1. 创建pandas对象1.1 前言1.2 使用DataFrame类创建pandas对象1.3 对DataFrame对象进行索引1.4 使用Series类创建pandas对象1.5 对DataFrame Series对象使用常见方法 2. pandas读取文件2.1 使用pd.read_*方法读取文件2.2 使用to_*保存数据2.3 使用info()方法查看…

Python 判断键是否存在字典中(新手入门、实战案例)

在早期的Python2版本中&#xff0c;可以使用 dict.has_key()方法来判断一个键是否存在于字典中。 在Python3中&#xff0c;dict.has_key()方法被废弃了&#xff0c;不能再被使用。如果在Python3中尝试使用dict.has_key()方法会导致 AttributeError异常。 那在Python3中要如何判…

Linux:指令再认识

文章目录 前言一、知识点1. Linux下一切皆文件&#xff0c;也就是说显示器也是一种文件2. 指令是什么&#xff1f;3. ll 与 ls -l4. 日志5. 管道6. 时间戳 二、基本指令1. man指令2. cp指令3. mv指令4. 查看文件1&#xff09;cat/tac指令——看小文件2&#xff09;more/less指令…

Qt:QtCreator使用

用一个QtCreator适配所有Qt版本 首先Qt和QtCreator版本并不是通用的&#xff0c;一个电脑中可以安装很多个Qt版本&#xff0c;但只需要安装一个最新版本的QtCreator即可 Qt是一个语言&#xff0c;也可理解为一个SDK库&#xff0c;Qt目前最新版本为6.7 QtCreator是一个集成开发…

嵌入式Linux的AXI平台(platform)驱动教程

本文以JFMQL100的Linux系统的AXI接口的平台驱动为例&#xff0c;介绍嵌入式Linux的平台驱动编写、测试软件编写以及验证方式。本文的方法适用于任意嵌入式芯片Linux的物理地址映射的平台&#xff08;platform&#xff09;驱动的编写、测试与应用。 本文中AXI的开始地址为0x8000…

Python浪漫之画星星

效果图&#xff08;动态的哦&#xff01;&#xff09;&#xff1a; 完整代码&#xff08;上教程&#xff09;&#xff1a; import turtle import random import time # 导入time模块# 创建一个画布 screen turtle.Screen() screen.bgcolor("red")# 创建一个海龟&a…

Coppelia Sim (v-REP)仿真 机器人3D相机手眼标定与实时视觉追踪 (一)

coppelia sim[V-REP]仿真实现 机器人于3D相机手眼标定与实时视觉追踪 一 标定板的制作生成标定的PDF文件PDF转为图像格式图像加载到仿真中 二 仿真场景设置加载机器人加载的控制dummy ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b48549d355d8441d8dfc20bc7ba7196…

MYSQL作业三

准备工作 查询操作

Linux复习-C++

参考博客&#xff1a; https://blog.csdn.net/qq_45254369/article/details/126023482?ops_request_misc%257B%2522request%255Fid%2522%253A%252277629891-A0F3-4EFC-B1AC-410093596085%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&req…

力扣 167. 两数之和 II - 输入有序数组

目录 一、题目二、思路三、代码 一、题目 二、思路 由于数组是 非递减顺序排列 的&#xff0c;利用该先验知识&#xff0c;可以设置两个指针在数组的起始和末尾&#xff0c;相向前进&#xff0c;与暴力枚举相比可以加快寻找速度。 三、代码 class Solution {public int[] tw…

面向对象(上)

1.初始对象 学习目标&#xff1a;理解使用对象完成数据组织的思路 可以看到加入了登记表后&#xff0c;我们学生所提供的信息就组织的简洁明了 程序中也是有多种多样的方式来记录数据 思考&#xff1a;使用变量记录数据太乱了&#xff0c;如果程序中也和生活中一样&#xff…

Linux - 动静态库

文章目录 一、静态库1、定义2、文件扩展名3、生成静态库4、使用静态库5、静态库的优缺点 二、动态库1、定义2、文件扩展名3、生成动态库4、使用动态库5、动态库的优缺点6、简单动态库加载 三、补充 一、静态库 1、定义 静态库是在编译时链接到最终的可执行文件中的库。这意味着…

Java Web项目实战:从零基础到项目开发全流程

Java Web开发环境搭建 Java Web开发需要Java运行环境、IDE&#xff08;如IntelliJ IDEA或Eclipse&#xff09;以及Maven集成开发工具等。首先&#xff0c;确保电脑上安装有Java 8或更高版本&#xff0c;可以通过访问Java官网获取最新版本。其次&#xff0c;安装IDE&#xff0c…

深入理解跳出率:如何利用百度统计优化网站用户体验

在数字营销的世界中&#xff0c;跳出率是一个关键指标&#xff0c;它衡量了用户对网站的第一印象和内容的吸引力。最近&#xff0c;我在分析我的网站数据时发现&#xff0c;跳出率常常高达100%&#xff0c;这促使我深入研究了跳出率的含义和影响因素。 跳出率的定义与计算 跳…

AndroidStudio Koala更改jdk版本 2024-1-2

升级android studio版本到 AndroidStudio Koala之后&#xff0c;发现修改jdk版本的方式不同了。如下&#xff1a; 修改jdk版本

《Python网络安全项目实战》

《Python网络安全项目实战》 项目1 Python 环境安装任务1.1 Windows上安装Python任务1.2 Ubuntu环境下安装Python 项目2 Python基础练习任务2.1 使用数据类型任务2.2 使用组合数据类型任务2.3 使用控制结构任务2.4 使用函数任务2.5 使用模块 项目3 处理文件中的数据任务3.1 读文…

Ajax:XMLHttpRequest

Ajax&#xff1a;XMLHttpRequest XMLHttpRequestgeturlpost 数据交换格式XMLjson XMLHttpRequest Level 2请求时限表单数据操纵文件上传 XMLHttpRequest XMLHttpRequest简称 xhr&#xff0c;是浏览器提供的 Javascript 对象&#xff0c;通过它可以请求服务器上的数据资源。 jQ…

如何在vscode中使用鼠标滑轮滚动来改变字体大小

实现内容&#xff1a;如何在vscode中使用鼠标滑轮滚动来改变字体大小 使用场景&#xff1a;我是在Ubuntu中安装的vscode 需求&#xff1a;因为最近在用这个&#xff0c;但是在使用过程中发现vscode的字体大小有点小&#xff0c;所以想改变下 实现滚轮滑动改变字体大小的具体步…

Spring Boot框架下的酒店住宿登记系统

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

2024 7月算法刷题记录

小米真题 手机流畅运行的秘密 时间限制&#xff1a;1.000S 空间限制&#xff1a;256MB 题目描述 8 月份发布会一结束&#xff0c;米小兔就在公司领到了一台最新发布的 Xiaomi MIX Fold 3 手机&#xff0c;这是一款小米旗舰折叠屏手机&#xff0c;并搭载了全新升级架构的 MI…