TCP 粘包-拆包问题及解决方案

歧义在”TCP”上,这个”粘包”跟TCP其实没关系。这里的”粘包”其实是应用程序中没有处理好数据包分割,两个应用层的数据包粘在一块了。不过面试都那么问,所以把问题复述一遍。在面试过程中可以说明一下不是TCP协议的问题,而是因为没有处理好数据包分割,两个应用层的数据包粘在一块了;这也是让面试官眼前一亮的一次机会。

TCP粘包拆包问题

- TCP 全称是 Transmission Control Protocol(传输控制协议),它由 IETF 的 RFC 793 定义,是一种面向连接的点对点的传输层通信协议。
- 粘包拆包问题是处于⽹络⽐较底层的问题,在数据链路层、⽹络层以及传输层都有可能发⽣;
- TCP会发生粘包问题;TCP⽆消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题;
- UDP不会发生粘包问题;UDP具有保护消息边界,在每个UDP包中就有了消息头(UDP长度、源端口、目的端口、校验和)。

什么是粘包 – 拆包问题

  • 粘包问题
- 粘包问题指,当发送方发送了数据包 消息1 - ABC消息2 - DEF 时,但接收方接收到的数据包却是 消息 -  ABCDEF,像这种一次性读取了两条数据包的数据粘连在一起的情况就叫做粘包(正常情况应该是一条一条读取的)。

TCP 粘包-拆包问题及解决方案
  • 拆包问题
- 拆包问题是指,当发送方发送了数据包  消息1 -  ABC 消息2 - DEF 时,接收方接收到数据包经拆分后获得了 ABCDEF 两个数据包信息的情况,像这种情况有时候也叫做半包。

TCP 粘包-拆包问题及解决方案

为什么存在粘包 – 拆包问题

- TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息;

- TCP 协议是流式协议;所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要认为手动地去给这些协议划分边界。
  • 粘包主要原因
- 发送方每次写入数据 < 接收方套接字(Socket)缓冲区大小;
- 接收方读取套接字(Socket)缓冲区数据不够及时。
  • 拆包问题
- 发送方每次写入数据 > 接收方套接字(Socket)缓冲区大小;
- 发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),既TCP报⽂⻓度-TCP头部⻓度>MSS时发生拆包问题。

粘包 – 拆包 演示

PasteServer.java: 服务端;
PasteClient.java: 客户端;

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

/**
 * @author hosystem
 * @version 1.0
 */
public class PasteServer {
    // 字节数组的长度
    private static final int BYTE_LENGTH = 20;
    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器
        ServerSocket serverSocket = new ServerSocket(9999);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 得到客户端发送的流对象
        try (InputStream inputStream = clientSocket.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;
            }
        }
    }
}
  • PasteClient.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author hosystem
 * @version 1.0
 */
public class PasteClient {

    public static void main(String[] args) throws IOException {
        // 创建 Socket 客户端并尝试连接服务器端
        Socket socket = new Socket("127.0.0.1", 9999);
        // 发送的消息内容
        final String message = "hello.java";
        // 使用输出流发送消息
        try (OutputStream outputStream = socket.getOutputStream()) {
            // 给服务器端发送 10 次消息
            for (int i = 0; i < 10; i++) {
                // 发送消息
                outputStream.write(message.getBytes());
            }
            outputStream.close();
        }finally {
            socket.close();
        }
    }
}

TCP 粘包-拆包问题及解决方案

通过上述结果我们可以看出,服务器端发生了粘包和拆包问题,因为客户端发送了 10 次固定的”hello.java.”的消息;正常的结果应该是服务器端也接收到了 10 次固定的消息才对,但结果并非如此。

粘包 – 拆包 解决方案

解决方案
- 方案一: 设置定⻓消息,服务端每次读取既定⻓度的内容作为⼀条完整消息(固定缓冲区大小);

- 方式二: 使⽤⾃定义协议+编解码器(封装请求协议);

- 方案三: 设置消息边界,服务端从⽹络流中按消息编辑分离出消息内容(特殊字符结尾,按行读取)。

优缺点
- 方案一: 从以上代码可以看出,虽然这种方式可以解决粘包和拆包的问题,但这种固定缓冲区大小的方式增加了不必要的数据传输;当这种方式当发送的数据比较小时会使用空字符来弥补,所以这种方式就大大的增加了网络传输的负担,所以它也不是最佳的解决方案。

- 方案二: 实现较为复杂,更多情况下使用该种实现;Dubbo实现自定义的传输协议,使用Netty来实现可降低编码复杂程度,netty框架对于粘包有专门encoder和decoder接口来处理。

- 方案三: 特殊字符的方案其实是最不可取的;TCP是面向流的;所以应该认为TCP传输的是字节流,任何一个字节都可能被传输;在这种情况下,特殊字符也不特殊了,没法和正常数据区分。

方式一: 固定缓冲区大小

固定缓冲区大小的实现方案,只需要控制服务器端和客户端发送和接收字节的(数组)长度相同即可。

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

/**
 * @author hosystem
 * @version 1.1
 */
public class PasteServer {

    // 字节数组的长度
    private static final int BYTE_LENGTH = 1024;
    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器
        ServerSocket serverSocket = new ServerSocket(9999);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 得到客户端发送的流对象
        try (InputStream inputStream = clientSocket.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;
            }
        }
    }
}
  • PasteClient.java
mport java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author hosystem
 * @version 1.1
 */
public class PasteClient {

    // 字节数组的长度
    private static final int BYTE_LENGTH = 1024;

    public static void main(String[] args) throws IOException {
        // 创建 Socket 客户端并尝试连接服务器端
        Socket socket = new Socket("127.0.0.1", 9999);
        // 发送的消息内容
        final String message = "hello.java";
        // 使用输出流发送消息
        OutputStream outputStream = socket.getOutputStream();
        try {

            //将数组装成定长字节数组
            byte[] bytes = new byte[BYTE_LENGTH];
            int index = 0;
            for (byte b : message.getBytes()) {
                bytes[index++] = b;
            }

            // 给服务器端发送 10 次消息
            for (int i = 0; i < 10; i++) {
                // 发送消息
                outputStream.write(bytes,0,BYTE_LENGTH);
            }
        }finally {
            socket.close();
            outputStream.close();
        }
    }
}

TCP 粘包-拆包问题及解决方案

方式二: 封装请求协议

将请求的数据封装为两部分:数据头+数据正文,在数据头中存储数据正文的大小,当读取的数据小于数据头中的大小时,继续读取数据,直到读取的数据长度等于数据头中的长度时才停止。
实现起来较为复杂,这里不给出代码,可以使用netty完成方式二。

方式三: 特殊字符结尾 – 按行读取

使用 Java 中自带的 BufferedReader 和 BufferedWriter,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \n 来结尾,读取的时候使用readLine 按行来读取数据,通过遇到结束标志 \n来结束行的读取。

  • PasteServer.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author hosystem
 * @version 1.3
 */
public class PasteServer {

    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器
        ServerSocket serverSocket = new ServerSocket(9999);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 得到客户端发送的流对象
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
            while (true) {
                String msg = bufferedReader.readLine();
                if (msg != null) {
                    // 成功接收到客户端的消息并打印
                    System.out.println("接收到客户端的信息:" + msg);
                }
            }
        }
    }
}
  • PasteClient.java
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class PasteClient {

    public static void main(String[] args) throws IOException {
        // 创建 Socket 客户端并尝试连接服务器端
        Socket socket = new Socket("127.0.0.1", 9999);
        // 发送的消息内容
        final String message = "hello.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();
            }
        }finally {
            socket.close();
        }
    }
}

TCP 粘包-拆包问题及解决方案

Original: https://www.cnblogs.com/HOsystem/p/15431649.html
Author: HOsystem
Title: TCP 粘包-拆包问题及解决方案

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/607048/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • redis高可用

    Redis-高可用(主从复制、哨兵模式、集群) 1.主从复制 1.1 主从复制简介 在 Redis 复制的基础上,使用和配置主从复制非常简单,能使得从 Redis 从服务器(下文称…

    Linux 2023年6月13日
    082
  • WPF 已知问题 Popup 失焦后导致 ListBox 无法用 MouseWheel 滚动问题和解决方法

    本文记录在 Popup 失焦后导致 ListBox 无法用 MouseWheel 滚动问题 本文记录在 Popup 失焦后导致 ListBox 无法用 MouseWheel 滚动问…

    Linux 2023年6月6日
    0102
  • 手把手教你在Linux系统下安装MySQL

    在CentOS中默认安装有MariaDB,这个是MySQL的分支,但为了需要,还是要在系统中安装MySQL,而且安装完成之后可以直接覆盖掉MariaDB。 1. 下载并安装MySQ…

    Linux 2023年6月14日
    090
  • c++智能指针的使用,shared_ptr,unique_ptr,weak_ptr

    c++智能指针的使用 官方参考 普通指针的烦恼:内存泄漏,多次释放,提前释放 智能指针 负责自动释放所指向的对象。 三种智能指针 shared_ptr,unique_ptr,wea…

    Linux 2023年6月14日
    0126
  • python2.6.6安装Image模块

    python2.6.6安装Image模块1、下载Image模块源码地址:http://www.pythonware.com/products/pil/index.htm2、加压文件…

    Linux 2023年6月14日
    082
  • 二分查找

    一:二分查找算法 本文章列出刷题中常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。 ps:什么最大值的最小,最远的最近。->都是二分 1:1二分查找框架 int …

    Linux 2023年6月7日
    077
  • Linux运行Jar包方式

    1 运行Jar包 第一种方式 java -jar xxx.jar 最基本的方式,程序运行的信息会一直输出在控制台,ctrl+c中断或者关闭窗口时,程序中断执行。 第二种方式 jav…

    Linux 2023年6月7日
    092
  • 【Javaweb】在项目中添加MyBatis依赖等

    pom.xml 仓库 如果你没有配置阿里云仓库镜像源,可以到这里来找 https://mvnrepository.com/ 如果你配置了阿里云仓库镜像源,可以来这里找 https:…

    Linux 2023年6月14日
    089
  • Linux 压缩、解压缩命令

    Linux 压缩、解压缩命令 tar 语法命令 tar [options…] [files] options:选择描述-A 追加tar文件至归档-c 创建一个新文档-d…

    Linux 2023年6月6日
    091
  • 内部类

    内部类:将一个类的定义放在另一个类的定义内部。内部类机制可以把逻辑相关的类组织在一起,并控制位于内部的类的可视性。 内部类与组合是完全不同的概念。 内部类不仅是一种代码隐藏机制(将…

    Linux 2023年6月8日
    090
  • 进程相关指令

    pgrep 查找进程名 KILL 删除 执行中的进程和工作 free 打印系统情况和内存情况 free [-bkmgotsh] free -h total used free sh…

    Linux 2023年6月7日
    096
  • 微服务,【容器亚健康状态】问题,研究和解决

    —【前言】— 我问:”程序有『亚健康状态』吗?” 一个正常的人,应该这样回答:”什么?程序,亚健康。。。?你神经病吧?我…

    Linux 2023年6月14日
    080
  • python 多线程

    python 多线程 多线程流程 导入模块 import threading 通过线程类型创建线程对象 &#x7EBF;&#x7A0B;&#x5BF9;&a…

    Linux 2023年6月13日
    074
  • zabbix自定义监控进程与日志

    zabbix自定义监控进程与日志 zabbix自定义监控进程与日志 zabbix自定义监控进程 zabbix自定义监控日志 zabbix自定义监控进程 现在我们需要监控客户端的某一…

    Linux 2023年6月13日
    0114
  • 多用户共享文件

    假设有三个用户:Tom Jerry Bob.其中,tom和Jerry都属于market部,Bob属于tech部,请在/usr目录下创建两个用户共享的目录market和public,…

    Linux 2023年6月13日
    084
  • linux命令之wget下载

    wget wget 是一个下载文件的工具。 格式 wget [&#x53C2;&#x6570;] [URL&#x5730;&#x5740;] 常用参…

    Linux 2023年5月27日
    078
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球