TCP粘包问题

概念

TCP三大特性:面向连接、可靠传输和基于字节流,第三个特性面向字节流是指TCP协议属于流式协议,内容与内容之间没有明确的分界符号,需要人为去给消息协议定义边界规则,来区分不同的消息。举一个例子

  1. 发送端发送了两份数据,在传输层被划分为7个数据包,两份数据各占3.5个数据包,由于是字节流,两份数据的0.5个数据包之间没有边界
  2. 接收端接收到数据后,需要通过边界规则将0.5个数据包正确拼接到其他数据包还原两份完整数据

粘包根源

  • TCP协议:面向字节流是指TCP协议属于流式协议,内容与内容之间没有明确的分界符号,多个消息可能会被编码到同一个数据包中
  • 发送端:Nagle算法会将小数据包积累到一定大小后再进行发送,导致多个小包被合并为一个大包,从而产生粘包现象
  • 接收端:当接收端从 TCP 缓冲区中读取数据时,TCP 并不知道数据包的边界,因此接收到的数据可能会包含多个已粘在一起的数据包
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * TCP粘包问题演示 - 服务端
 * 模拟接收多个消息被粘在一起的情况
 */
public class TcpStickyPacketServer {

    private static final int PORT = 8888;

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("TCP粘包问题演示服务端启动,端口: " + PORT);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接: " + clientSocket.getRemoteSocketAddress());
                // 为每个客户端创建新线程处理
                new Thread(new ClientHandler(clientSocket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    static class ClientHandler implements Runnable {
        private final Socket clientSocket;
        public ClientHandler(Socket socket) {
            this.clientSocket = socket;
        }
        @Override
        public void run() {
            try (InputStream inputStream = clientSocket.getInputStream()) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    // 模拟粘包:一次性读取可能包含多个消息
                    String receivedData = new String(buffer, 0, bytesRead);
                    System.out.println("【服务端接收】原始数据长度: " + bytesRead + ", 内容: " + receivedData);
                    // 尝试解析消息(这里会出现粘包问题)
                    parseMessages(receivedData);
                }
            } catch (IOException e) {
                System.out.println("客户端断开连接");
            } finally {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 有问题的消息解析方法 - 无法正确处理粘包
         */
        private void parseMessages(String data) {
            // 假设每个消息应该是 "Message X" 的格式
            // 但由于粘包,可能收到 "Message 1Message 2Message 3"
            System.out.println("【解析结果】无法正确分割消息: " + data);
            System.out.println("----------------------------------------");
        }
    }
}

解决方案

解决粘包问题的关键就是:定义消息边界,具体有以下方案:

  • 固定消息长度:消息长度固定,适合各类消息长度近似的场景,一般不会使用,会造成大量的空间浪费
  • 消息分隔符:通过一个特殊符号标识消息的末尾,其不足之处在于,如果消息体本身包含这些分隔符,会导致消息被错误解析,因此需要对分隔符进行转义
  • 消息头+消息体:消息的头部添加长度字段,表示消息的总长度,接收端可以先读取长度字段,再根据该长度读取完整的消息。这种方法较为通用且适用于各种长度的消息
posted @ 2025-11-13 20:48  xxs不是小学生  阅读(1)  评论(0)    收藏  举报