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;

    @Bean
    public ServerBootstrap serverBootstrap(NettyTcpServerHandler nettyTcpServerHandler) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected 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();
    }

    @PreDestroy
    public 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;
    }

    @Override
    public 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> {

    @Override
    protected 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("收到不重要数据,未发送响应。");
        }*/
    }

    @Override
    public 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{

    @Override
    protected 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); // 提取 $ 和 * 之间的部分

            // 计算 BCC
            for (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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用ORDER BY排序

在一个不明确的查询结果中排序返回的行。ORDER BY子句用于排序。如果使用了ORDER BY子句&#xff0c;它必须位于SQL语句的最后。 SELECT语句的执行顺序如下&#xff1a; 1.FROM子句 2.WHERE子句 3.SELECT子句 4.ORDER BY子句 示例一&#xff1a;查询employees表中的所有雇…

通俗易懂的入门 Axure RP文章 ,速学

目录 1. Axure RP简介&#xff1f; 2. Axure RP基本操作 &#xff08;1&#xff09;入门理解 &#xff08;2&#xff09;插入形状 &#xff08;3&#xff09;位置对齐、 &#xff08;4&#xff09;资源库 3. Axure RP基本交互 &#xff08;1&#xff09;切换不同的页面 …

进程间通信大总结Linux

目录 进程间通信介绍 进程间通信目的 进程间通信发展 进程间通信分类 管道 System V IPC POSIX IPC 管道 什么是管道 匿名管道 用fork来共享管道原理 站在文件描述符角度-深度理解管道 管道读写规则 管道特点 命名管道 创建一个命名管道 匿名管道与命名管道的区…

《云原生安全攻防》-- K8s攻击案例:权限维持的攻击手法

在本节课程中&#xff0c;我们将一起深入了解K8s权限维持的攻击手法&#xff0c;通过研究这些攻击手法的技术细节&#xff0c;来更好地认识K8s权限维持所带来的安全风险。 在这个课程中&#xff0c;我们将学习以下内容&#xff1a; K8s权限维持&#xff1a;简单介绍K8s权限维持…

UG2312软件安装教程+Siemens NX三维建模中文安装包下载

一、软件下载 【软件名称】&#xff1a;UG 2312 【支持系统】&#xff1a;win10/win11 【百度网盘】&#xff1a; https://pan.baidu.com/s/1oF-X29m1f5pDhElwi0rK8A?pwd70zi 二、UG NX软件 UG&#xff08;Unigraphics NX&#xff09;是一款集 CAD、CAM、CAE 于一体的高效…

大范围实景三维智能调色 | 模方自动化匀色解决方案

《实景三维中国建设总体实施方案&#xff08;2023—2025年&#xff09;》、《实景三维中国建设技术大纲》等相关文件中指出&#xff0c;倾斜Mesh三维模型修饰要求模型整体色彩真实&#xff0c;无明显色差。9月&#xff0c;自然资源部在国务院新闻发布会上表示&#xff0c;实景三…

Linux:线程及其控制

我们已经学了线程的创建&#xff0c;现在要学习线程的控制 线程等待 我们来先写一个没有线程等待的代码&#xff1a; pthcon.c: #include<stdio.h> #include<pthread.h> void* gopthread(void* arg){while(1){printf("pthread is running\n");sleep(1…

银行客户贷款行为数据挖掘与分析

#1024程序员节 | 征文# 在新时代下&#xff0c;消费者的需求结构、内容与方式发生巨大改变&#xff0c;企业要想获取更多竞争优势&#xff0c;需要借助大数据技术持续创新。本文分析了传统商业银行面临的挑战&#xff0c;并基于knn、逻辑回归、人工神经网络三种算法&#xff0…

SpringBoot实现微信支付接口调用及回调函数(商户参数获取)

#1024程序员节 | 征文 # 一、具体业务流程 1. 用户下单 - 前端操作&#xff1a; - 用户在应用中选择商品、填写订单信息&#xff08;如地址、联系方式等&#xff09;&#xff0c;并点击“下单”按钮。 - 前端将订单信息&#xff08;商品ID、数量、价格等&#xff09;发送…

Pytorch 实现图片分类

CNN 网络适用于图片识别&#xff0c;卷积神经网络主要用于图片的处理识别。卷积神经网络&#xff0c;包括一下几部分&#xff0c;输入层、卷积层、池化层、全链接层和输出层。 使用 CIFAR-10 进行训练&#xff0c; CIFAR-10 中图片尺寸为 32 * 32。卷积层通过卷积核移动进行计…

C++ —— map系列的使用

目录 1. map和multimap参考文档 2. map类的介绍 3. pair 4. map的增删查 4.1 插入 4.2 删除 4.3 查找 5. map的数据修改 6. map的operator[] 7. multimap和map的差异 1. map和multimap参考文档 - C Referencehttps://legacy.cplusplus.com/reference/map/ 2. map类的…

04 springboot-工程搭建案例(多环境部署,数据源, Swagger, 国际化,工具类)

项目搭建模板(多环境切换) springboot系列&#xff0c;最近持续更新中&#xff0c;如需要请关注 如果你觉得我分享的内容或者我的努力对你有帮助&#xff0c;或者你只是想表达对我的支持和鼓励&#xff0c;请考虑给我点赞、评论、收藏。您的鼓励是我前进的动力&#xff0c;让我…

基于CRNN模型的多位数字序列识别的应用【代码+数据集+python环境+GUI系统】

基于CRNN模型的多位数字序列识别的应用【代码数据集python环境GUI系统】 基于CRNN模型的多位数字序列识别的应用【代码数据集python环境GUI系统】 背景意义 多位手写数字识别&#xff0c;即计算机从纸张文档、照片、触摸屏等来源接收并解释可理解的手写数字输入的能力。 随着…

2024软件测试面试秘籍(含答案+文档)

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 Part1 1、你的测试职业发展是什么&#xff1f; 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师…

低代码可视化-uniapp海报可视化设计-代码生成

在uni-app中&#xff0c;海报生成器通常是通过集成特定的插件或组件来实现的&#xff0c;这些插件或组件提供了生成海报所需的功能和灵活性。我们采用了lime-painter海报组件。lime-painter是一款canvas海报组件&#xff0c;可以更轻松地生成海报。它支持通过JSON及Template的方…

【Linux】如何升级宝塔面板

执行命令&#xff0c;即可升级 curl https://io.bt.sy/install/update_panel.sh|bash

【Unity 实用工具篇】 | UGUI 循环列表 SuperScrollView,快速上手使用

前言 【Unity 实用工具篇】 | UGUI 循环列表 SuperScrollView&#xff0c;快速上手使用一、UGUI ScrollRect拓展插件&#xff1a;SuperScrollView1.1 介绍1.2 效果展示1.3 使用说明及下载 二、SuperScrollView 快速上手使用2.1 LoopListView22.2 LoopGridView2.3 LoopStaggered…

【Python爬虫】获取汽车之家车型配置附代码(2024.10)

参考大哥&#xff0c;感谢大哥&#xff1a;https://blog.csdn.net/weixin_43498642/article/details/136896338 【任务目标】 工作需要想更方便地下载汽车之家某车系配置清单&#xff1b;&#xff08;垃圾汽车之家不给下载导出表格&#xff0c;配置页叉掉了车系要出来还要重新…

提问: 监督学习, 无监督学习, 机器学习, 深度学习的关系? (通义千问2.5的回答)

前言: 以下内容由AI大模型通义千问大模型2.5生成 监督学习, 无监督学习, 机器学习, 深度学习的关系? 监督学习、无监督学习、机器学习和深度学习是人工智能领域的几个重要概念&#xff0c;它们之间存在一定的关系和区别。下面我将详细解释这些概念及其相互之间的关系&#xf…

Unity中使用UnityEvent遇到Bug

UnityEvent绑定过程中&#xff0c;放在Start&#xff08;&#xff09;中绑定会报错&#xff08;通过脚本添加UnityEvent事件脚本&#xff0c;绑定&#xff09; 绑定事件放在OnEnable&#xff08;&#xff09;中不会报错&#xff0c;但是依然不可以立刻添加UnityEvent事件脚本紧…