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)

大家都在看

  • vi和vim文本编辑器

    vi和vim文本编辑器 vi和vim模式的相互切换 快捷键使用案例 拷贝当前行yy; 拷贝当前行向下的5行 5yy; 并粘贴(p) 删除当前行dd; 删除当前行向下的5行 5dd …

    Linux 2023年6月11日
    0105
  • shell 获取进程号

    Shell最后运行的后台PID(后台运行的最后一个进程的进程ID号) $! Shell本身的PID(即脚本运行的当前进程ID号 $$ Original: https://www.c…

    Linux 2023年5月28日
    094
  • python入门基础知识一(基于孙兴华python自动化)

    print(‘aaa’)等价于print(“aaa”) 英文单引号和双引号在字符串的输出上并无区别,但如果要打印这么一段话:I&#8…

    Linux 2023年6月7日
    096
  • SpringBoot-druid

    SpringBoot-druid 9.1 druid简介 Java程序很大一部分要操作数据库,为了提高性能操作数据库,又不得不使用数据库连接池。 Druid 是阿里巴巴开源平台上一…

    Linux 2023年6月14日
    0110
  • 关于NLog在.NET CORE下如何进行日志的持久化及通过邮件发送日志

    配置过程 安装NLog 通过Nuget进行集成(NuGet Gallery | NLog.Web.AspNetCore 4.14.0) 通过命令行安装 Install-Packag…

    Linux 2023年6月14日
    092
  • ACP 知识点总结

    记录下学习ACP过程不断遇到的且需要记录的知识点: 在阿里云专有网络VPC创建之后,路由器也是随着VPC一起自动创建,所以不需要手动创建,这个时候需要继续创建交换机才能在交换机种创…

    Linux 2023年6月14日
    094
  • Redis 服务监控 redis-faina 命令查看 和 redis-live 界面查看

    Redis现在在很多公司中应用已经很广泛了,但是如何监控redis,实时的观察redis的性能,下面就关于Redis的常用监控介绍一下。 一、使用redis自带的info命令和mo…

    Linux 2023年5月28日
    0117
  • 祖传代码如何优化性能?

    hello大家好呀,我是小楼~ 今天又带来一次性能优化的分享,这是我刚进公司时接手的祖传(坏笑)项目,这个项目在我的文章中屡次被提及,我在它上面做了很多的性能优化,比如《记一次提升…

    Linux 2023年6月8日
    0127
  • Emacs 基础offset值

    cc-mode有如下规定:One of the symbols +, -, ++, –, *, or /These special symbols describe a…

    Linux 2023年6月13日
    091
  • Android下获取FPS的几种方法

    FPS(Frames Per Second)是关乎Android用户体验最为重要的指标之一,而在VR中更是如此。为了评估VR系统、VR SDK及Unity应用的性能,通常会实时获取…

    Linux 2023年6月7日
    0100
  • ruoyi接口权限校验

    此文章属于ruoyi项目实战系列 ruoyi系统在前端主要通过权限字符包含与否来动态显示目录和按钮。为了防止通过http请求绕过权限限制,后端接口也需要进行相关权限设计。 @Pre…

    Linux 2023年6月7日
    0160
  • vue动画效果和vue组件——day03

    <div id="app"><br> <input type="button" value="&am…

    Linux 2023年6月7日
    093
  • 【spring-boot】配置Redis工具类

    如何在spring-boot中使用Redis工具类 修改pom.xml文件 新增spring-boot-starter-data-redis配置 org.springframewo…

    Linux 2023年5月28日
    0107
  • Redis下载及安装(windows版)

    下载地址1、Github下载地址:https://github.com/MicrosoftArchive/redis/releases2、百度网盘下载地址 https://pan….

    Linux 2023年5月28日
    089
  • Makefile

    target … : prerequisites … command … … target可以是一个object file(目标文件),也可以是一个执行文件,还可以…

    Linux 2023年6月7日
    099
  • rsync

    rsync是什么 rsync特性 1)可以镜像保存整个目录树和文件系统。 2)可以很容易做到保持原来文件的权限、时间、软硬连接等。 3)无需特殊权限即可安装。 4)快速:第一次同步…

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