粘包和半包问题及解决办法

news2024/11/19 19:33:28

粘包问题是指数据在传输时,在一条消息中读取到了另一条消息的部分数据,这种现象就叫做粘包。

半包问题是指数据在传输时,接收端只收到了部分数据,而非完整的数据,就叫做半包。

产生粘包和半包问题原因:

这些问题发生在 TCP/IP 协议中,因为 TCP 是面向连接的传输协议,它是以“流”的形式传输数据的,而“流”数据是没有明确的开始和结尾边界的,所以就会出现粘包问题

大部分情况下我们都把粘包问题和半包问题看成同一个问题

问题代码演示

  • 服务器端用来接收消息
  • 客户端用来发送一段固定的消息。

通过输出服务器端接收到的信息来观察粘包问题。服务器端代码实现如下:

package com.nien.test.sticky;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author ally-coding
 * @Date: 2023/10/16 23:47
 * @Project cetc_test
 * @Description: 粘包服务器端测试
 */
public class ServSocket {
    private static final int BYTE_LENGTH = 20;
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        //获取客户端连接
        Socket clientSocker = serverSocket.accept();
        //得到客户端发送的流对象
        try(InputStream inputStream = clientSocker.getInputStream()){
            while(true){
                //循环获取客户端发送的信息
                byte[] bytes = new byte[BYTE_LENGTH];
                // 读取客户端发送的信息
                int count = inputStream.read(bytes, 0, BYTE_LENGTH);
                if(count>0){
                    System.out.println("接受到客户端的信息是:"+new String(bytes));
                }
                count=0;
            }
        }
    }
}

 客户端代码:

package com.nien.test.sticky;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author ally-coding
 * @Date: 2023/10/16 23:54
 * @Project cetc_test
 * @Description: 粘包客户端端测试
 */
public class ClientSocket {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",8888);
        final String message = "Hello world!";
        try(OutputStream outputStream = socket.getOutputStream()){
            for (int i = 0; i < 10; i++) {
                outputStream.write(message.getBytes());
            }
        }
    }
}

执行结果如下所示。

可以明显看出,服务器端发生了粘包问题。

解决办法

1.发送方和接收方固定发送数据的大小,当字符长度不够时用空字符弥补,有了固定大小之后就知道每条消息的具体边界了,这样就没有粘包的问题了;
2.在 TCP 协议的基础上封装一层自定义数据协议,在自定义数据协议中,包含数据头(存储数据的大小)和数据的具体内容,这样服务端得到数据之后,通过解析数据头就可以知道数据的具体长度了,也就没有粘包的问题了;
3.以特殊的字符结尾,比如以“\n”结尾,这样我们就知道数据的具体边界了,从而避免了粘包问题(推荐方案)

方法1 固定发送数据的大小

代码实现:

服务端代码:

package com.nien.test.sticky.solver1;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author ally-coding
 * @Date: 2023/10/17 0:40
 * @Project cetc_test
 * @Description: 粘包问题解决1-服务端
 */
public class Server1 {
    private static final int BYTE_LENGTH = 1024;
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9091);
        //获取客户端连接
        Socket clientSocker = serverSocket.accept();
        //得到客户端发送的流对象
        try(InputStream inputStream = clientSocker.getInputStream()){
            while(true){
                //循环获取客户端发送的信息
                byte[] bytes = new byte[BYTE_LENGTH];
                // 读取客户端发送的信息
                int count = inputStream.read(bytes, 0, BYTE_LENGTH);
                if(count>0){
                    System.out.println("接受到客户端的信息是:"+new String(bytes).trim());
                }
                count=0;
            }
        }
    }
}

客户端代码:

package com.nien.test.sticky.solver1;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author ally-coding
 * @Date: 2023/10/17 0:40
 * @Project cetc_test
 * @Description: 粘包问题解决1-客户端
 */
public class Client1 {

    private static final int BYTE_LENGTH=1024;

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",9091);
        final String messgae = "Hello world!";
        try(OutputStream outputStream = socket.getOutputStream()){
            byte[] bytes = new byte[BYTE_LENGTH];
            int idx = 0;
            for (byte b : messgae.getBytes()){
                bytes[idx] = b;
                idx++;
            }
            for (int i = 0; i < 10; i++) {
                outputStream.write(bytes, 0, BYTE_LENGTH);
            }
        }
    }
}

执行结果如下所示。

虽然这种方式可以解决粘包问题,但这种固定数据大小的传输方式,当数据量比较小时会使用空字符来填充,所以会额外的增加网络传输的负担。

方法2 在 TCP 协议的基础上封装一层自定义数据协议

步骤1 编写一个消息封装类 2编写客户端 3编写服务器

1.编写消息封装类代码:

package com.nien.test.sticky.solver2;

import com.sun.org.apache.regexp.internal.RE;

import java.io.IOException;
import java.io.InputStream;
import java.text.NumberFormat;

/**
 * @author ally-coding
 * @Date: 2023/10/17 0:52
 * @Project cetc_test
 * @Description: 消息封装类
 */
public class SocketPacket {

    static final int HEAD_SIZE=8;

    /**
     * 将协议封装为:协议头 + 协议体
     * @param content
     * @return
     */
    public byte[] toBytes(String content){
        //协议体 byte数据
        byte[] bodyByte = content.getBytes();
        int bodyByteLength = bodyByte.length;
        // 最终封装对象
        byte[] result = new byte[HEAD_SIZE + bodyByteLength];
        // 借助 NumberFormat 将 int 转换为 byte[]
        NumberFormat numberFormat = NumberFormat.getNumberInstance();
        numberFormat.setMinimumIntegerDigits(HEAD_SIZE);
        numberFormat.setGroupingUsed(false);
        //协议头 byte数组
        byte[] headByte = numberFormat.format(bodyByteLength).getBytes();
        // 封装协议头
        System.arraycopy(headByte, 0, result, 0, HEAD_SIZE);
        // 封装协议体
        System.arraycopy(bodyByte,0, result, HEAD_SIZE, bodyByteLength);
        return result;
    }

    /**
     * 获取消息头的内容(也就是消息体的长度)
     * @param inputStream
     * @return
     * @throws IOException
     */
    public int getHeader(InputStream inputStream) throws IOException {
        int result = 0;
        byte[] bytes = new byte[HEAD_SIZE];
        inputStream.read(bytes, 0, HEAD_SIZE);
        //得到消息体的字节长度
        result = Integer.valueOf(new String(bytes));
        return result;
    }
}

2.编写客户端代码

package com.nien.test.sticky.solver2;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Random;

/**
 * @author ally-coding
 * @Date: 2023/10/17 1:30
 * @Project cetc_test
 * @Description: 客户端
 */
public class Client2 {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",9093);
        String[] message = {"Hello world","Hello java"};
        SocketPacket socketPacket = new SocketPacket();
        try(OutputStream outputStream = socket.getOutputStream()){
            for (int i = 0; i < 10; i++) {
                String msg = message[new Random().nextInt(message.length)];
                byte[] bytes = socketPacket.toBytes(msg);
                outputStream.write(bytes, 0, bytes.length);
                outputStream.flush();
            }
        }
    }
}

3.编写服务端

package com.nien.test.sticky.solver2;

import java.io.IOException;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author ally-coding
 * @Date: 2023/10/17 9:19
 * @Project cetc_test
 * @Description: 服务器端 使用线程池来处理每个客户端的业务请求
 */
public class Server2 {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9093);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 用线程池处理更多的客户端
        ThreadPoolExecutor threadPoolExecutor  = new ThreadPoolExecutor(100,150,100,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        threadPoolExecutor.submit(()->{
            //客户端消息处理
            processMessage(clientSocket);
        });
    }


    private static void processMessage(Socket clientSocket){
        // Socket 封装对象
        SocketPacket socketPacket = new SocketPacket();
        // 获取客户端发送的消息对象
        try(InputStream inputStream = clientSocket.getInputStream()) {
            while (true){
                // 获取消息头(也就是消息体的长度)
                int bodyLength = socketPacket.getHeader(inputStream);
                // 消息体 byte 数组
                byte[] bodyBytes = new byte[bodyLength];
                // 每次实际读取字节数
                int readCount = 0;
                // 消息体赋值下标
                int bodyIndex = 0;
                // 循环接收消息头中定义的长度
                while (bodyIndex<=(bodyLength-1) &&
                        (readCount = inputStream.read(bodyBytes, bodyIndex, bodyLength))!= -1){
                    bodyIndex += readCount;
                }
                bodyIndex=0;
                // 成功接收到客户端的消息并打印
                System.out.println("接收到客户端的信息:" + new String(bodyBytes));
            }
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}

​运行结果如下所示。

此方法虽然可以解决粘包问题,但消息的设计和代码的实现复杂度比较高,所以也不是理想的解决方案。

方法3 以特殊的字符结尾

代码实现:

服务器代码:

package com.nien.test.sticky.solver3;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author ally-coding
 * @Date: 2023/10/18 0:40
 * @Project cetc_test
 * @Description:
 */
public class Server3 {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9092);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 使用线程池处理更多的客户端
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100,150,100,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        threadPool.submit(()->{
           //消息处理
            processMessage(clientSocket);
        });
    }

    /**
     * 消息处理
     * @param clientSocket
     */
    private static void processMessage(Socket clientSocket){
        // 获取客户端发送的消息流对象
        try(BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(clientSocket.getInputStream()))) {
            while (true){
                // 按行读取客户端发送的消息
                String msg = bufferedReader.readLine();
                if(msg!=null){
                    System.out.println("接收到客户端的信息:" + msg);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码:

package com.nien.test.sticky.solver3;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;

/**
 * @author ally-coding
 * @Date: 2023/10/18 0:40
 * @Project cetc_test
 * @Description:
 */
public class Client3 {

    public static void main(String[] args) throws IOException {
        // 启动 Socket 并尝试连接服务器
        Socket socket = new Socket("127.0.0.1", 9092);
        String message = "Hi,Java."; // 发送消息
        try (BufferedWriter bufferedWriter = new BufferedWriter(
                new OutputStreamWriter(socket.getOutputStream()))) {
            // 给服务器端发送 10 次消息
            for (int i = 0; i < 10; i++) {
                // 注意:结尾的 \n 不能省略,它表示按行写入
                bufferedWriter.write(message + "\n");
                // 刷新缓冲区(此步骤不能省略)
                bufferedWriter.flush();
            }
        }
    }
}

执行结果如下图所示。

该方法最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性。

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

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

相关文章

二维码智慧门牌管理系统:确保数据准确,强制校验GPS信号强度

文章目录 前言一、数据采集多种方式二、提高工作效率与管理效率 前言 在快速发展的科技时代&#xff0c;我们推出了一款最新的门牌系统解决方案——二维码智慧门牌。这款门牌不仅具备高效的管理功能&#xff0c;还为入口管理提供全新的智慧化解决方案。 一、数据采集多种方式 …

【6. N 字形变换】

目录 一、题目描述二、算法原理三、代码实现 一、题目描述 二、算法原理 三、代码实现 class Solution { public:string convert(string s, int numRows) {if(numRows1){return s;}string ret;int d 2 * numRows - 2;//第0行size_t i 0, n s.size();while (i < n){ret …

N沟道场效应管 FDA69N25深度图解 工作原理应用

深力科推荐一款 FDA69N25是高压 MOSFET产品&#xff0c;基于平面条形和 DMOS 技术。 该 MOSFET 产品专用于降低通态电阻&#xff0c;并提供更好的开关性能和更高的雪崩能量强度。 该器件系列适用于开关电源转换器应用&#xff0c;如功率因数校正&#xff08;PFC&#xff09;、…

2023年中国公共卫生信息化竞争现状及行业市场规模分析[图]

公共卫生信息化主要是指国家到地方各级卫健委下属的公共卫生健康主管部门&#xff0c;为提高信息化水平&#xff0c;综合运用计算机技术、信息通信技术等信息技术&#xff0c;以提升公共卫生监督执法、食品药品监督执法、卫生信息资源整合、疾病控制预防、从业人员健康体检等业…

Chrome 115之后的版本,安装和使用chromedriver

在Python中使用selenium 时报如下错误&#xff1a; 1. 老版本chrome对应的chromedriver 下载地址&#xff1a;CNPM Binaries Mirror 2. 新版本chrome对应的chromedriver 下载地址&#xff1a;Chrome for Testing availability

iPhone 15 拆机调查来了;小米澎湃 OS 彻底重写底层架构丨 RTE 开发者日报 Vol.69

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」&#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「…

【C/C++笔试练习】内联函数、缺省参数、函数重载、类定义、不要二、字符串转成整数、Fibonacci数列、合法括号序列判断

文章目录 C/C笔试练习1.内联函数&#xff08;1&#xff09;内联函数的使用&#xff08;2&#xff09;内联函数的使用 2.缺省参数&#xff08;3&#xff09;缺省参数概念理解 3.函数重载&#xff08;4&#xff09;函数重载的定义&#xff08;5&#xff09;函数重载的定义 4.类定…

2023年中国清净剂行业需求现状及前景分析[图]

清净剂用于中和由于燃烧和润滑油氧化产生的酸性物质&#xff0c;并清除颗粒和污物。这类杂质在油中的溶解度有限&#xff0c;因此&#xff0c;清净剂可以最大程度减少沉积物的生成&#xff0c;降低污染&#xff0c;提高环保排放标准。成熟产品有磺酸盐、硫化烷基酚盐、烷基水杨…

前端渲染后端返回的HTML格式的数据

在日常开发中&#xff0c;经常有需要前端渲染后端返回页面的需求&#xff0c;对于不同数据结构&#xff0c;前端的渲染方式也不尽相同&#xff0c;本文旨在对各种情况进行总结。 后端返回纯html文件格式 数据包含html标签等元素&#xff0c;数据类型如下图&#xff1a; 前端通…

分享一下微信小程序怎么添加成绩查询插件

微信小程序如何添加成绩查询插件 一、引言 微信小程序是一种方便快捷的应用程序&#xff0c;可以在微信中使用。随着教育的不断发展&#xff0c;学校和家长对于学生的成绩查询需求也越来越高。通过在微信小程序中添加成绩查询插件&#xff0c;可以为学生和家长提供更加便捷的…

超火的双臂烹饪机器人Project YORI,分分钟成为你的专属大厨!

原创 | 文 BFT机器人 当前行业内有两种通用的烹饪自动化方法&#xff1a;一种是“制造一个可以在普通厨房中运作的烹饪机器人&#xff0c;因为每个人都有厨房”&#xff0c;这听起来很不错&#xff0c;但接下来你就必须使你的烹饪机器人能够在厨房环境中正常运行&#xff0c;这…

极智项目 | 实战人脸识别签到系统

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多经验分享 大家好&#xff0c;我是极智视界&#xff0c;本文来介绍 实战人脸识别签到系统。 本文介绍的 实战人脸识别签到系统&#xff0c;提供完整的可以一键执行的项目工程源码&#xff0c;获取方式有两个&#xff1a…

java 调用 360 接口实现批量查询手机号码归属地

网上的手机号码归属地查询&#xff0c;要么限制查询条数&#xff0c;要么收费&#xff0c;于是找到一个 360 提供的查询 api 使用多线程异步查询&#xff0c;Future 确保查询结果顺序与输入顺序一致 核心 Controller package com.example.phonenumber.controller;import cn.h…

铜死亡+铁死亡,搭配WGCNA+单细胞+分型+实验

今天给同学们分享一篇铜死亡铁死亡WGCNA单细胞分型实验的生信文章“Cross-talk between cuproptosis and ferroptosis regulators defines the tumor microenvironment for the prediction of prognosis and therapies in lung adenocarcinoma”&#xff0c;这篇文章于2023年1月…

Vue-Cli - Vue 脚手架的创建 以及 目录结构说明

目录 一、Vue-Cli 1.1、Vue 脚手架的创建 二、Vue 脚手架目录结构说明 1.1、vscode 使用 Vue 脚手架 1.2、结构说明 一、Vue-Cli 1.1、Vue 脚手架的创建 注意&#xff1a;你要提前安装好 node.js &#xff08;官网下载即可&#xff09;&#xff0c;配置好环境变量. a&…

【内存管理】虚拟内存、物理内存之间的关系

什么是虚拟内存、什么是物理内存&#xff1f; Linux 虚拟内存是操作系统中一个重要概念&#xff0c;它允许程序在更大的地址空间中运行&#xff0c;并提供了高效的内存管理机制。 什么是虚拟内存&#xff1f; 虚拟内存是操作系统的一种内存管理技术&#xff0c;它将系统中的…

MacBook忘记开机密码啦!教你怎么找回密码

文章目录 一、前言二、使用恢复模式重置密码2.1 Apple ID找回密码2.2 终端resetpassword重设密码 三、总结 一、前言 在上一章《忘记开机密码啦&#xff01;我教你用ventoy找回密码》&#xff0c;我们已经学会了如何使用ventoy U盘启动器中的ntpwedit工具来找回windows的本地用…

Java模拟双向链表,增删操作

public static void main(String[] args) {ArrayList arrayList new ArrayList();Node zhangsan new Node("张三");Node lisi new Node("李四");Node wanger new Node("王二");//模拟双向链表&#xff0c;将表中元素依次连接起来zhangsan.ne…

【已解决】pyinstaller 将程序打包成 exe 文件后,无法保存视频或者保存的视频为空文件

这里写自定义目录标题 问题描述解决方法方法一方法二 参考 问题描述 使用pyinstaller将python程序打包为exe文件&#xff0c;其中包含保存视频的代码。直接运行脚本时&#xff0c;程序能够正确的保存视频。但是通过pyinstaller打包成exe文件后&#xff0c;exe文件无法保存视频…

EthernetIP 转MODBUS RTU协议网关连接FANUC机器人作为EthernetIP通信从站

远创智控YC-EIPM-RTU网关产品是一款高效的数据采集工具&#xff0c;它可以通过各种数据接口与工业领域的仪表、PLC、计量设备等产品连接&#xff0c;实时采集这些设备中的运行数据、状态数据等信息。采集到的数据经过整合和运算等操作后&#xff0c;可以被传输到其他设备或者云…