MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TCP/IP协议栈中的分片与重组技术

2024-10-134.5k 阅读

TCP/IP协议栈中的分片与重组技术

网络传输的限制与分片需求

在网络通信中,数据并非是以一个完整的大块直接从源端传输到目的端。网络中存在各种限制因素,其中一个关键因素是链路层的最大传输单元(MTU,Maximum Transmission Unit)。MTU 定义了在网络层以下(通常是数据链路层)能够承载的最大数据包大小。例如,以太网的 MTU 一般是 1500 字节。

当应用层产生的数据量较大,形成的 IP 数据包大小超过了链路层的 MTU 时,就需要对数据包进行分片。假设一个应用层要发送 3000 字节的数据,在封装成 IP 数据包后(假设 IP 首部为 20 字节),总大小为 3020 字节,远远超过了以太网 1500 字节的 MTU。这种情况下,就必须将这个大的 IP 数据包分割成多个较小的数据包,每个数据包大小都在 MTU 限制范围内,才能在链路上传输。

IP 分片机制

  1. 分片字段 在 IP 首部中有几个字段与分片密切相关。IP 首部的格式如下(简化描述):
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- **Identification(标识)**:这个 16 位的字段用于标识一个特定的 IP 数据包。当一个大的 IP 数据包被分片后,所有的分片都具有相同的标识值。这样,目的端在重组分片时,可以依据这个标识来确认哪些分片属于同一个原始数据包。
- **Flags(标志)**:3 位的标志字段,其中最重要的两位是 MF(More Fragments)和 DF(Don't Fragment)。
    - **MF**:MF = 1 表示该分片不是最后一个分片,后面还有其他分片;MF = 0 表示这是最后一个分片。
    - **DF**:DF = 1 表示不允许分片,如果数据包大小超过 MTU 且 DF 位被设置,路由器将丢弃该数据包,并向源端发送一个 ICMP 差错报文,提示“需要进行分片但设置了不分片(DF)标志”。
- **Fragment Offset(片偏移)**:13 位的片偏移字段,它表示该分片在原始数据中的位置,以 8 字节为单位。例如,片偏移为 10,意味着该分片的数据是从原始数据的第 80 字节开始的。通过片偏移,目的端可以按照正确的顺序重组分片。

2. 分片过程 假设源端要发送一个 3020 字节的 IP 数据包(20 字节 IP 首部 + 3000 字节数据),而链路层 MTU 为 1500 字节。由于 IP 首部固定为 20 字节,所以每个分片最多能承载 1480 字节的数据。

第一个分片: - IP 首部:20 字节 - 数据:1480 字节 - MF = 1,表示后面还有分片 - 片偏移 = 0

第二个分片: - IP 首部:20 字节 - 数据:1480 字节 - MF = 1,表示后面还有分片 - 片偏移 = 1480 / 8 = 185

第三个分片: - IP 首部:20 字节 - 数据:40 字节(3000 - 1480 - 1480) - MF = 0,表示这是最后一个分片 - 片偏移 = (1480 + 1480) / 8 = 370

这些分片在网络中独立传输,可能会经过不同的路径,到达目的端的顺序也可能与发送顺序不同。

IP 重组机制

  1. 重组缓存 目的端在接收到分片后,需要将它们重组为原始的 IP 数据包。为了实现这一过程,目的端会维护一个重组缓存。当一个分片到达时,目的端首先检查其标识字段,以确定该分片所属的原始数据包。如果在重组缓存中还没有为该标识对应的数据包建立缓存空间,则创建一个新的缓存。

  2. 重组过程 根据分片的片偏移字段,目的端将分片的数据放置在重组缓存中的正确位置。例如,片偏移为 0 的分片数据放在缓存的起始位置,片偏移为 185 的分片数据放在从起始位置偏移 1480 字节(185 * 8)的位置。

当 MF = 0 的分片到达时,目的端知道所有的分片都已收齐,可以进行重组。它按照片偏移顺序将所有分片的数据拼接起来,再加上 IP 首部,就得到了原始的 IP 数据包。然后,这个完整的 IP 数据包就可以被传递到上层协议(如 TCP 或 UDP)进行进一步处理。

代码示例(以 C 语言为例,模拟分片与重组)

  1. 分片代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MTU 1500
#define IP_HEADER_SIZE 20

typedef struct {
    unsigned short identification;
    unsigned char MF : 1;
    unsigned char DF : 1;
    unsigned short fragmentOffset;
} FragmentInfo;

void fragmentPacket(char *originalPacket, int originalLength, FragmentInfo *fragments, int *fragmentLengths, int *numFragments) {
    int dataLength = originalLength - IP_HEADER_SIZE;
    int numFullFragments = dataLength / (MTU - IP_HEADER_SIZE);
    int remainingData = dataLength % (MTU - IP_HEADER_SIZE);

    *numFragments = numFullFragments;
    if (remainingData > 0) {
        (*numFragments)++;
    }

    int offset = 0;
    for (int i = 0; i < *numFragments; i++) {
        fragments[i].identification = 1234; // 假设一个标识值
        fragments[i].DF = 0;
        if (i < *numFragments - 1) {
            fragments[i].MF = 1;
            fragmentLengths[i] = MTU;
        } else {
            fragments[i].MF = 0;
            fragmentLengths[i] = IP_HEADER_SIZE + remainingData;
        }
        fragments[i].fragmentOffset = offset / 8;
        offset += fragmentLengths[i] - IP_HEADER_SIZE;
    }
}

int main() {
    char originalPacket[3020];
    memset(originalPacket, 0, sizeof(originalPacket));
    // 假设填充了 3000 字节的数据
    for (int i = IP_HEADER_SIZE; i < 3020; i++) {
        originalPacket[i] = i - IP_HEADER_SIZE;
    }

    FragmentInfo fragments[10];
    int fragmentLengths[10];
    int numFragments;

    fragmentPacket(originalPacket, 3020, fragments, fragmentLengths, &numFragments);

    for (int i = 0; i < numFragments; i++) {
        printf("Fragment %d: MF = %d, DF = %d, Offset = %d, Length = %d\n", i, fragments[i].MF, fragments[i].DF, fragments[i].fragmentOffset, fragmentLengths[i]);
    }

    return 0;
}
  1. 重组代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define IP_HEADER_SIZE 20

typedef struct {
    unsigned short identification;
    unsigned char MF : 1;
    unsigned char DF : 1;
    unsigned short fragmentOffset;
} FragmentInfo;

void reassemblePacket(char **fragments, FragmentInfo *fragmentInfos, int *fragmentLengths, int numFragments, char *reassembledPacket) {
    int totalLength = 0;
    for (int i = 0; i < numFragments; i++) {
        totalLength += fragmentLengths[i] - IP_HEADER_SIZE;
    }
    totalLength += IP_HEADER_SIZE;

    // 复制第一个分片的 IP 首部
    memcpy(reassembledPacket, fragments[0], IP_HEADER_SIZE);

    int offset = 0;
    for (int i = 0; i < numFragments; i++) {
        int fragmentDataLength = fragmentLengths[i] - IP_HEADER_SIZE;
        memcpy(reassembledPacket + IP_HEADER_SIZE + fragmentInfos[i].fragmentOffset * 8, fragments[i] + IP_HEADER_SIZE, fragmentDataLength);
    }
}

int main() {
    // 假设已经接收到了分片
    char *fragments[3];
    FragmentInfo fragmentInfos[3];
    int fragmentLengths[3];

    fragments[0] = (char *)malloc(1500);
    fragments[1] = (char *)malloc(1500);
    fragments[2] = (char *)malloc(60);

    // 假设填充了分片数据
    for (int i = 0; i < 1500; i++) {
        fragments[0][i] = i;
    }
    for (int i = 0; i < 1500; i++) {
        fragments[1][i] = i + 1500;
    }
    for (int i = 0; i < 60; i++) {
        fragments[2][i] = i + 3000;
    }

    fragmentInfos[0].identification = 1234;
    fragmentInfos[0].MF = 1;
    fragmentInfos[0].DF = 0;
    fragmentInfos[0].fragmentOffset = 0;
    fragmentLengths[0] = 1500;

    fragmentInfos[1].identification = 1234;
    fragmentInfos[1].MF = 1;
    fragmentInfos[1].DF = 0;
    fragmentInfos[1].fragmentOffset = 185;
    fragmentLengths[1] = 1500;

    fragmentInfos[2].identification = 1234;
    fragmentInfos[2].MF = 0;
    fragmentInfos[2].DF = 0;
    fragmentInfos[2].fragmentOffset = 370;
    fragmentLengths[2] = 60;

    char reassembledPacket[3020];
    reassemblePacket(fragments, fragmentInfos, fragmentLengths, 3, reassembledPacket);

    // 可以进一步检查重组后的数据包完整性
    for (int i = 0; i < 3020; i++) {
        printf("%d ", reassembledPacket[i]);
    }
    printf("\n");

    for (int i = 0; i < 3; i++) {
        free(fragments[i]);
    }

    return 0;
}

分片与重组中的问题与优化

  1. 超时问题 在重组过程中,如果某些分片长时间未到达,目的端不能无限期等待。通常会设置一个重组超时时间。如果在超时时间内未能收齐所有分片,目的端将丢弃已收到的分片,并释放重组缓存空间。这可以避免资源的浪费,但也可能导致数据丢失,尤其是在网络不稳定的情况下。
  2. 缓存管理 重组缓存需要合理管理,以避免内存耗尽。一方面,要确保能够容纳足够多的分片进行重组;另一方面,对于已经完成重组或超时的缓存,要及时释放空间。可以采用链表等数据结构来动态管理重组缓存,提高内存使用效率。
  3. 优化传输 在应用层,可以通过调整数据发送策略来减少分片的发生。例如,合理控制应用层数据包的大小,使其尽量接近但不超过 MTU。此外,一些网络协议(如 TCP)在传输层会进行拥塞控制和分段,也可以间接减少 IP 层的分片。

总结

分片与重组技术是 TCP/IP 协议栈中解决网络传输限制的重要机制。理解其原理对于网络开发、网络故障排查等都具有重要意义。通过本文的介绍以及代码示例,希望读者能够深入掌握这一技术,并在实际工作中灵活应用。同时,面对分片与重组过程中的各种问题,需要不断优化网络设计和代码实现,以提高网络通信的可靠性和效率。