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

TCP/IP协议栈在C语言中的实现实践

2021-07-051.3k 阅读

TCP/IP 协议栈基础概念

TCP/IP 协议族概述

TCP/IP(Transmission Control Protocol/Internet Protocol)协议族是一组用于实现网络通信的协议集合。它起源于 20 世纪 70 年代美国国防部高级研究计划局(ARPA)的网络研究项目,如今已成为互联网通信的事实上的标准。TCP/IP 协议族包含了多个层次的协议,从底层的网络接口层到高层的应用层,每个层次都有其特定的功能和职责。

TCP/IP 协议栈层次结构

  1. 网络接口层:这是 TCP/IP 协议栈的最底层,负责与物理网络进行交互。它处理物理介质上的信号传输、帧的封装和解封装等任务。常见的网络接口层协议包括以太网协议、PPP(Point - to - Point Protocol)等。以太网协议用于局域网环境,定义了数据帧的格式以及如何在以太网上传输数据。PPP 则常用于拨号上网等点到点连接场景。
  2. 网络层:网络层的主要协议是 IP(Internet Protocol),其核心功能是实现网络寻址和路由选择。IP 协议为每个网络设备分配唯一的 IP 地址,使得数据能够在不同的网络之间进行传输。在这一层,还涉及到一些辅助协议,如 ICMP(Internet Control Message Protocol)用于网络诊断和控制信息的传输,ARP(Address Resolution Protocol)用于将 IP 地址解析为物理地址(如以太网 MAC 地址)。
  3. 传输层:传输层有两个主要协议,即 TCP(Transmission Control Protocol)和 UDP(User Datagram Protocol)。TCP 是一种面向连接的、可靠的传输协议。它通过三次握手建立连接,在数据传输过程中保证数据的有序性、完整性,并能够进行流量控制和拥塞控制。UDP 则是一种无连接的、不可靠的传输协议,它不保证数据的可靠交付和顺序,但具有简单、高效的特点,适用于一些对实时性要求较高但对数据准确性要求相对较低的应用场景,如音频和视频流传输。
  4. 应用层:应用层包含了各种面向用户应用的协议,如 HTTP(Hypertext Transfer Protocol)用于网页传输、SMTP(Simple Mail Transfer Protocol)用于邮件发送、FTP(File Transfer Protocol)用于文件传输等。这些协议定义了应用程序之间通信的规则和数据格式。

在 C 语言中实现 TCP/IP 协议栈相关功能

网络编程基础 - Socket 编程

在 C 语言中进行网络编程,最常用的方式是使用 Socket 接口。Socket 是一种进程间通信(IPC)机制,它提供了一种在不同主机或同一主机上的不同进程之间进行网络通信的方法。

  1. Socket 类型

    • 流套接字(SOCK_STREAM):基于 TCP 协议,提供可靠的、面向连接的数据传输。适用于对数据准确性和顺序要求较高的应用,如文件传输、远程登录等。
    • 数据报套接字(SOCK_DGRAM):基于 UDP 协议,提供无连接的数据传输。数据以数据报的形式发送,不保证可靠交付和顺序。适用于对实时性要求较高、对数据准确性要求相对较低的应用,如实时视频流、音频流等。
    • 原始套接字(SOCK_RAW):允许直接访问网络层和传输层协议,可用于开发自定义协议或进行网络测试、网络监控等高级应用。
  2. Socket 创建与初始化: 在使用 Socket 进行通信之前,需要先创建 Socket。以下是创建 TCP 流套接字的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define IP_ADDRESS "127.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&servaddr.sin_zero, '\0', sizeof(servaddr.sin_zero));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("Connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = "Hello, server!";
    send(sockfd, buffer, strlen(buffer), 0);

    memset(buffer, 0, sizeof(buffer));
    recv(sockfd, buffer, sizeof(buffer), 0);
    printf("Received from server: %s\n", buffer);

    close(sockfd);
    return 0;
}

在上述代码中,首先使用 socket 函数创建了一个 TCP 流套接字。socket 函数的第一个参数 AF_INET 表示使用 IPv4 地址族,第二个参数 SOCK_STREAM 表示创建的是流套接字,第三个参数 0 表示使用默认协议(这里是 TCP)。

然后,填充 servaddr 结构体,该结构体包含了服务器的地址信息,包括地址族、端口号和 IP 地址。htons 函数用于将主机字节序的端口号转换为网络字节序,inet_addr 函数将点分十进制表示的 IP 地址转换为网络字节序的二进制形式。

接着,使用 connect 函数连接到服务器。如果连接成功,就可以通过 send 函数向服务器发送数据,通过 recv 函数从服务器接收数据。最后,使用 close 函数关闭套接字。

  1. UDP Socket 示例: 以下是一个简单的 UDP 客户端示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define IP_ADDRESS "127.0.0.1"
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[BUFFER_SIZE];

    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&servaddr.sin_zero, '\0', sizeof(servaddr.sin_zero));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);

    char *msg = "Hello, UDP server!";
    sendto(sockfd, (const char *)msg, strlen(msg), MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("Message sent.\n");

    socklen_t len = sizeof(servaddr);
    recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&servaddr, &len);
    buffer[BUFFER_SIZE - 1] = '\0';
    printf("Received from server: %s\n", buffer);

    close(sockfd);
    return 0;
}

在这个 UDP 客户端代码中,同样先使用 socket 函数创建了一个 UDP 套接字,不过第二个参数为 SOCK_DGRAM。然后,通过 sendto 函数向服务器发送数据,sendto 函数需要指定目标地址和端口。recvfrom 函数用于从服务器接收数据,它会返回接收到的数据以及发送方的地址信息。

实现简单的 TCP 服务器

  1. 基本原理: TCP 服务器通过监听特定的端口,等待客户端的连接请求。当客户端发起连接请求时,服务器接受该连接,然后可以与客户端进行数据交互。在 C 语言中,使用 Socket 接口可以实现 TCP 服务器的功能。

  2. 代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BACKLOG 5
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd, connfd;
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(sockfd, BACKLOG) < 0) {
        perror("Listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    socklen_t len = sizeof(cliaddr);
    // 接受客户端连接
    connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    if (connfd < 0) {
        perror("Accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[BUFFER_SIZE];
    memset(buffer, 0, sizeof(buffer));
    recv(connfd, buffer, sizeof(buffer), 0);
    printf("Received from client: %s\n", buffer);

    char *response = "Hello, client! Server received your message.";
    send(connfd, response, strlen(response), 0);

    close(connfd);
    close(sockfd);
    return 0;
}

在上述代码中,首先创建了一个 TCP 套接字。然后,使用 bind 函数将套接字绑定到指定的地址和端口。INADDR_ANY 表示服务器可以接受来自任何网络接口的连接。

接着,使用 listen 函数开始监听连接,BACKLOG 参数指定了等待连接队列的最大长度。当有客户端连接请求到达时,accept 函数会接受该连接,并返回一个新的套接字 connfd,用于与客户端进行通信。

通过 recv 函数从客户端接收数据,通过 send 函数向客户端发送响应数据。最后,关闭与客户端的连接套接字 connfd 和监听套接字 sockfd

实现简单的 UDP 服务器

  1. 基本原理: UDP 服务器不像 TCP 服务器那样需要建立连接。它只需绑定到特定的端口,然后等待接收来自客户端的数据报。接收到数据报后,可以根据需要进行处理并返回响应。

  2. 代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    char buffer[BUFFER_SIZE];

    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DUDP, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    socklen_t len = sizeof(cliaddr);
    recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
    buffer[BUFFER_SIZE - 1] = '\0';
    printf("Received from client: %s\n", buffer);

    char *response = "Hello, UDP client! Server received your message.";
    sendto(sockfd, (const char *)response, strlen(response), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);

    close(sockfd);
    return 0;
}

此代码创建了一个 UDP 套接字,并将其绑定到指定的端口。通过 recvfrom 函数接收来自客户端的数据报,同时获取客户端的地址信息。然后,通过 sendto 函数向客户端发送响应数据报。最后关闭套接字。

深入 TCP 协议实现细节

TCP 三次握手过程剖析

  1. 第一次握手: 客户端向服务器发送一个 SYN(Synchronize)包,该包中包含客户端的初始序列号(Sequence Number,简称 seq)。这个序列号是随机生成的,用于标识数据段在数据流中的位置。客户端进入 SYN_SENT 状态,等待服务器的响应。

  2. 第二次握手: 服务器接收到客户端的 SYN 包后,会回复一个 SYN + ACK 包。其中,SYN 部分是服务器的初始序列号,ACK 部分是对客户端 SYN 包的确认,确认号(Acknowledgment Number,简称 ack)为客户端的序列号加 1。服务器进入 SYN_RCVD 状态。

  3. 第三次握手: 客户端接收到服务器的 SYN + ACK 包后,向服务器发送一个 ACK 包。这个 ACK 包的确认号为服务器的序列号加 1,序列号为客户端在第一次握手中发送的序列号加 1。服务器接收到这个 ACK 包后,双方的连接建立成功,客户端和服务器都进入 ESTABLISHED 状态,可以开始进行数据传输。

在 C 语言中模拟 TCP 三次握手(简单示意)

虽然无法完全模拟底层的 TCP 三次握手过程,但可以通过 Socket 编程的行为来近似展示其原理。以下代码展示了一个简化的 TCP 连接建立过程:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define IP_ADDRESS "127.0.0.1"

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&servaddr.sin_zero, '\0', sizeof(servaddr.sin_zero));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);

    // 发起连接(类似第一次握手)
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("Connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("First 'handshake' (connect) successful.\n");

    char buffer[1024] = "SYN from client";
    send(sockfd, buffer, strlen(buffer), 0);
    printf("SYN sent from client.\n");

    memset(buffer, 0, sizeof(buffer));
    recv(sockfd, buffer, sizeof(buffer), 0);
    if (strstr(buffer, "SYN + ACK from server")!= NULL) {
        printf("Received SYN + ACK from server.\n");
    }

    buffer[0] = 'A';
    buffer[1] = 'C';
    buffer[2] = 'K';
    buffer[3] = '\0';
    send(sockfd, buffer, strlen(buffer), 0);
    printf("ACK sent from client.\n");

    close(sockfd);
    return 0;
}

在这个代码中,通过 connect 函数模拟了客户端发起的第一次握手(连接请求)。然后,客户端发送一个类似 SYN 的消息,接着接收服务器的类似 SYN + ACK 的响应,最后发送 ACK 消息,近似地展示了 TCP 三次握手的过程。

TCP 数据传输与滑动窗口机制

  1. 滑动窗口原理: TCP 使用滑动窗口机制来实现流量控制和提高数据传输效率。发送方和接收方都有一个滑动窗口,窗口大小表示可以发送或接收的未确认数据的数量。

发送方在发送数据时,会将数据放入窗口中,并启动定时器。当接收到接收方的确认(ACK)时,窗口向前滑动,允许发送更多的数据。如果在定时器超时之前没有收到 ACK,发送方会重传未确认的数据。

接收方通过调整窗口大小来通知发送方自己的接收能力。如果接收方的缓冲区已满,它会减小窗口大小,发送方会相应地减少发送数据的速率,从而避免数据丢失。

  1. 在 C 语言实现中体现滑动窗口概念(简单示例)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define IP_ADDRESS "127.0.0.1"
#define WINDOW_SIZE 5

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&servaddr.sin_zero, '\0', sizeof(servaddr.sin_zero));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("Connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char data[WINDOW_SIZE][1024];
    for (int i = 0; i < WINDOW_SIZE; i++) {
        snprintf(data[i], sizeof(data[i]), "Data segment %d", i);
    }

    int window_start = 0;
    int window_end = WINDOW_SIZE;
    int ack_received[WINDOW_SIZE] = {0};

    while (window_start < WINDOW_SIZE) {
        for (int i = window_start; i < window_end; i++) {
            send(sockfd, data[i], strlen(data[i]), 0);
            printf("Sent: %s\n", data[i]);
        }

        for (int i = window_start; i < window_end; i++) {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            recv(sockfd, buffer, sizeof(buffer), 0);
            if (strstr(buffer, "ACK")!= NULL) {
                int segment_num = atoi(buffer + 3);
                ack_received[segment_num] = 1;
                printf("Received ACK for segment %d\n", segment_num);
            }
        }

        while (ack_received[window_start]) {
            window_start++;
            window_end++;
            if (window_end > WINDOW_SIZE) {
                window_end = WINDOW_SIZE;
            }
        }
    }

    close(sockfd);
    return 0;
}

在这个示例中,模拟了发送方的滑动窗口机制。发送方将数据分成多个数据段,按照窗口大小发送数据,并等待接收方的 ACK。接收到 ACK 后,更新窗口的起始位置,允许发送更多的数据。

深入 UDP 协议实现细节

UDP 数据报结构

UDP 数据报由首部和数据两部分组成。首部长度固定为 8 字节,包含以下字段:

  1. 源端口号(16 位):标识发送方的端口号。
  2. 目的端口号(16 位):标识接收方的端口号。
  3. 长度(16 位):整个 UDP 数据报的长度,包括首部和数据部分,最小值为 8(仅首部)。
  4. 校验和(16 位):用于检测数据报在传输过程中是否发生错误。校验和的计算包括 UDP 首部、数据以及一个伪首部(包含源 IP 地址、目的 IP 地址、协议类型和 UDP 长度)。

UDP 校验和计算在 C 语言中的实现

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// 计算 UDP 校验和
uint16_t calculate_udp_checksum(const void *buf, size_t len) {
    uint32_t sum = 0;
    const uint16_t *ptr = (const uint16_t *)buf;

    while (len > 1) {
        sum += *ptr++;
        len -= 2;
    }

    if (len > 0) {
        sum += *(const uint8_t *)ptr;
    }

    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }

    return ~sum;
}

int main() {
    char udp_data[] = "Hello, UDP!";
    uint16_t checksum = calculate_udp_checksum(udp_data, sizeof(udp_data));
    printf("Calculated UDP checksum: 0x%04x\n", checksum);
    return 0;
}

在上述代码中,calculate_udp_checksum 函数用于计算 UDP 数据报的校验和。它先将数据按 16 位进行累加,最后处理剩余的字节。通过循环将高位的和加到低位,最终取反得到校验和。

UDP 应用场景与优缺点分析

  1. 应用场景

    • 实时音视频传输:如视频会议、在线直播等应用,对实时性要求极高,少量的数据丢失对整体质量影响较小,UDP 能够满足这种快速传输的需求。
    • 网络游戏:游戏中的实时状态更新、玩家操作指令等数据需要快速传输,UDP 协议的低延迟特性使其成为网络游戏开发的常用选择。
    • DNS(Domain Name System)查询:DNS 查询通常要求快速响应,UDP 可以在较短的时间内完成查询请求和响应,提高 DNS 服务的效率。
  2. 优点

    • 简单高效:UDP 协议相对简单,没有 TCP 那样复杂的连接建立、流量控制和拥塞控制机制,因此在数据传输过程中开销较小,能够快速地发送数据。
    • 低延迟:由于不需要等待确认和进行复杂的控制,UDP 可以更快地将数据发送出去,适用于对实时性要求高的应用场景。
  3. 缺点

    • 不可靠:UDP 不保证数据的可靠交付,可能会出现数据丢失、重复或乱序的情况。
    • 缺乏流量控制和拥塞控制:在网络拥塞时,UDP 不会主动降低发送速率,可能导致网络状况进一步恶化。

网络层 IP 协议相关实现

IP 数据报结构

IP 数据报由首部和数据两部分组成。IPv4 首部长度可变,最小为 20 字节,包含以下主要字段:

  1. 版本(4 位):标识 IP 协议的版本,通常为 4(IPv4)。
  2. 首部长度(4 位):以 4 字节为单位,表示 IP 首部的长度。
  3. 服务类型(8 位):用于指定对数据报的处理方式,如优先级、延迟、吞吐量等。
  4. 总长度(16 位):整个 IP 数据报的长度,包括首部和数据部分,最大值为 65535 字节。
  5. 标识(16 位):用于唯一标识一个数据报,在分片和重组时使用。
  6. 标志(3 位):其中包括是否允许分片、是否是最后一片等标志。
  7. 片偏移(13 位):表示该片在原始数据报中的位置,以 8 字节为单位。
  8. 生存时间(8 位):数据报在网络中可以经过的最大跳数,每经过一个路由器,TTL 值减 1,当 TTL 为 0 时,数据报将被丢弃。
  9. 协议(8 位):标识上层协议,如 6 表示 TCP,17 表示 UDP。
  10. 首部校验和(16 位):用于检测 IP 首部在传输过程中是否发生错误。

在 C 语言中构建简单的 IP 数据报(部分字段填充示例)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <arpa/inet.h>

// 构建简单的 IP 数据报
void build_ip_datagram(uint8_t *ip_datagram, uint32_t source_ip, uint32_t dest_ip) {
    // 版本和首部长度
    ip_datagram[0] = (4 << 4) | (5);

    // 服务类型
    ip_datagram[1] = 0;

    // 总长度(假设数据部分为 100 字节)
    uint16_t total_length = htons(20 + 100);
    memcpy(ip_datagram + 2, &total_length, 2);

    // 标识
    uint16_t identification = htons(1234);
    memcpy(ip_datagram + 4, &identification, 2);

    // 标志和片偏移
    ip_datagram[6] = 0;
    ip_datagram[7] = 0;

    // 生存时间
    ip_datagram[8] = 64;

    // 协议(假设为 UDP)
    ip_datagram[9] = 17;

    // 首部校验和(暂时设为 0,实际需要计算)
    ip_datagram[10] = 0;
    ip_datagram[11] = 0;

    // 源 IP 地址
    memcpy(ip_datagram + 12, &source_ip, 4);

    // 目的 IP 地址
    memcpy(ip_datagram + 16, &dest_ip, 4);
}

int main() {
    uint8_t ip_datagram[1500];
    uint32_t source_ip = inet_addr("192.168.1.1");
    uint32_t dest_ip = inet_addr("192.168.1.2");

    build_ip_datagram(ip_datagram, source_ip, dest_ip);

    // 这里可以进一步处理数据部分和计算首部校验和等操作

    return 0;
}

在这个代码中,展示了如何填充 IP 数据报的部分字段,如版本、首部长度、服务类型、总长度等。实际应用中,还需要根据具体需求进一步完善数据报的构建,包括计算首部校验和、添加数据部分等操作。

IP 路由选择基础概念与简单实现思路

  1. 路由选择概念: IP 路由选择是指在网络中确定数据报从源主机到目的主机所经过路径的过程。路由器根据路由表中的信息来决定如何转发数据报。路由表包含了目的网络地址、下一跳地址以及度量值等信息。度量值用于表示到达目的网络的代价,常见的度量值有跳数、带宽、延迟等。

  2. 简单路由选择实现思路(基于静态路由表): 在 C 语言中,可以通过结构体数组来表示静态路由表。以下是一个简单的示例代码框架:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <arpa/inet.h>

// 定义路由表项结构体
typedef struct {
    uint32_t destination_network;
    uint32_t subnet_mask;
    uint32_t next_hop;
    int metric;
} RouteEntry;

// 初始化路由表
void init_routing_table(RouteEntry *routing_table, int num_entries) {
    // 示例初始化
    routing_table[0].destination_network = inet_addr("192.168.1.0");
    routing_table[0].subnet_mask = inet_addr("255.255.255.0");
    routing_table[0].next_hop = inet_addr("192.168.0.1");
    routing_table[0].metric = 1;

    // 其他表项初始化...
}

// 根据目的 IP 查找下一跳
uint32_t find_next_hop(RouteEntry *routing_table, int num_entries, uint32_t dest_ip) {
    for (int i = 0; i < num_entries; i++) {
        if ((dest_ip & routing_table[i].subnet_mask) == routing_table[i].destination_network) {
            return routing_table[i].next_hop;
        }
    }
    return 0; // 未找到匹配路由
}

int main() {
    RouteEntry routing_table[10];
    int num_entries = 3;

    init_routing_table(routing_table, num_entries);

    uint32_t dest_ip = inet_addr("192.168.1.10");
    uint32_t next_hop = find_next_hop(routing_table, num_entries, dest_ip);

    if (next_hop) {
        char next_hop_str[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &next_hop, next_hop_str, INET_ADDRSTRLEN);
        printf("Next hop for destination %s is %s\n", inet_ntoa((struct in_addr){dest_ip}), next_hop_str);
    } else {
        printf("No route found for destination %s\n", inet_ntoa((struct in_addr){dest_ip}));
    }

    return 0;
}

在这个示例中,定义了一个 RouteEntry 结构体来表示路由表项,包括目的网络地址、子网掩码、下一跳地址和度量值。init_routing_table 函数用于初始化路由表,find_next_hop 函数根据目的 IP 地址查找对应的下一跳地址。

网络接口层相关实现

以太网帧结构

以太网帧是在以太网网络中传输的数据单元,其结构如下:

  1. 目的 MAC 地址(6 字节):接收方的物理地址。
  2. 源 MAC 地址(6 字节):发送方的物理地址。
  3. 类型/长度(2 字节):如果值大于 1500,则表示上层协议类型,如 0x0800 表示 IP 协议;如果值小于等于 1500,则表示以太网帧的数据部分长度。
  4. 数据(46 - 1500 字节):包含上层协议的数据,如 IP 数据报。
  5. 帧校验序列(4 字节):用于检测帧在传输过程中是否发生错误。

在 C 语言中构建简单的以太网帧(部分字段填充示例)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// 构建简单的以太网帧
void build_ethernet_frame(uint8_t *ethernet_frame, const uint8_t *dest_mac, const uint8_t *source_mac) {
    // 目的 MAC 地址
    memcpy(ethernet_frame, dest_mac, 6);

    // 源 MAC 地址
    memcpy(ethernet_frame + 6, source_mac, 6);

    // 类型(假设为 IP 协议)
    uint16_t type = 0x0800;
    ethernet_frame[12] = (type >> 8) & 0xFF;
    ethernet_frame[13] = type & 0xFF;

    // 这里可以进一步添加数据部分和计算帧校验序列等操作
}

int main() {
    uint8_t ethernet_frame[1518];
    uint8_t dest_mac[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
    uint8_t source_mac[] = {0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB};

    build_ethernet_frame(ethernet_frame, dest_mac, source_mac);

    // 进一步处理数据部分和帧校验序列等

    return 0;
}

在这个代码中,展示了如何填充以太网帧的目的 MAC 地址、源 MAC 地址和类型字段。实际应用中,还需要添加数据部分和计算帧校验序列等操作。

ARP 协议原理与简单实现思路

  1. ARP 协议原理: ARP(Address Resolution Protocol)用于将 IP 地址解析为物理地址(如以太网 MAC 地址)。当主机需要向另一个主机发送数据时,如果它只知道目标主机的 IP 地址,就会发送一个 ARP 请求广播包,包含自己的 IP 地址和 MAC 地址以及目标主机的 IP 地址。网络中的所有主机都会接收到这个广播包,但只有目标主机(其 IP 地址与请求中的目标 IP 地址匹配)会回复一个 ARP 响应包,包含自己的 MAC 地址。

  2. 简单 ARP 实现思路(基于缓存表): 在 C 语言中,可以通过结构体数组来实现一个简单的 ARP 缓存表。以下是一个示例代码框架:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <arpa/inet.h>

// 定义 ARP 缓存表项结构体
typedef struct {
    uint32_t ip_address;
    uint8_t mac_address[6];
    int is_valid;
} ARPEntry;

// 初始化 ARP 缓存表
void init_arp_cache(ARPEntry *arp_cache, int num_entries) {
    for (int i = 0; i < num_entries; i++) {
        arp_cache[i].is_valid = 0;
    }
}

// 添加 ARP 缓存表项
void add_arp_entry(ARPEntry *arp_cache, int num_entries, uint32_t ip_address, const uint8_t *mac_address) {
    for (int i = 0; i < num_entries; i++) {
        if (!arp_cache[i].is_valid) {
            arp_cache[i].ip_address = ip_address;
            memcpy(arp_cache[i].mac_address, mac_address, 6);
            arp_cache[i].is_valid = 1;
            break;
        }
    }
}

// 根据 IP 地址查找 MAC 地址
int find_mac_address(ARPEntry *arp_cache, int num_entries, uint32_t ip_address, uint8_t *mac_address) {
    for (int i = 0; i < num_entries; i++) {
        if (arp_cache[i].is_valid && arp_cache[i].ip_address == ip_address) {
            memcpy(mac_address, arp_cache[i].mac_address, 6);
            return 1;
        }
    }
    return 0;
}

int main() {
    ARPEntry arp_cache[10];
    int num_entries = 5;

    init_arp_cache(arp_cache, num_entries);

    uint32_t ip_address = inet_addr("192.168.1.10");
    uint8_t mac_address[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};

    add_arp_entry(arp_cache, num_entries, ip_address, mac_address);

    uint8_t found_mac[6];
    if (find_mac_address(arp_cache, num_entries, ip_address, found_mac)) {
        printf("MAC address for IP %s is ", inet_ntoa((struct in_addr){ip_address}));
        for (int i = 0; i < 6; i++) {
            printf("%02x:", found_mac[i]);
        }
        printf("\n");
    } else {
        printf("MAC address not found for IP %s\n", inet_ntoa((struct in_addr){ip_address}));
    }

    return 0;
}

在这个示例中,定义了一个 ARPEntry 结构体来表示 ARP 缓存表项,包括 IP 地址、MAC 地址和有效性标志。init_arp_cache 函数用于初始化缓存表,add_arp_entry 函数用于添加新的表项,find_mac_address 函数根据 IP 地址查找对应的 MAC 地址。