当前位置:首页 > 行业动态 > 正文

如何在C语言中实现Ping功能?

在 Linux 系统中,可以使用 ping 命令来测试网络连通性。要检查与 IP 地址为 192.168.1.1 的主机之间的连接,可以在终端中输入:,,“ bash,ping 192.168.1.1,“,,这将发送 ICMP 回显请求到指定的 IP 地址,并显示响应时间及丢包情况。

在Linux系统中,ping命令是一种常用的网络工具,用于测试主机之间的连通性以及测量数据包的往返时间,以下是关于如何在Linux上实现ping命令的详细步骤和解释:

一、理解ICMP协议

ping命令基于ICMP(Internet Control Message Protocol)协议工作,ICMP允许主机或路由器报告差错情况或提供有关异常情况的报告。ping命令使用的是ICMP回送请求和回送应答报文类型,以判断目标主机是否可达。

二、创建原始套接字

在Linux中,要发送ICMP包,需要使用原始套接字(raw socket),原始套接字可以直接访问网络层,允许我们构造自定义的网络协议数据包。

int ip_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (ip_fd < 0) {
    perror("socket error");
    exit(1);
}

三、设置ICMP包头

ICMP包头包括类型、代码、校验和、标识符和序列号等字段,对于ICMP回送请求,类型字段应设置为8。

struct icmphdr {
    u_int8_t icmp_type;    /* ICMP message type */
    u_int8_t icmp_code;    /* Optional ICMP code */
    u_int16_t icmp_cksum;  /* Optional ICMP checksum */
    union {
        u_int32_t ih_idseq;     /* ICMP ID and sequence number */
        u_int32_t ih_gateway;    /* Target gateway address */
        struct {
            u_int16_t unused;
            u_int16_t mtu;
        } ih_pmtu;
    } icmp_hun;
};

四、计算校验和

由于IP协议是不可靠的传输协议,为了确保数据在传输过程中没有发生变化,需要在ICMP包头中加入校验和字段,校验和的计算方法是将ICMP包头视为由若干个16位整数组成,对这些整数进行二进制反码求和,然后取反码得到校验和。

unsigned short cal_chksum(void *buffer, int len) {
    unsigned long sum = 0;
    unsigned short *data = buffer;
    while (len > 1) {
        sum += *data++;
        if (sum & 0x80000000)
            sum = (sum & 0xFFFF) + (sum >> 16);
        len -= 2;
    }
    if (len)
        sum += *(unsigned char *)data;
    while (sum >> 16)
        sum = (sum & 0xFFFF) + (sum >> 16);
    return ~sum;
}

五、构造并发送ICMP包

构造ICMP回送请求包,并使用原始套接字发送出去,需要记录当前时间,以便后续计算往返时间。

struct timeval begin_time;
gettimeofday(&begin_time, NULL);
struct icmphdr icmphdr;
icmphdr.icmp_type = ICMP_ECHO;
icmphdr.icmp_code = 0;
icmphdr.icmp_cksum = 0;
icmphdr.icmp_seq = htons(++sequence);
icmphdr.icmp_id = getpid() & 0xFFFF; // Use process ID as the ID field
icmphdr.icmp_cksum = cal_chksum(&icmphdr, sizeof(icmphdr)); // Update checksum after setting other fields
sendto(ip_fd, &icmphdr, sizeof(icmphdr), 0, (struct sockaddr*)&send_addr, sizeof(send_addr));

六、接收ICMP回显应答包并处理

接收到ICMP回显应答包后,解析包头信息,并计算往返时间,如果收到的是其他类型的ICMP包,则忽略。

struct sockaddr_in recv_addr;
socklen_t addrlen = sizeof(recv_addr);
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
if (recvfrom(ip_fd, buffer, sizeof(buffer), 0, (struct sockaddr*)&recv_addr, &addrlen) <= 0) {
    perror("recvfrom error");
    exit(1);
}
struct iphdr *iphdr = buffer;
struct icmphdr *icmphdr = (struct icmphdr*)(buffer + (iphdr->ip_hl << 2)); // IP header length is in bytes, shift left by 2 to get it in 32-bit words
if (icmphdr->icmp_type == ICMP_ECHOREPLY && icmphdr->icmp_id == getpid() & 0xFFFF) {
    struct timeval recv_time;
    gettimeofday(&recv_time, NULL);
    int rtt = (recv_time.tv_sec begin_time.tv_sec) * 1000 + (recv_time.tv_usec begin_time.tv_usec) / 1000.0; // Calculate round-trip time in milliseconds
    printf("From: %s, RTT: %d ms
", inet_ntoa(recv_addr.sin_addr), rtt);
} else {
    printf("Received non-echo reply or invalid packet
");
}

七、主函数逻辑

主函数负责解析命令行参数,设置信号处理函数以定时发送ICMP包,并根据用户输入的目标地址构造并发送ICMP包。

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <hostname/IP>
", argv[0]);
        exit(1);
    }
    // Create raw socket for ICMP
    int ip_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (ip_fd < 0) {
        perror("socket error");
        exit(1);
    }
    // Set up signal handler for alarm to send ICMP packets at regular intervals
    struct sigaction act;
    act.sa_handler = handle_alarm;
    act.sa_flags = SA_SIGINFO; // Use SA_SIGINFO to pass additional data to signal handler
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);
    // Set up destination address based on command line argument
    struct sockaddr_in send_addr;
    memset(&send_addr, 0, sizeof(send_addr));
    send_addr.sin_family = AF_INET;
    if (inet_aton(argv[1], &send_addr.sin_addr) == 0) { // Check if it's an IP address
        struct hostent *host = gethostbyname(argv[1]);
        if (host == NULL) {
            perror("Unknown host");
            exit(1);
        }
        memcpy(&send_addr.sin_addr, host->h_addr, host->h_length);
    }
    // Send ICMP packets at regular intervals until interrupted by user (Ctrl+C)
    while (1) {
        sleep(1); // Sleep for 1 second before sending next packet
        alarm(1); // Set alarm to trigger after 1 second
    }
    close(ip_fd); // Close the raw socket when done
    return 0;
}

八、FAQs问答环节

问:为什么在发送ICMP包之前要将校验和字段置为0?

答:在发送ICMP包之前将校验和字段置为0是因为ICMP协议要求在传输过程中对整个包(包括校验和字段本身)进行校验和计算,通过将校验和字段先置为0,可以确保在计算校验和时不会受到之前值的影响,接收方在接收到ICMP包后也会重新计算校验和,并与包中的校验和字段进行比较,以验证数据的完整性,如果校验和不匹配,则说明数据在传输过程中发生了变化,该ICMP包将被丢弃。

问:在ping命令中,如何指定发送的数据包数量和间隔时间?

答:在Linux的ping命令中,可以通过-c选项来指定发送的数据包数量,例如ping -c 4 www.example.com表示发送4个数据包,通过-i选项可以指定发送数据包的间隔时间,例如ping -i 0.5 www.example.com表示每0.5秒发送一个数据包,这些选项使得ping命令更加灵活,可以根据实际需求进行调整。