C语言实现ICMP Ping程序 | AI生成和翻译

Home 2025.03

ICMP(互联网控制消息协议)是一种网络层协议,用于IP网络中的诊断和错误报告。它最著名的应用是通过 ping 工具测试主机之间的连通性。ICMP消息直接封装在IP数据包中(无TCP或UDP层),包含类型、代码、校验和与载荷。对于 ping 而言,”回显请求”(类型8)和”回显应答”(类型0)消息至关重要。

ICMP的工作流程概览如下:

  1. 发送方(例如您的机器)向目标IP发送ICMP回显请求
  2. 若目标可达且配置为响应,则返回ICMP回显应答
  3. 发送方根据应答到达时间计算往返时延(RTT)

要在C语言中实现类似 ping 的程序(不使用 libpcaplibnet 等库),我们将使用原始套接字(SOCK_RAW)直接构造和发送ICMP数据包。这需要root权限(例如使用 sudo),因为原始套接字绕过了高层抽象。

以下是一个简化的C程序,用于发送ICMP回显请求并监听回显应答,模拟基本的 ping 功能。


C语言实现ICMP Ping程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <time.h>
#include <errno.h>

// 计算ICMP校验和
unsigned short checksum(void *b, int len) {
    unsigned short *buf = b;
    unsigned int sum = 0;
    unsigned short result;

    for (sum = 0; len > 1; len -= 2) {
        sum += *buf++;
    }
    if (len == 1) {
        sum += *(unsigned char *)buf;
    }
    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    result = ~sum;
    return result;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("用法:%s <目标IP>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int sock_fd;
    struct sockaddr_in dest_addr;
    char packet[64];  // ICMP头部+数据载荷
    struct icmphdr *icmp;
    char recv_buffer[1024];
    struct timespec start, end;

    // 创建ICMP原始套接字
    sock_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sock_fd < 0) {
        perror("套接字创建失败(请以root权限运行)");
        exit(EXIT_FAILURE);
    }

    // 设置目标地址
    dest_addr.sin_family = AF_INET;
    if (inet_pton(AF_INET, argv[1], &dest_addr.sin_addr) <= 0) {
        perror("无效IP地址");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }

    // 准备ICMP回显请求数据包
    memset(packet, 0, sizeof(packet));
    icmp = (struct icmphdr *)packet;
    icmp->type = ICMP_ECHO;        // 类型8:回显请求
    icmp->code = 0;
    icmp->un.echo.id = getpid();   // 使用进程ID作为标识符
    icmp->un.echo.sequence = 1;    // 序列号
    for (int i = sizeof(*icmp); i < 64; i++) {
        packet[i] = i;             // 填充虚拟数据作为载荷
    }
    icmp->checksum = 0;            // 填充数据包后计算校验和
    icmp->checksum = checksum(packet, 64);

    printf("正在ping %s...\n", argv[1]);

    for (int i = 0; i < 4; i++) { // 发送4次ping
        // 记录发送时间
        clock_gettime(CLOCK_MONOTONIC, &start);

        // 发送ICMP回显请求
        if (sendto(sock_fd, packet, 64, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
            perror("发送失败");
            close(sock_fd);
            exit(EXIT_FAILURE);
        }

        // 接收ICMP回显应答
        struct sockaddr_in from_addr;
        socklen_t from_len = sizeof(from_addr);
        int bytes_received = recvfrom(sock_fd, recv_buffer, sizeof(recv_buffer), 0,
                                      (struct sockaddr *)&from_addr, &from_len);
        if (bytes_received < 0) {
            perror("接收失败");
            continue;
        }

        // 记录接收时间
        clock_gettime(CLOCK_MONOTONIC, &end);

        // 解析接收的IP和ICMP头部
        struct iphdr *ip = (struct iphdr *)recv_buffer;
        struct icmphdr *icmp_reply = (struct icmphdr *)(recv_buffer + (ip->ihl * 4));
        if (icmp_reply->type == ICMP_ECHOREPLY && icmp_reply->un.echo.id == getpid()) {
            double rtt = (end.tv_sec - start.tv_sec) * 1000.0 +
                         (end.tv_nsec - start.tv_nsec) / 1000000.0;
            printf("%d字节来自 %s:序列=%d 时延=%.2f毫秒\n",
                   bytes_received - (ip->ihl * 4), argv[1], icmp_reply->un.echo.sequence, rtt);
        } else {
            printf("意外ICMP响应:类型=%d\n", icmp_reply->type);
        }

        sleep(1); // 每次ping间隔1秒
        icmp->un.echo.sequence++; // 递增序列号
        icmp->checksum = 0;
        icmp->checksum = checksum(packet, 64); // 重新计算校验和
    }

    close(sock_fd);
    return 0;
}

实现原理

  1. 套接字创建
    • socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) 创建用于ICMP的原始套接字,允许手动构造ICMP数据包
  2. ICMP数据包构造
    • ICMP头部(struct icmphdr)填充字段包括:
      • type = ICMP_ECHO(8)表示回显请求
      • code = 0
      • id 设置为进程ID用于标识ping请求
      • sequence 用于跟踪单个请求
    • 添加载荷(虚拟数据)后,计算整个数据包的校验和
  3. 发送过程
    • sendto() 将数据包发送至目标IP。由于ICMP在传输层之下操作,无需指定端口
  4. 接收过程
    • recvfrom() 捕获包含ICMP回显应答的原始IP数据包
    • 跳过IP头部(ihl * 4 字节)定位到ICMP头部
    • 验证是否为回显应答(类型0)且标识符匹配
  5. 时延计算
    • 通过 clock_gettime() 以毫秒为单位测量RTT
  6. 校验和计算
    • checksum() 函数按ICMP要求计算16位二进制反码和

编译与使用


注意事项

这是在用户空间实现ICMP通信的最底层方案。若需更底层操作,则需要编写内核级代码直接与IP栈交互。如有进一步调整需求,欢迎提出!


Back Donate