【必会面试题】TCP协议的粘包拆包

目录

TCP协议的粘包和拆包是数据传输过程中常见的现象,它们不是TCP协议本身的设计目的,而是基于TCP协议的特性自然产生的结果。

TCP数据报文结构

字段名 English Name 长度(比特) 描述
源端口号 Source Port 16 发送方的端口号,用于标识发送数据的应用程序
目标端口号 Destination Port 16 接收方的端口号,用于标识接收数据的应用程序
序列号 Sequence Number 32 发送的数据段的第一个字节的序列号,用于数据排序与确认
确认号 Acknowledgment Number 32 确认收到对方的序列号,期望收到的下一个数据段的第一个字节序号
数据偏移(DO) Data Offset 4 报头长度,指示TCP报头有多少个32位字(单位不是比特),最小值5
保留 Reserved 6 未使用,保留为今后使用,目前应置为0
URG Urgent 1 紧急指针有效标志
ACK Acknowledge 1 确认标志,表示确认字段有效
PSH Push 1 提示接收方应该尽快将数据交付给上层协议处理
RST Reset 1 复位连接标志,用于异常终止连接
SYN Synchronize 1 同步序号,用于建立连接
FIN Finish 1 结束标志,表示发送方已经完成数据发送任务,可以关闭连接
窗口大小 Window Size 16 通知对方其接收缓冲区的大小
校验和 Checksum 16 包含TCP报头和数据部分的校验和,用于错误检测
紧急指针 Urgent Pointer 16 只有当URG标志为1时有意义,指向紧急数据的最后一个字节后续的第一个字节
选项与填充 Options & Padding 可变 可选字段,用于扩展功能,如最大报文段大小(MSS)等,不足4字节需填充
  • 一个示例
java 复制代码
0000 0000 0000 0001 0000 0000 0000 0010  # 源端口:1,目的端口:2
0000 0000 0000 0000 0000 0000 0000 0001  # 序列号:1
0000 0000 0000 0000 0000 0000 0000 0002  # 确认号:2
0101 0000 0000 0000 0000 0000 0000 0000  # 数据偏移:5,保留:0,控制位:010(SYN, ACK)
0000 0000 0000 0010 0000 0000 0000 0000  # 窗口大小:2
0000 0000 0000 0000 0000 0000 0000 0000  # 校验和:0(示例值,实际计算)
0000 0000 0000 0000 0000 0000 0000 0000  # 紧急指针:0
0000 0000 0000 0000 0000 0000 0000 0000  # 选项:无
0101 0101 0101 0101 0101 0101 0101 0101  # 数据:"Hello"的ASCII编码

粘包

概念:在TCP协议中,粘包指的是多个TCP报文段在传输过程中被接收端连续接收,而没有明显的边界区分,看起来就像是一个连续的数据包。这是因为TCP是一个面向流的协议,它不对每个应用层消息单独封装。

  • 假设客户端发送了两个数据包,分别是"Hello"和"World"。在没有粘包的情况下,它们应该分别发送。但在粘包的情况下,它们可能被合并为一个数据包发送。
  1. 客户端发送的数据包1("Hello"):
java 复制代码
0000 0000 0000 0001 0000 0000 0000 0010  # 源端口:1,目的端口:2
0000 0000 0000 0000 0000 0000 0000 0001  # 序列号:1
0000 0000 0000 0000 0000 0000 0000 0002  # 确认号:2
... # 其他TCP头部字段
0101 0101 0101 0101 0101  # 数据:"Hello"的ASCII编码
  1. 客户端发送的数据包2("World"):
java 复制代码
0000 0000 0000 0001 0000 0000 0000 0010  # 源端口:1,目的端口:2
0000 0000 0000 0001 0000 0000 0000 0006  # 序列号:6(假设)
0000 0000 0000 0000 0000 0000 0000 0007  # 确认号:7(假设)
... # 其他TCP头部字段
0101 0111 0111 0111 0110 1100  # 数据:"World"的ASCII编码

在粘包的情况下,接收方可能收到如下的数据包:

java 复制代码
0101 0101 0101 0101 0101 0101 1110 1100  # 数据:"HelloWorld"的ASCII编码

拆包

概念:与粘包相反,拆包是指发送端的一个较大的TCP数据包在传输过程中被分割成多个较小的数据包到达接收端。这是因为网络层和传输层(TCP)有各自的MTU(最大传输单元)限制,数据包必须适应这些限制才能在网络中传输。

原因

  • MTU限制:不同网络设备和链路可能有不同的MTU,数据包必须被拆分以适应最小的MTU。
  • 拥塞控制:TCP的拥塞控制机制也可能导致数据被拆分,以减缓发送速度,避免网络拥塞。

如何处理粘包和拆包

TCP协议的粘包拆包可能会导致以下问题:

  1. 数据丢失:在拆包过程中,如果某个分片的数据丢失,可能导致整个原始数据无法还原。
  2. 数据错误:粘包情况下,接收端可能将多个数据包误认为是单个数据包,从而导致数据解析错误。
  3. 性能下降:为了解决粘包拆包问题,需要额外的处理机制,这会增加系统的复杂性和计算开销。

一般解决这些问题有以下几种思路:

  1. 定长包头:在每个数据包前加上固定长度的包头,包头中包含数据包的实际长度。这样,接收端可以根据包头中的长度信息来分割数据包。
java 复制代码
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    private final int frameLength;

    public FixedLengthFrameDecoder(int frameLength) {
        this.frameLength = frameLength;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes()< frameLength) {
            return;
        }

        in.markReaderIndex(); // 标记当前读指针位置
        int dataLength = in.readInt(); // 读取数据长度

        if (dataLength > frameLength - 4) { // 检查数据长度是否合法
            in.resetReaderIndex(); // 不合法时重置读指针
            throw new CorruptedFrameException("data length is larger than the frame length.");
        }

        byte[] data = new byte[dataLength];
        in.readBytes(data);
        out.add(data);
    }
}
  1. 分隔符:在数据包之间使用特殊的分隔符作为标识,接收端根据分隔符来分割数据包。这种方法适用于数据包内容不包含分隔符的场景。
java 复制代码
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
    private static final byte DELIMITER = '#';

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int readableBytes = in.readableBytes();
        int readIndex = in.readerIndex();

        while (readableBytes > 0) {
            byte nextByte = in.getByte(readIndex);
            if (nextByte == DELIMITER) {
                break;
            }
            readableBytes--;
            readIndex++;
        }

        if (readableBytes == 0) {
            return;
        }

        int dataLength = readIndex - in.readerIndex();
        byte[] data = new byte[dataLength];
        in.readBytes(data);
        out.add(data);
    }
}
  1. 长度前缀:在数据包前加上表示数据长度的字段,接收端根据长度字段来分割数据包。这种方法适用于数据包长度可变的情况。
java 复制代码
public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
    private static final int LENGTH_FIELD_LENGTH = 4;
    private static final int LENGTH_FIELD_OFFSET = 0;
    private static final int LENGTH_ADJUSTMENT = 0;
    private static final int INITIAL_BYTES_TO_STRIP = LENGTH_FIELD_LENGTH;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < LENGTH_FIELD_LENGTH) {
            return;
        }

        in.markReaderIndex(); // 标记当前读指针位置
        int dataLength = in.readInt(); // 读取数据长度

        if (dataLength < 0) { // 检查数据长度是否合法
            in.resetReaderIndex(); // 不合法时重置读指针
            throw new CorruptedFrameException("negative length: " + dataLength);
        }

        if (in.readableBytes()< dataLength + LENGTH_FIELD_LENGTH) {
            in.resetReaderIndex(); // 如果可读字节不足以容纳整个数据包,重置读指针
            return;
        }

        in.readerIndex(in.readerIndex() + LENGTH_FIELD_LENGTH); // 跳过长度字段
        byte[] data = new byte[dataLength];
        in.readBytes(data);
        out.add(data);
    }
}

一般我们通过现有框架如Netty能更方便的解决这些问题。
Netty提供了多种解码器,如FixedLengthFrameDecoder、DelimiterBasedFrameDecoder和LengthFieldBasedFrameDecoder,可以更有效地处理粘包拆包问题。

相关推荐
海绵波波10740 分钟前
Webserver(4.3)TCP通信实现
服务器·网络·tcp/ip
幺零九零零4 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
热爱跑步的恒川4 小时前
【论文复现】基于图卷积网络的轻量化推荐模型
网络·人工智能·开源·aigc·ai编程
云飞云共享云桌面5 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
音徽编程7 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
幺零九零零8 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
23zhgjx-NanKon8 小时前
华为eNSP:QinQ
网络·安全·华为
23zhgjx-NanKon8 小时前
华为eNSP:mux-vlan
网络·安全·华为
点点滴滴的记录9 小时前
RPC核心实现原理
网络·网络协议·rpc
Lionhacker9 小时前
网络工程师这个行业可以一直干到退休吗?
网络·数据库·网络安全·黑客·黑客技术