Tcp粘包和拆包

TCP 粘包和拆包是网络编程中常见的问题,本质是 TCP 协议面向字节流的特性导致的数据边界不明确。TCP 的 面向字节流特性数据传输就像水流一样连续,没有明确的“数据包”边界。这种设计让 TCP 能高效传输数据,但也导致应用层必须自己处理数据的“分段”逻辑。
TCP 协议本身 不感知应用层的数据结构,它只负责按顺序传输字节流。发送方的多次 write 操作和接收方的多次 read 操作之间没有一一对应的关系,导致数据可能被 合并(粘包) 或 拆分(拆包)。

想象两个水桶通过一根水管连接:
发送方:向水管中倒入多次水(多次 write 数据)。
接收方:从水管中舀水(多次 read 数据)。
问题:每次倒入的水量(数据包)和舀出的水量(读取的字节数)可能不一致,导致无法区分每次倒入的“一杯水”的边界。

粘包(TCP粘包问题)
粘包是指发送方连续发送多个数据包时,接收方无法准确分辨出每一个完整的数据包,导致接收方接收到的数据是多个消息粘在一起的情况。

例如,发送端发送了两个数据包,分别是“Hello”和“World”。但是,接收端收到的数据可能是“HelloWorld”——这就是粘包。

拆包(TCP拆包问题)
拆包是指发送的数据包太大,接收方不能一次性读取一个完整的数据包,导致接收方只能接收到部分数据,而剩余的部分需要继续接收。接收方的缓冲区通常会被分割成多个部分进行接收,可能导致拆包现象。

例如,发送端发送了一个大的数据包“HelloWorld123456”,而接收端的缓存限制只能一次读取到“HelloWorld”部分,剩下的“123456”需要再次读取。

为什么会出现粘包和拆包?
TCP是流式协议,没有消息边界。
数据包的大小不固定,网络状况不稳定时,会发生拆包或者粘包。
操作系统内核的缓冲区大小限制,可能会导致发送的数据被分割。
接收方的处理方式不当,比如没有正确地判断消息边界。

import java.io.*;
import java.net.*;

public class TCPClient {
    public static void main(String[] args) throws IOException {
        // 创建Socket连接
        Socket socket = new Socket("localhost", 8080);
        // 获取输出流
        OutputStream os = socket.getOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(os);

        // 发送两条消息
        String msg1 = "Hello";
        String msg2 = "World";
        
        bos.write(msg1.getBytes());
        bos.write(msg2.getBytes());
        bos.flush(); // 强制发送数据
        System.out.println("Message sent.");
        
        bos.close();
        socket.close();
    }
}
import java.io.*;
import java.net.*;

public class TCPServer {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket监听端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("Server started...");

        // 等待客户端连接
        Socket socket = serverSocket.accept();
        InputStream is = socket.getInputStream();
        BufferedInputStream bis = new BufferedInputStream(is);

        // 读取数据
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = bis.read(buffer)) != -1) {
            String received = new String(buffer, 0, bytesRead);
            System.out.println("Received message: " + received);
        }

        bis.close();
        socket.close();
    }
}

这个例子中,客户端发送了两条消息:“Hello” 和 “World”,但由于TCP协议没有消息边界,接收端无法明确知道数据的边界。在实际运行中,服务端可能会接收到两个消息连在一起,像这样:“HelloWorld”,这就是粘包问题。

如果客户端发送的数据比较大,接收端的缓冲区有限制,接收端可能只能接收一部分数据,剩余的部分会等待下一次读取,这就是拆包问题。

如何解决粘包和拆包问题?
固定长度的消息:每个消息固定长度,这样接收端可以按照固定的长度来读取数据。
特殊分隔符:在每个消息的末尾添加特殊字符(如\n),接收端根据分隔符来识别消息的结束。
自定义协议:通过在每个消息头中加入消息的长度信息,接收端根据这个长度来读取数据。

通过添加消息长度来解决拆包和粘包问题
我们可以给每个消息添加一个固定的头部,表示消息的长度。这样,接收端就可以知道每条消息的长度,从而正确地拆解数据。

public static void main(String[] args) throws IOException {
        // 创建Socket连接
        Socket socket = new Socket("localhost", 8080);
        // 获取输出流
        OutputStream os = socket.getOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(os);

        // 发送两条消息
        String msg1 = "Hello";
        String msg2 = "World";

        // 发送消息长度+消息内容
        sendMessage(bos, msg1);
        sendMessage(bos, msg2);

        bos.close();
        socket.close();
    }

    private static void sendMessage(BufferedOutputStream bos, String msg) throws IOException {
        byte[] msgBytes = msg.getBytes();
        int length = msgBytes.length;
        bos.write(length >> 8);  // 发送长度的高字节
        bos.write(length);       // 发送长度的低字节
        bos.write(msgBytes);     // 发送消息内容
        bos.flush();
    }
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket监听端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("Server started...");

        // 等待客户端连接
        Socket socket = serverSocket.accept();
        InputStream is = socket.getInputStream();
        BufferedInputStream bis = new BufferedInputStream(is);


        while (true) {
            int lengthHigh = bis.read();
            int lengthLow = bis.read();
            if (lengthHigh == -1 || lengthLow == -1) break;

            int length = (lengthHigh << 8) | lengthLow;
            byte[] msgBytes = new byte[length];
            bis.read(msgBytes, 0, length);

            String received = new String(msgBytes);
            System.out.println("Received message: " + received);
        }

        bis.close();
        socket.close();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值