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


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

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

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

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

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

    问题代码演示

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

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

    1. package com.nien.test.sticky;
    2. import java.io.IOException;
    3. import java.io.InputStream;
    4. import java.net.ServerSocket;
    5. import java.net.Socket;
    6. /**
    7. * @author ally-coding
    8. * @Date: 2023/10/16 23:47
    9. * @Project cetc_test
    10. * @Description: 粘包服务器端测试
    11. */
    12. public class ServSocket {
    13. private static final int BYTE_LENGTH = 20;
    14. public static void main(String[] args) throws IOException {
    15. ServerSocket serverSocket = new ServerSocket(8888);
    16. //获取客户端连接
    17. Socket clientSocker = serverSocket.accept();
    18. //得到客户端发送的流对象
    19. try(InputStream inputStream = clientSocker.getInputStream()){
    20. while(true){
    21. //循环获取客户端发送的信息
    22. byte[] bytes = new byte[BYTE_LENGTH];
    23. // 读取客户端发送的信息
    24. int count = inputStream.read(bytes, 0, BYTE_LENGTH);
    25. if(count>0){
    26. System.out.println("接受到客户端的信息是:"+new String(bytes));
    27. }
    28. count=0;
    29. }
    30. }
    31. }
    32. }

     客户端代码:

    1. package com.nien.test.sticky;
    2. import java.io.IOException;
    3. import java.io.OutputStream;
    4. import java.net.Socket;
    5. /**
    6. * @author ally-coding
    7. * @Date: 2023/10/16 23:54
    8. * @Project cetc_test
    9. * @Description: 粘包客户端端测试
    10. */
    11. public class ClientSocket {
    12. public static void main(String[] args) throws IOException {
    13. Socket socket = new Socket("127.0.0.1",8888);
    14. final String message = "Hello world!";
    15. try(OutputStream outputStream = socket.getOutputStream()){
    16. for (int i = 0; i < 10; i++) {
    17. outputStream.write(message.getBytes());
    18. }
    19. }
    20. }
    21. }

    执行结果如下所示。

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

    解决办法

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

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

    代码实现:

    服务端代码:

    1. package com.nien.test.sticky.solver1;
    2. import java.io.IOException;
    3. import java.io.InputStream;
    4. import java.net.ServerSocket;
    5. import java.net.Socket;
    6. /**
    7. * @author ally-coding
    8. * @Date: 2023/10/17 0:40
    9. * @Project cetc_test
    10. * @Description: 粘包问题解决1-服务端
    11. */
    12. public class Server1 {
    13. private static final int BYTE_LENGTH = 1024;
    14. public static void main(String[] args) throws IOException {
    15. ServerSocket serverSocket = new ServerSocket(9091);
    16. //获取客户端连接
    17. Socket clientSocker = serverSocket.accept();
    18. //得到客户端发送的流对象
    19. try(InputStream inputStream = clientSocker.getInputStream()){
    20. while(true){
    21. //循环获取客户端发送的信息
    22. byte[] bytes = new byte[BYTE_LENGTH];
    23. // 读取客户端发送的信息
    24. int count = inputStream.read(bytes, 0, BYTE_LENGTH);
    25. if(count>0){
    26. System.out.println("接受到客户端的信息是:"+new String(bytes).trim());
    27. }
    28. count=0;
    29. }
    30. }
    31. }
    32. }

    客户端代码:

    1. package com.nien.test.sticky.solver1;
    2. import java.io.IOException;
    3. import java.io.OutputStream;
    4. import java.net.Socket;
    5. /**
    6. * @author ally-coding
    7. * @Date: 2023/10/17 0:40
    8. * @Project cetc_test
    9. * @Description: 粘包问题解决1-客户端
    10. */
    11. public class Client1 {
    12. private static final int BYTE_LENGTH=1024;
    13. public static void main(String[] args) throws IOException {
    14. Socket socket = new Socket("127.0.0.1",9091);
    15. final String messgae = "Hello world!";
    16. try(OutputStream outputStream = socket.getOutputStream()){
    17. byte[] bytes = new byte[BYTE_LENGTH];
    18. int idx = 0;
    19. for (byte b : messgae.getBytes()){
    20. bytes[idx] = b;
    21. idx++;
    22. }
    23. for (int i = 0; i < 10; i++) {
    24. outputStream.write(bytes, 0, BYTE_LENGTH);
    25. }
    26. }
    27. }
    28. }

    执行结果如下所示。

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

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

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

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

    1. package com.nien.test.sticky.solver2;
    2. import com.sun.org.apache.regexp.internal.RE;
    3. import java.io.IOException;
    4. import java.io.InputStream;
    5. import java.text.NumberFormat;
    6. /**
    7. * @author ally-coding
    8. * @Date: 2023/10/17 0:52
    9. * @Project cetc_test
    10. * @Description: 消息封装类
    11. */
    12. public class SocketPacket {
    13. static final int HEAD_SIZE=8;
    14. /**
    15. * 将协议封装为:协议头 + 协议体
    16. * @param content
    17. * @return
    18. */
    19. public byte[] toBytes(String content){
    20. //协议体 byte数据
    21. byte[] bodyByte = content.getBytes();
    22. int bodyByteLength = bodyByte.length;
    23. // 最终封装对象
    24. byte[] result = new byte[HEAD_SIZE + bodyByteLength];
    25. // 借助 NumberFormat 将 int 转换为 byte[]
    26. NumberFormat numberFormat = NumberFormat.getNumberInstance();
    27. numberFormat.setMinimumIntegerDigits(HEAD_SIZE);
    28. numberFormat.setGroupingUsed(false);
    29. //协议头 byte数组
    30. byte[] headByte = numberFormat.format(bodyByteLength).getBytes();
    31. // 封装协议头
    32. System.arraycopy(headByte, 0, result, 0, HEAD_SIZE);
    33. // 封装协议体
    34. System.arraycopy(bodyByte,0, result, HEAD_SIZE, bodyByteLength);
    35. return result;
    36. }
    37. /**
    38. * 获取消息头的内容(也就是消息体的长度)
    39. * @param inputStream
    40. * @return
    41. * @throws IOException
    42. */
    43. public int getHeader(InputStream inputStream) throws IOException {
    44. int result = 0;
    45. byte[] bytes = new byte[HEAD_SIZE];
    46. inputStream.read(bytes, 0, HEAD_SIZE);
    47. //得到消息体的字节长度
    48. result = Integer.valueOf(new String(bytes));
    49. return result;
    50. }
    51. }

    2.编写客户端代码

    1. package com.nien.test.sticky.solver2;
    2. import java.io.IOException;
    3. import java.io.OutputStream;
    4. import java.net.Socket;
    5. import java.util.Random;
    6. /**
    7. * @author ally-coding
    8. * @Date: 2023/10/17 1:30
    9. * @Project cetc_test
    10. * @Description: 客户端
    11. */
    12. public class Client2 {
    13. public static void main(String[] args) throws IOException {
    14. Socket socket = new Socket("127.0.0.1",9093);
    15. String[] message = {"Hello world","Hello java"};
    16. SocketPacket socketPacket = new SocketPacket();
    17. try(OutputStream outputStream = socket.getOutputStream()){
    18. for (int i = 0; i < 10; i++) {
    19. String msg = message[new Random().nextInt(message.length)];
    20. byte[] bytes = socketPacket.toBytes(msg);
    21. outputStream.write(bytes, 0, bytes.length);
    22. outputStream.flush();
    23. }
    24. }
    25. }
    26. }

    3.编写服务端

    1. package com.nien.test.sticky.solver2;
    2. import java.io.IOException;
    3. import java.io.InputStream;
    4. import java.net.ServerSocket;
    5. import java.net.Socket;
    6. import java.util.concurrent.LinkedBlockingQueue;
    7. import java.util.concurrent.ThreadPoolExecutor;
    8. import java.util.concurrent.TimeUnit;
    9. /**
    10. * @author ally-coding
    11. * @Date: 2023/10/17 9:19
    12. * @Project cetc_test
    13. * @Description: 服务器端 使用线程池来处理每个客户端的业务请求
    14. */
    15. public class Server2 {
    16. public static void main(String[] args) throws IOException {
    17. ServerSocket serverSocket = new ServerSocket(9093);
    18. // 获取客户端连接
    19. Socket clientSocket = serverSocket.accept();
    20. // 用线程池处理更多的客户端
    21. ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100,150,100,
    22. TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
    23. threadPoolExecutor.submit(()->{
    24. //客户端消息处理
    25. processMessage(clientSocket);
    26. });
    27. }
    28. private static void processMessage(Socket clientSocket){
    29. // Socket 封装对象
    30. SocketPacket socketPacket = new SocketPacket();
    31. // 获取客户端发送的消息对象
    32. try(InputStream inputStream = clientSocket.getInputStream()) {
    33. while (true){
    34. // 获取消息头(也就是消息体的长度)
    35. int bodyLength = socketPacket.getHeader(inputStream);
    36. // 消息体 byte 数组
    37. byte[] bodyBytes = new byte[bodyLength];
    38. // 每次实际读取字节数
    39. int readCount = 0;
    40. // 消息体赋值下标
    41. int bodyIndex = 0;
    42. // 循环接收消息头中定义的长度
    43. while (bodyIndex<=(bodyLength-1) &&
    44. (readCount = inputStream.read(bodyBytes, bodyIndex, bodyLength))!= -1){
    45. bodyIndex += readCount;
    46. }
    47. bodyIndex=0;
    48. // 成功接收到客户端的消息并打印
    49. System.out.println("接收到客户端的信息:" + new String(bodyBytes));
    50. }
    51. } catch (IOException e) {
    52. System.out.println(e.getMessage());
    53. }
    54. }
    55. }

    ​运行结果如下所示。

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

    方法3 以特殊的字符结尾

    代码实现:

    服务器代码:

    1. package com.nien.test.sticky.solver3;
    2. import java.io.BufferedReader;
    3. import java.io.IOException;
    4. import java.io.InputStreamReader;
    5. import java.net.ServerSocket;
    6. import java.net.Socket;
    7. import java.util.concurrent.LinkedBlockingQueue;
    8. import java.util.concurrent.ThreadPoolExecutor;
    9. import java.util.concurrent.TimeUnit;
    10. /**
    11. * @author ally-coding
    12. * @Date: 2023/10/18 0:40
    13. * @Project cetc_test
    14. * @Description:
    15. */
    16. public class Server3 {
    17. public static void main(String[] args) throws IOException {
    18. ServerSocket serverSocket = new ServerSocket(9092);
    19. // 获取客户端连接
    20. Socket clientSocket = serverSocket.accept();
    21. // 使用线程池处理更多的客户端
    22. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100,150,100,
    23. TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
    24. threadPool.submit(()->{
    25. //消息处理
    26. processMessage(clientSocket);
    27. });
    28. }
    29. /**
    30. * 消息处理
    31. * @param clientSocket
    32. */
    33. private static void processMessage(Socket clientSocket){
    34. // 获取客户端发送的消息流对象
    35. try(BufferedReader bufferedReader = new BufferedReader(
    36. new InputStreamReader(clientSocket.getInputStream()))) {
    37. while (true){
    38. // 按行读取客户端发送的消息
    39. String msg = bufferedReader.readLine();
    40. if(msg!=null){
    41. System.out.println("接收到客户端的信息:" + msg);
    42. }
    43. }
    44. } catch (IOException e) {
    45. e.printStackTrace();
    46. }
    47. }
    48. }

    客户端代码:

    1. package com.nien.test.sticky.solver3;
    2. import java.io.BufferedWriter;
    3. import java.io.IOException;
    4. import java.io.OutputStreamWriter;
    5. import java.net.Socket;
    6. /**
    7. * @author ally-coding
    8. * @Date: 2023/10/18 0:40
    9. * @Project cetc_test
    10. * @Description:
    11. */
    12. public class Client3 {
    13. public static void main(String[] args) throws IOException {
    14. // 启动 Socket 并尝试连接服务器
    15. Socket socket = new Socket("127.0.0.1", 9092);
    16. String message = "Hi,Java."; // 发送消息
    17. try (BufferedWriter bufferedWriter = new BufferedWriter(
    18. new OutputStreamWriter(socket.getOutputStream()))) {
    19. // 给服务器端发送 10 次消息
    20. for (int i = 0; i < 10; i++) {
    21. // 注意:结尾的 \n 不能省略,它表示按行写入
    22. bufferedWriter.write(message + "\n");
    23. // 刷新缓冲区(此步骤不能省略)
    24. bufferedWriter.flush();
    25. }
    26. }
    27. }
    28. }

    执行结果如下图所示。

    该方法最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性。
  • 相关阅读:
    一零四、大数据可视化技术与应用实训(展示大屏幕)
    vue中预览xml并高亮显示
    MATLAB数值计算与数据分析(3)
    Centos7 TiDB 数据库安装部署
    C语言实现用弦截法求 f(x)=x^3-5*x^2+16*x-80=0 的根
    洛谷 P1148 拱猪计分
    应用案例 | 基于三维机器视觉的机器人麻袋拆垛应用解决方案
    Java Heap Space: Understanding and Resolving Memory Issues
    C#对字典容器Dictionary<TKey, TValue>内容进行XML序列化或反序列化报错解决方法
    机器学习部分知识点总结
  • 原文地址:https://blog.csdn.net/Ally441/article/details/133905639