Linux C语言如何实现ping功能?

99ANYc3cd6
预计阅读时长 31 分钟
位置: 首页 C语言 正文

下面我将分步讲解如何实现一个简化版的 ping 程序。

linux c语言 ping
(图片来源网络,侵删)

ping 的工作原理

我们需要理解 ping 的核心工作流程:

  1. 构造 ICMP 数据包ping 程序构造一个 ICMP "回显请求" (Echo Request) 数据包,这个数据包包含了:
    • ICMP 头部:类型(8)、代码(0)、校验和、标识符、序列号等。
    • 有效载荷:通常是发送方的一些数据,比如时间戳,用于计算往返时间。
  2. 发送数据包:程序将这个 ICMP 数据包通过原始套接字 发送到目标主机的 IP 地址。
  3. 接收响应:程序在同一原始套接字上等待接收数据,当目标主机收到回显请求后,会返回一个 ICMP "回显应答" (Echo Reply) 数据包。
  4. 解析响应:程序收到数据包后,解析其内容,验证它是否是对之前发送请求的应答。
  5. 计算时间:比较请求和应答中的时间戳,计算出往返时间。
  6. 显示结果:将结果(如 "64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=15.2 ms")打印到屏幕上。

关键技术和数据结构

1 所需头文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>          // IP 头部
#include <netinet/ip_icmp.h>     // ICMP 头部
#include <arpa/inet.h>
#include <sys/time.h>            // 用于 gettimeofday()
#include <errno.h>

2 ICMP 数据包结构

一个完整的 ICMP Echo Request 包包含 IP 头部和 ICMP 头部,在 Linux 中,我们可以使用系统定义的结构体来方便地构造它们。

// ICMP 头部 (已包含在 <netinet/ip_icmp.h> 中)
struct icmp {
    uint8_t  icmp_type;     // 类型, 8 for Echo Request
    uint8_t  icmp_code;     // 代码, 0 for Echo Request
    uint16_t icmp_cksum;    // 校验和
    uint16_t icmp_id;       // 标识符
    uint16_t icmp_seq;      // 序列号
    // ... 后面是时间戳等数据
};
// IP 头部 (已包含在 <netinet/ip.h> 中)
struct ip {
    // ... (我们不需关心所有字段)
    uint8_t  ip_hl:4;       // 头部长度 (以 32 位字为单位)
    uint8_t  ip_v:4;        // 版本 (IPv4)
    // ...
};

3 校验和计算

校验和是网络协议中确保数据完整性的重要部分,ICMP 和 IP 头部都需要计算校验和。

计算步骤:

linux c语言 ping
(图片来源网络,侵删)
  1. 将校验和字段先置为 0。
  2. 将数据部分视为一系列的 16 位(2字节)整数,并将它们相加(如果数据长度为奇数,则在末尾补一个 0 字节)。
  3. 如果相加结果超过 16 位(即溢出),则将溢出的部分加到低 16 位上(这个过程称为“回卷”)。
  4. 将最终结果的 16 位取反,得到校验和。

下面是一个通用的校验和计算函数:

// 校验和计算函数
unsigned short calculate_checksum(unsigned short *addr, int len) {
    long sum = 0;
    while (len > 1) {
        sum += *addr++;
        len -= 2;
    }
    // 如果长度是奇数,加上最后一个字节
    if (len == 1) {
        sum += *(unsigned char *)addr;
    }
    // 回卷
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    // 取反
    return ~sum;
}

C 语言实现代码

下面是一个功能完整的 ping 程序的源代码,代码包含了详细的注释来解释每一步的作用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <errno.h>
#include <signal.h>
#define PACKET_SIZE 64
#define DEFAULT_TTL 64
// 全局变量,用于 Ctrl+C 退出
volatile int keep_running = 1;
void signal_handler(int signum) {
    if (signum == SIGINT) {
        printf("\nPing program terminated.\n");
        keep_running = 0;
    }
}
// 校验和计算函数
unsigned short calculate_checksum(unsigned short *addr, int len) {
    long sum = 0;
    while (len > 1) {
        sum += *addr++;
        len -= 2;
    }
    if (len == 1) {
        sum += *(unsigned char *)addr;
    }
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return ~sum;
}
// 构造 ICMP 包
void build_icmp_packet(char *packet, int packet_size, int seq_num) {
    struct icmp *icmp_hdr = (struct icmp *)packet;
    struct timeval *tval;
    icmp_hdr->icmp_type = ICMP_ECHO;
    icmp_hdr->icmp_code = 0;
    icmp_hdr->icmp_cksum = 0;
    icmp_hdr->icmp_id = htons(getpid()); // 使用 PID 作为标识符
    icmp_hdr->icmp_seq = htons(seq_num);
    // 在数据部分填充时间戳
    tval = (struct timeval *)icmp_hdr->icmp_data;
    gettimeofday(tval, NULL);
    // 计算校验和
    icmp_hdr->icmp_cksum = calculate_checksum((unsigned short *)icmp_hdr, packet_size);
}
// 解析接收到的 IP 包
void parse_ip_packet(char *recv_buf, int recv_len, struct sockaddr_in *from_addr) {
    struct ip *ip_hdr = (struct ip *)recv_buf;
    int ip_hdr_len = ip_hdr->ip_hl * 4;
    struct icmp *icmp_hdr = (struct icmp *)(recv_buf + ip_hdr_len);
    // 确保是 ICMP 包
    if (icmp_hdr->icmp_type != ICMP_ECHOREPLY) {
        return;
    }
    // 确保是来自我们自己的请求的应答
    if (icmp_hdr->icmp_id != htons(getpid())) {
        return;
    }
    // 获取发送时间
    struct timeval *sent_time = (struct timeval *)icmp_hdr->icmp_data;
    struct timeval current_time;
    gettimeofday(&current_time, NULL);
    // 计算往返时间 (RTT)
    long rtt = (current_time.tv_sec - sent_time->tv_sec) * 1000 +
               (current_time.tv_usec - sent_time->tv_usec) / 1000;
    printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%ld ms\n",
           recv_len - ip_hdr_len,
           inet_ntoa(from_addr->sin_addr),
           ntohs(icmp_hdr->icmp_seq),
           ip_hdr->ip_ttl,
           rtt);
}
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <hostname or IP>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    // 注册信号处理,用于 Ctrl+C
    signal(SIGINT, signal_handler);
    // 1. 创建原始套接字 (需要 root 权限)
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sockfd < 0) {
        perror("socket");
        fprintf(stderr, "This program requires root privileges to create a raw socket.\n");
        exit(EXIT_FAILURE);
    }
    // 2. 设置 TTL
    if (setsockopt(sockfd, IPPROTO_IP, IP_TTL, &DEFAULT_TTL, sizeof(DEFAULT_TTL)) < 0) {
        perror("setsockopt");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    // 3. 获取目标主机 IP
    struct sockaddr_in dest_addr;
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    if (inet_pton(AF_INET, argv[1], &dest_addr.sin_addr) <= 0) {
        // 如果不是 IP 地址,尝试解析主机名
        struct hostent *host = gethostbyname(argv[1]);
        if (host == NULL) {
            herror("gethostbyname");
            close(sockfd);
            exit(EXIT_FAILURE);
        }
        memcpy(&dest_addr.sin_addr, host->h_addr, host->h_length);
    }
    printf("PING %s (%s): %d data bytes\n", argv[1], inet_ntoa(dest_addr.sin_addr), PACKET_SIZE);
    char send_packet[PACKET_SIZE];
    char recv_packet[4096]; // 接收缓冲区要足够大
    struct sockaddr_in from_addr;
    socklen_t from_len = sizeof(from_addr);
    int seq_num = 0;
    // 4. 循环发送和接收
    while (keep_running) {
        seq_num++;
        // 构造 ICMP 包
        memset(send_packet, 0, sizeof(send_packet));
        build_icmp_packet(send_packet, sizeof(send_packet), seq_num);
        // 发送包
        if (sendto(sockfd, send_packet, sizeof(send_packet), 0,
                   (struct sockaddr *)&dest_addr, sizeof(dest_addr)) <= 0) {
            perror("sendto");
            continue;
        }
        // 设置接收超时 (1秒)
        struct timeval tv;
        tv.tv_sec = 1;
        tv.tv_usec = 0;
        setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
        // 接收包
        int recv_len = recvfrom(sockfd, recv_packet, sizeof(recv_packet), 0,
                                (struct sockaddr *)&from_addr, &from_len);
        if (recv_len > 0) {
            parse_ip_packet(recv_packet, recv_len, &from_addr);
        } else {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                printf("Request timeout for icmp_seq=%d\n", seq_num);
            } else {
                perror("recvfrom");
            }
        }
        // 间隔 1 秒
        sleep(1);
    }
    close(sockfd);
    return 0;
}

如何编译和运行

  1. 保存代码:将上面的代码保存为 my_ping.c

  2. 编译: 由于我们使用了原始套接字,编译时需要链接 resolv 库(用于 gethostbyname)。

    linux c语言 ping
    (图片来源网络,侵删)
    gcc my_ping.c -o my_ping -lresolv
  3. 运行注意:创建原始套接字需要 root 权限。

    sudo ./my_ping <目标主机名或IP>
    sudo ./my_ping www.baidu.com
    # 或
    sudo ./my_ping 8.8.8.8

代码的局限性

这个简化版的 ping 程序与系统自带的 ping 命令相比,有一些局限性:

  • 权限要求:必须使用 sudo 运行,因为创建原始套接字需要超级用户权限。
  • ICMP 过滤:有些网络策略或防火墙可能会过滤掉 ICMP 包,导致程序无法正常工作,系统自带的 ping 可能使用其他技巧(如原始 IP 套接字)来绕过。
  • 功能简化:没有实现 -c(发送指定次数后退出)、-i(间隔时间)、-s(包大小)等常用参数。
  • 错误处理:对各种网络错误的处理比较简单。
  • 数据包分片:没有处理 IP 数据包分片的情况。

通过实现这个 C 语言的 ping 程序,你可以学到:

  1. 原始套接字 的创建和使用。
  2. ICMP 协议 的数据包结构和字段含义。
  3. 校验和 的计算原理和实现。
  4. 网络字节序主机字节序 的转换 (htons, ntohs)。
  5. gettimeofday 用于高精度计时。
  6. inet_ptongethostbyname 用于主机名和 IP 地址的转换。
  7. setsockopt 用于设置套接字选项(如 TTL 和超时)。

这是一个非常经典且富有教育意义的网络编程项目。

-- 展开阅读全文 --
头像
织梦模板文件修改步骤是怎样的?
« 上一篇 01-22
织梦图片路径为何修改不了?
下一篇 » 01-22

相关文章

取消
微信二维码
支付宝二维码

目录[+]