用C语言实现ping命令

前言

最近在跟着小菜学编程温习网络方面的知识,正好看了其中一章《用C语言实现ping命令》,觉得蛮有意思的,还能更进一步加深网络方面的理解。因此就当学习随手记录了一下,并对Demo部分给出了注解。

代码学习

该代码共有以下函数组成,建议阅读顺序:main->send_echo_request->recv_echo_reply->ping

在这里插入图片描述

icmp_echo 结构体

icmp_echo结构体用于封装ICMP报文。ICMP报文格式如下:

在这里插入图片描述

//定义ICMP报文结构体
struct __attribute__((__packed__)) icmp_echo {
    // header
    uint8_t type; //类型 
    uint8_t code; //代码 
    uint16_t checksum;//校验和 

    uint16_t ident;//标识符 
    uint16_t seq; //符号 

    // data
    double sending_ts;//发送时间 
    char magic[MAGIC_LEN]; //字符串 
};
/*
__attrubte__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐
*/

send_echo_request函数

send_echo_request的作用是发送回显请求。简单来说,就是定义ICMP报文,然后发送到目的地址去编程发起网络通信,离不开套接字,收发 ICMP 报文当然也不例外

具体解释看注释部分。

//发送回显请求 
int send_echo_request(int sock, struct sockaddr_in* addr, int ident, int seq) {
    // allocate memory for icmp packet
    // 定义ICMP报文
    struct icmp_echo icmp;
    bzero(&icmp, sizeof(icmp));

    // fill header files
    //定义头部数据
    icmp.type = 8;
    icmp.code = 0;
    icmp.ident = htons(ident);
    icmp.seq = htons(seq);

    // fill magic string
    //字符串拷贝
    strncpy(icmp.magic, MAGIC, MAGIC_LEN);

    // fill sending timestamp
    //获取时间戳
    icmp.sending_ts = get_timestamp();

    // calculate and fill checksum
    // 计算校验和
    icmp.checksum = htons(
        calculate_checksum((unsigned char*)&icmp, sizeof(icmp))
    );

    // send it
    /*
		sendto() 用来将数据由指定的 socket 传给对方主机
    	参数1:socket文件描述符
		参数2:发送数据的首地址
		参数3:数据长度
		参数4:默认方式发送
		参数5:存放目的主机的IP和端口信息
		参数6: 参数5的长度 
	*/
    int bytes = sendto(sock, &icmp, sizeof(icmp), 0,(struct sockaddr*)addr, sizeof(*addr));
    if (bytes == -1) {
        return -1;
    }
    return 0;
}

recv_echo_reply函数

recv_echo_reply函数主要实现使用recv_echo_reply接收ICMP回显应答报文。

//实现recv_echo_reply用于接收ICMP回显应答报文
int recv_echo_reply(int sock, int ident) {
    // allocate buffer
    //定义缓冲区 
    unsigned char buffer[IP_BUFFER_SIZE];
    //sockaddr_in 结构体
    struct sockaddr_in peer_addr;

    // receive another packet
    int addr_len = sizeof(peer_addr);
    /*
		recvfrom()本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址 
		s:标识一个已连接套接口的描述字。 
		buf:接收数据缓冲区。 
		len:缓冲区长度。 
		flags:调用操作方式。 
		from:(可选)指针,指向装有源地址的缓冲区。 
		fromlen:(可选)指针,指向from缓冲区长度值。 
	*/
    int bytes = recvfrom(sock, buffer, sizeof(buffer), 0,
        (struct sockaddr*)&peer_addr, &addr_len);
    if (bytes == -1) {
        // normal return when timeout
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            return 0;
        }

        return -1;
    }
	//IP头部长度 
    int ip_header_len = (buffer[0] & 0xf) << 2;
    // find icmp packet in ip packet
    //从 IP 报文中取出 ICMP 报文
	struct icmp_echo* icmp = (struct icmp_echo*)(buffer + ip_header_len);

    // check type
    if (icmp->type != 0 || icmp->code != 0) {
        return 0;
    }

    // match identifier
    //ntohs()是一个函数名,作用是将一个16位数由网络字节顺序转换为主机字节顺序
    if (ntohs(icmp->ident) != ident) {
        return 0;
    }

    // print info
    printf("%s seq=%-5d %8.2fms\n",
        inet_ntoa(peer_addr.sin_addr),
        ntohs(icmp->seq),
        (get_timestamp() - icmp->sending_ts) * 1000
    );

    return 0;
}

ping函数


int ping(const char *ip) {
	//存储目标地址
    // for store destination address
    struct sockaddr_in addr;
    //清空信息·
    bzero(&addr, sizeof(addr));
	//设置地址和端口号
    // fill address, set port to 0
    addr.sin_family = AF_INET;
    addr.sin_port = 0;
    //inet_aton是一个计算机函数,功能是将一个字符串IP地址转换为一个32位的网络序列IP地址。
    if (inet_aton(ip, (struct in_addr*)&addr.sin_addr.s_addr) == 0) {
        fprintf(stderr, "bad ip address: %s\n", ip);
        return -1;
    };

    // create raw socket for icmp protocol
    //创建一个原始套接字,协议类型为 IPPROTO_ICMP 
    int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sock == -1) {
        perror("create raw socket");
        return -1;
    }
	//设置套接字时限
    // set socket timeout option
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = RECV_TIMEOUT_USEC;
    int ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    if (ret == -1) {
        perror("set socket option");
        close(s);
        return -1;
    }

    double next_ts = get_timestamp();
    int ident = getpid();//取得进程识别码
    int seq = 1;
	
	//循环发送和接收ICMP
    for (;;) {
        // time to send another packet
        double current_ts = get_timestamp();
        if (current_ts >= next_ts) {
            // send it
            ret = send_echo_request(sock, &addr, ident, seq);
            if (ret == -1) {
                perror("Send failed");
            }

            // update next sendint timestamp to one second later
            next_ts = current_ts + 1;
            // increase sequence number
            seq += 1;
        }
        // try to receive and print reply
        ret = recv_echo_reply(sock, ident);
        if (ret == -1) {
            perror("Receive failed");
        }
    }
    close(s);
    return 0;
}

calculate_checksum函数:这个函数是用于计算校验和,具体算法请百度。

完整代码

#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>

#define MAGIC "1234567890"
#define MAGIC_LEN 11
#define IP_BUFFER_SIZE 65536
#define RECV_TIMEOUT_USEC 100000
/*
__attrubte__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐
*/

//定义ICMP回环结构体 
struct __attribute__((__packed__)) icmp_echo {
    // header
    uint8_t type; //类型 
    uint8_t code; //代码 
    uint16_t checksum;//校验和 

    uint16_t ident;//标识符 
    uint16_t seq; //符号 

    // data
    double sending_ts;//发送时间 
    char magic[MAGIC_LEN]; //字符串 
};
// 获取时间戳

double get_timestamp() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + ((double)tv.tv_usec) / 1000000;
}
//计算校验和

uint16_t calculate_checksum(unsigned char* buffer, int bytes) {
    uint32_t checksum = 0;
    unsigned char* end = buffer + bytes;

    // odd bytes add last byte and reset end
    if (bytes % 2 == 1) {
        end = buffer + bytes - 1;
        checksum += (*end) << 8;
    }

    // add words of two bytes, one by one
    while (buffer < end) {
        checksum += (buffer[0] << 8) + buffer[1];

        // add carry if any
        uint32_t carray = checksum >> 16;
        if (carray != 0) {
            checksum = (checksum & 0xffff) + carray;
        }

        buffer += 2;
    }
    // negate it
    checksum = ~checksum;

    return checksum & 0xffff;
}
//发送回显请求 
int send_echo_request(int sock, struct sockaddr_in* addr, int ident, int seq) {
    // allocate memory for icmp packet
    struct icmp_echo icmp;
    bzero(&icmp, sizeof(icmp));

    // fill header files
    icmp.type = 8;
    icmp.code = 0;
    icmp.ident = htons(ident);
    icmp.seq = htons(seq);

    // fill magic string
    strncpy(icmp.magic, MAGIC, MAGIC_LEN);

    // fill sending timestamp
    icmp.sending_ts = get_timestamp();

    // calculate and fill checksum
    icmp.checksum = htons(
        calculate_checksum((unsigned char*)&icmp, sizeof(icmp))
    );

    // send it
    /*
		sendto() 用来将数据由指定的 socket 传给对方主机
    	参数1:socket文件描述符
		参数2:发送数据的首地址
		参数3:数据长度
		参数4:默认方式发送
		参数5:存放目的主机的IP和端口信息
		参数6: 参数5的长度 
	*/
    int bytes = sendto(sock, &icmp, sizeof(icmp), 0,(struct sockaddr*)addr, sizeof(*addr));
    if (bytes == -1) {
        return -1;
    }
    return 0;
}
//实现recv_echo_reply用于接收ICMP回显应答报文
int recv_echo_reply(int sock, int ident) {
    // allocate buffer
    //定义缓冲区 
    unsigned char buffer[IP_BUFFER_SIZE];
    //sockaddr_in 结构体
    struct sockaddr_in peer_addr;

    // receive another packet
    int addr_len = sizeof(peer_addr);
    /*
		recvfrom()本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址 
		s:标识一个已连接套接口的描述字。 
		buf:接收数据缓冲区。 
		len:缓冲区长度。 
		flags:调用操作方式。 
		from:(可选)指针,指向装有源地址的缓冲区。 
		fromlen:(可选)指针,指向from缓冲区长度值。 
	*/
    int bytes = recvfrom(sock, buffer, sizeof(buffer), 0,
        (struct sockaddr*)&peer_addr, &addr_len);
    if (bytes == -1) {
        // normal return when timeout
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            return 0;
        }

        return -1;
    }
	//IP头部长度 
    int ip_header_len = (buffer[0] & 0xf) << 2;
    // find icmp packet in ip packet
    //从 IP 报文中取出 ICMP 报文
	struct icmp_echo* icmp = (struct icmp_echo*)(buffer + ip_header_len);

    // check type
    if (icmp->type != 0 || icmp->code != 0) {
        return 0;
    }

    // match identifier
    //ntohs()是一个函数名,作用是将一个16位数由网络字节顺序转换为主机字节顺序
    if (ntohs(icmp->ident) != ident) {
        return 0;
    }

    // print info
    printf("%s seq=%-5d %8.2fms\n",
        inet_ntoa(peer_addr.sin_addr),
        ntohs(icmp->seq),
        (get_timestamp() - icmp->sending_ts) * 1000
    );

    return 0;
}

int ping(const char *ip) {
    // for store destination address
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));

    // fill address, set port to 0
    addr.sin_family = AF_INET;
    addr.sin_port = 0;
    //inet_aton是一个计算机函数,功能是将一个字符串IP地址转换为一个32位的网络序列IP地址。
    if (inet_aton(ip, (struct in_addr*)&addr.sin_addr.s_addr) == 0) {
        fprintf(stderr, "bad ip address: %s\n", ip);
        return -1;
    };

    // create raw socket for icmp protocol
    //创建一个原始套接字,协议类型为 IPPROTO_ICMP 
    int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sock == -1) {
        perror("create raw socket");
        return -1;
    }

    // set socket timeout option
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = RECV_TIMEOUT_USEC;
    int ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    if (ret == -1) {
        perror("set socket option");
        close(s);
        return -1;
    }

    double next_ts = get_timestamp();
    int ident = getpid();//取得进程识别码
    int seq = 1;

    for (;;) {
        // time to send another packet
        double current_ts = get_timestamp();
        if (current_ts >= next_ts) {
            // send it
            ret = send_echo_request(sock, &addr, ident, seq);
            if (ret == -1) {
                perror("Send failed");
            }

            // update next sendint timestamp to one second later
            next_ts = current_ts + 1;
            // increase sequence number
            seq += 1;
        }

        // try to receive and print reply
        ret = recv_echo_reply(sock, ident);
        if (ret == -1) {
            perror("Receive failed");
        }
    }

    close(s);

    return 0;
}

int main(int argc, const char* argv[]) {
    if (argc < 2) {
        fprintf(stderr, "no host specified");
        return -1;
    }
    return ping(argv[1]);
}

Demo演示❤

注意:由于直接编译无法通过,会报下面的错误。

(base) user@ubuntu:~/Desktop/HTTP/studyFork$ gcc a.c -o a
a.c: In function ‘ping’:
a.c:171:15: error: ‘s’ undeclared (first use in this function)
         close(s);
               ^
a.c:171:15: note: each undeclared identifier is reported only once for each function it appears in

这里,笔者索性就先将两处出现close(s)的语句先注释掉,然后就可以跑通了。

(base) user@ubuntu:~/Desktop/HTTP/studyFork$ vim a.c
(base) user@ubuntu:~/Desktop/HTTP/studyFork$ gcc a.c -o a
(base) user@ubuntu:~/Desktop/HTTP/studyFork$ ./a 8.8.8.8
create raw socket: Operation not permitted
(base) user@ubuntu:~/Desktop/HTTP/studyFork$ sudo ./a 8.8.8.8
[sudo] password for user: 
8.8.8.8 seq=1        46.23ms
8.8.8.8 seq=2        45.65ms
8.8.8.8 seq=3        46.09ms
8.8.8.8 seq=4        45.45ms
8.8.8.8 seq=5        45.49ms
8.8.8.8 seq=6        45.27ms

在这里插入图片描述

参考资料

https://fasionchan.com/network/icmp/ping-c/

https://blog.csdn.net/qq_33724710/article/details/51576444

  • 3
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值