美文网首页
在Linux上用C++实现Ping

在Linux上用C++实现Ping

作者: toMyLord | 来源:发表于2019-10-29 10:38 被阅读0次

    首先我们在terminal上使用ping命令并用wireshark软件抓包,看看实现ping命令需要那些协议,以及ping的数据包由那些内容构成。

    ping.png catch_ping.png

    用wireshark抓包后,发现ping命令发送的请求报文和收到的应答报文都是ICMP(Internet Control Message Protocol)网际控制报文协议。我们再仔细解析请求报文和应答报文:

    request.png reply.png

    发现ICMP报文是装在IP数据报中的,并且ICMP报文具有以下字段:Type(8位)、Code(8位)、Cheksum(16位)、Identifier(16位)、Sequence(16位)、Timestamp(8位)、Data。
    结合以上对ping命令的分析,我们可以想到,实现ping命令,需要用到ICMP协议的相关内容。想要实现对IMCP报文自定义构建,我们还需要用到SOCK_RAW原始套接字的相关内容。在Linux上申请原始套接字需要root权限,但是ping命令可以被普通用户正常运行,因此我们还需要用到在Linux上以普通用户运行特权指令的相关内容(在Linux上以普通用户运行特权指令在我的上一篇博客有详细的说明,本文不在赘述)。此外,我们可以观察到ping命令是通过捕获Ctrl+C指令后才结束的,但是在结束之前,还对整个发送接收情况做了总结,因此,实现Ping命令还需要用到Linux上的信号机制。

    ICMP报文

    IMCP协议用于在IP主机、路由器之间传递控制消息,允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP协议不是高层协议(看起来好像是高层协议,因为ICMP报文是装在IP数据报中,作为其中的数据部分),而是IP层的协议。ICMP报文作为IP层数据报的数据,加上数据报的首部,组成IP数据报发送出去。ICMP报文格式如下图所示:

    ICMP.png

    在谢希仁编著的第7版计算机网络教材中,ICMP报文的格式与上图相同,但是从上文的抓包情况来看,真正的ICMP报文在16位序列号数据之后,Data数据之前还加入了8位的时间戳数据。
    仔细对比上文对请求报文和应答报文的抓包数据,发现除了ICMP报文的序列号(seq)因包而异以外,Type字段也不相同。查阅文献后知道ICMP报文类型是根据Type字段和Code字段的组合来确定的。Type = 0,Code = 0代表回送请求(Echo Request),Type = 8,Code = 0代表回送应答(Echo Reply),分别对应ping命令的请求报文和应答报文。
    对于ICMP报文的校验和(Checksum)字段的计算,只需要以下几个步骤:

    • 将校验和字段置零。
    • 将每两个字节(16位)相加(二进制求和)直到最后得出结果,若出现最后还剩一个字节继续与前面结果相加。
    • (溢出)将高16位与低16位相加,直到高16位为0为止。
    • 将最后的结果(二进制)取反。

    用C/C++对ICMP报文数据的构造,可以直接利用ip_icmp.h头文件中有关ICMP报文的内容进行构造。struct icmp结构如下:

    struct icmp
    {
      uint8_t  icmp_type;   /* type of message, see below */
      uint8_t  icmp_code;   /* type sub code */
      uint16_t icmp_cksum;  /* ones complement checksum of struct */
      union
      {
        unsigned char ih_pptr;      /* ICMP_PARAMPROB */
        struct in_addr ih_gwaddr;   /* gateway address */
        struct ih_idseq             /* echo datagram */
        {
          uint16_t icd_id;
          uint16_t icd_seq;
        } ih_idseq;
        uint32_t ih_void;
    
        /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
        struct ih_pmtu
        {
          uint16_t ipm_void;
          uint16_t ipm_nextmtu;
        } ih_pmtu;
    
        struct ih_rtradv
        {
          uint8_t irt_num_addrs;
          uint8_t irt_wpa;
          uint16_t irt_lifetime;
        } ih_rtradv;
      } icmp_hun;
    #define icmp_pptr       icmp_hun.ih_pptr
    #define icmp_gwaddr     icmp_hun.ih_gwaddr
    #define icmp_id         icmp_hun.ih_idseq.icd_id
    #define icmp_seq        icmp_hun.ih_idseq.icd_seq
    #define icmp_void       icmp_hun.ih_void
    #define icmp_pmvoid     icmp_hun.ih_pmtu.ipm_void
    #define icmp_nextmtu    icmp_hun.ih_pmtu.ipm_nextmtu
    #define icmp_num_addrs  icmp_hun.ih_rtradv.irt_num_addrs
    #define icmp_wpa        icmp_hun.ih_rtradv.irt_wpa
    #define icmp_lifetime   icmp_hun.ih_rtradv.irt_lifetime
      union
      {
        struct
        {
          uint32_t its_otime;
          uint32_t its_rtime;
          uint32_t its_ttime;
        } id_ts;
        struct
        {
          struct ip idi_ip;
          /* options and then 64 bits of data */
        } id_ip;
        struct icmp_ra_addr id_radv;
        uint32_t   id_mask;
        uint8_t    id_data[1];
      } icmp_dun;
    #define icmp_otime  icmp_dun.id_ts.its_otime
    #define icmp_rtime  icmp_dun.id_ts.its_rtime
    #define icmp_ttime  icmp_dun.id_ts.its_ttime
    #define icmp_ip     icmp_dun.id_ip.idi_ip
    #define icmp_radv   icmp_dun.id_radv
    #define icmp_mask   icmp_dun.id_mask
    #define icmp_data   icmp_dun.id_data
    };
    

    上述结构体中我们只需要关注icmp_type、icmp_code、icmp_cksum、icmp_seq、icmp_id、icmp_data字段即可。我们可以直接在内存中利用指针对icmp结构体的指针指向数据块进行构造,以达到对整个IMCP报文的构建。例如:

    icmp_pointer->icmp_type = ICMP_ECHO;
    icmp_pointer->icmp_code = 0;
    icmp_pointer->icmp_cksum = 0;           //计算校验和之前先要将校验位置零
    icmp_pointer->icmp_seq = send_pack_num + 1; //用send_pack_num作为ICMP包序列号
    icmp_pointer->icmp_id = getpid();       //用进程号作为ICMP包标志
    

    IP数据报

    在ping命令中,我们使用recvfrom()函数接收到的回应报文是IP数据报,并且我们需要用到以下字段:

    • IP报头长度IHL(Internet Header Length)以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。
    • 生存时间TTL(Time To Live)以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。
      使用方法与上述ICMP报文结构体使用方法一致,这里给出struct ip的定义,不在过多说明:
    struct ip
      {
    #if __BYTE_ORDER == __LITTLE_ENDIAN
        unsigned int ip_hl:4;       /* header length */
        unsigned int ip_v:4;        /* version */
    #endif
    #if __BYTE_ORDER == __BIG_ENDIAN
        unsigned int ip_v:4;        /* version */
        unsigned int ip_hl:4;       /* header length */
    #endif
        uint8_t ip_tos;             /* type of service */
        unsigned short ip_len;      /* total length */
        unsigned short ip_id;       /* identification */
        unsigned short ip_off;      /* fragment offset field */
    #define IP_RF 0x8000            /* reserved fragment flag */
    #define IP_DF 0x4000            /* dont fragment flag */
    #define IP_MF 0x2000            /* more fragments flag */
    #define IP_OFFMASK 0x1fff       /* mask for fragmenting bits */
        uint8_t ip_ttl;             /* time to live */
        uint8_t ip_p;               /* protocol */
        unsigned short ip_sum;      /* checksum */
        struct in_addr ip_src, ip_dst;  /* source and dest address */
      };
    

    SOCK_RAW原始套接字

    实际上,我们常用的网络编程都是在应用层的手法操作,也就是大多数程序员接触到的流式套接字(SOCK_STREAM)和数据包式套接字(SOCK_DGRAM)。而这些数据包都是由系统提供的协议栈实现,用户只需要填充应用层报文即可,由系统完成底层报文头的填充并发送。然而Ping命令的实现中需要执行更底层的操作,这个时候就需要使用原始套接字(SOCK_RAW)来实现。
    原始套接字(SOCK_RAW)是一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心。原始套接字可以实现普通套接字无法处理ICMP、IGMP等网络报文,原始套接字也可以处理特殊的IPv4报文,此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。总体来说,原始套接字可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据。
    创建原始套接字的方法如下:

    socket(AF_INET, SOCK_RAW, protocol);
    

    这里的重点在于protocol字段,使用原始套接字之后,这个字段就不能简单的置零了。在头文件netinet/in.h中定义了系统中该字段目前能取的值,注意:有些系统中不一定实现了netinet/in.h中的所有协议。源代码的linux/in.h中和netinet/in.h中的内容一样。我们常见的有IPPROTO_TCPIPPROTO_UDPIPPROTO_ICMP。我们可以通过一下方法创建需要的ICMP协议的原始套接字:

    struct protoent * protocol;             //获取协议用
    //通过协议名称获取协议编号
    if((protocol = getprotobyname("icmp")) == NULL){
        fprintf(stderr, "Get protocol error:%s \n\a", strerror(errno));
        exit(1);
    }
    //创建原始套接字,这里需要root权限,申请完成之后应该降权处理
    if((sock_fd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) == -1){
        fprintf(stderr, "Greate RAW socket error:%s \n\a", strerror(errno));
        exit(1);
    }
    //降权处理,使该进程的EUID,SUID的值变成RUID的值
    setuid(getuid());
    

    用这种方式我就可以得到原始的IP包了,然后就可以自定义IP所承载的具体协议类型,如TCP,UDP或ICMP,并手动对每种承载在IP协议之上的报文进行填充。

    Linux上的捕获Ctrl+C信号

    在Linux C/C++程序中,如果程序一直以死循环的状态运行,以Ctrl+C结束,并且希望在结束时输出一些统计数据帮助用户分析,我们就需要用到信号处理机制。通过捕获到的需要的信号后,执行信号处理函数。为此我们需要用到sigaction()函数,该函数的的功能是检查或修改与制定信号相关联的处理动作,其原型为:

    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    

    signum参数用于指定需要捕获的型号类型,act参数指定新的型号处理方式,oldact参数输出先前型号的处理方式(如果不为NULL的话)。这个函数的关键在于对struct sigaction结构体的设置,我们首先找到该结构体的定义:

    struct sigaction
      {
        /* Signal handler.  */
    #if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDED
        union
          {
        /* Used if SA_SIGINFO is not set.  */
        __sighandler_t sa_handler;
        /* Used if SA_SIGINFO is set.  */
        void (*sa_sigaction) (int, siginfo_t *, void *);
          }
        __sigaction_handler;
    # define sa_handler     __sigaction_handler.sa_handler
    # define sa_sigaction   __sigaction_handler.sa_sigaction
    #else
        __sighandler_t sa_handler;
    #endif
    
        /* Additional set of signals to be blocked.  */
        __sigset_t sa_mask;
    
        /* Special flags.  */
        int sa_flags;
    
        /* Restore handler.  */
        void (*sa_restorer) (void);
      };
    

    注意到有如下几个字段:

    • sa_handler 与 sa_sigaction这两个字段为联合体,因此只能同时设置一个,这两个字段的作用都是用来存储信号处理函数的指针,但是sa_sigaction作为信号处理函数,可以传入自定义的参数,而sa_handler不行
    • sa_mask用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
    • sa_flags用来设置信号处理的其他相关操作,有下列数值可用,用OR运算组合:
      A_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
      SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式
      SA_RESTART:被信号中断的系统调用会自行重启
      SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来
      SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction
      因此,实现ping命令过程中,对Ctrl+C的捕获及处理的具体实现方法如下:
    void SingnalHandler(int signo) {        //信号处理函数
        //处理过程
        ...
        exit(0);
    }
    
    int main(int argc, char * argv[]) {
        struct sigaction action;            //sigaction结构体
    
        action.sa_handler = SingnalHandler;
        sigemptyset(&action.sa_mask);
        action.sa_flags = 0;
    
        sigaction(SIGINT,&action,NULL);     //SIGINT = 2捕获Ctrl+C
    
        while(1)
        {
            //死循环
            ...
            sleep(1);
        }
    }
    

    最终实现代码

    1、main.cpp

    #include <signal.h>
    #include "src/ping.h"
    
    Ping * p;
    
    void SingnalHandler(int signo) {
    
        p->statistic();
    
        exit(0);
    }
    
    int main(int argc, char * argv[]) {
        struct sigaction action;
    
        action.sa_handler = SingnalHandler;
        sigemptyset(&action.sa_mask);
        action.sa_flags = 0;
    
        sigaction(SIGINT,&action,NULL);
    
        Ping ping(argv[1], 1);
        p = &ping;
        ping.CreateSocket();
        while(1)
        {
            ping.SendPacket();
            ping.RecvPacket();
            sleep(1);
        }
    }
    

    2、ping.h(在src目录下)

    //
    // Created by mylord on 2019/9/26.
    //
    
    #ifndef MYPING_PING_H
    #define MYPING_PING_H
    
    #include <iostream>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    #include <string>
    #include <string.h>
    #include <netinet/ip_icmp.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <sys/time.h>
    #include <arpa/inet.h>
    
    #define PACK_SIZE 32                //最小的ICMP数据包大小,8字节的ICMP包头,16字节的DATA,其中DATA是timeval结构体
    
    class Ping {
    private:
        std::string input_domain;       //用来存储通过main函数的参数传入的域名或者ip
        std::string backup_ip;          //通过输入的域名或者ip转化成为的ip备份
    
        int sock_fd;
    
        int max_wait_time;              //最大等待时间
    
        int send_pack_num;              //发送的数据包数量
        int recv_pack_num;              //收到的数据包数量
        int lost_pack_num;              //丢失的数据包数量
    
        struct sockaddr_in send_addr;   //发送到目标的套接字结构体
        struct sockaddr_in recv_addr;   //接受来自目标的套接字结构体
    
        char send_pack[PACK_SIZE];      //用于保存发送的ICMP包
        char recv_pack[PACK_SIZE + 20];      //用于保存接收的ICMP包
    
        struct timeval first_send_time; //第一次发送ICMP数据包时的UNIX时间戳
        struct timeval recv_time;       //接收ICMP数据包时的UNIX时间戳
    
        double min_time;
        double max_time;
        double sum_time;
    
    
        int GeneratePacket();
        int ResolvePakcet(int pack_szie);
    
        unsigned short CalculateCksum(unsigned short * send_pack, int pack_size);
    
    public:
        Ping(const char * ip, int max_wait_time);
        ~Ping();
    
        void CreateSocket();
    
        void SendPacket();
        void RecvPacket();
    
        void statistic();
    };
    
    
    #endif //MYPING_PING_H
    

    3、ping.cpp(在src目录下)

    //
    // Created by mylord on 2019/9/26.
    //
    
    #include "ping.h"
    
    Ping::Ping(const char * ip, int max_wait_time){
        this->input_domain = ip;
    
        this->max_wait_time = max_wait_time < 3 ? max_wait_time : 3;
    
        this->send_pack_num = 0;
        this->recv_pack_num = 0;
        this->lost_pack_num = 0;
    
        this->min_time = 0;
        this->max_time = 0;
        this->sum_time = 0;
    }
    
    Ping::~Ping() {
        if(close(sock_fd) == -1) {
            fprintf(stderr, "Close socket error:%s \n\a", strerror(errno));
            exit(1);
        }
    }
    
    void Ping::CreateSocket(){
        struct protoent * protocol;             //获取协议用
        unsigned long in_addr;                  //用来保存网络字节序的二进制地址
        struct hostent host_info, * host_pointer; //用于gethostbyname_r存放IP信息
        char buff[2048];                         //gethostbyname_r函数临时的缓冲区,用来存储过程中的各种信息
        int errnop = 0;                         //gethostbyname_r函数存储错误码
    
        //通过协议名称获取协议编号
        if((protocol = getprotobyname("icmp")) == NULL){
            fprintf(stderr, "Get protocol error:%s \n\a", strerror(errno));
            exit(1);
        }
    
        //创建原始套接字,这里需要root权限,申请完成之后应该降权处理
        if((sock_fd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) == -1){
            fprintf(stderr, "Greate RAW socket error:%s \n\a", strerror(errno));
            exit(1);
        }
    
        //降权处理,使该进程的EUID,SUID的值变成RUID的值
        setuid(getuid());
    
        //设置send_addr结构体
        send_addr.sin_family = AF_INET;
    
        //判断用户输入的点分十进制的ip地址还是域名,如果是域名则将其转化为ip地址,并备份
        //inet_addr()将一个点分十进制的IP转换成一个长整数型数
        if((in_addr = inet_addr(input_domain.c_str())) == INADDR_NONE){
            //输入的不是点分十进制的ip地址
            if(gethostbyname_r(input_domain.c_str(), &host_info, buff, sizeof(buff), &host_pointer, &errnop)){
                //非法域名
                fprintf(stderr, "Get host by name error:%s \n\a", strerror(errno));
                exit(1);
            } else{
                //输入的是域名
                this->send_addr.sin_addr = *((struct in_addr *)host_pointer->h_addr);
            }
        } else{
            //输入的是点分十进制的地址
            this->send_addr.sin_addr.s_addr = in_addr;
        }
    
        //将ip地址备份下来
        this->backup_ip = inet_ntoa(send_addr.sin_addr);
    
        printf("PING %s (%s) %d(%d) bytes of data.\n", input_domain.c_str(),
                backup_ip.c_str(), PACK_SIZE - 8, PACK_SIZE + 20);
    
        gettimeofday(&first_send_time, NULL);
    }
    
    unsigned short Ping::CalculateCksum(unsigned short * send_pack, int pack_size){
        int check_sum = 0;              //校验和
        int nleft = pack_size;          //还未计算校验和的数据长度
        unsigned short * p = send_pack; //用来做临时指针
        unsigned short temp;            //用来处理字节长度为奇数的情况
    
        while(nleft > 1){
            check_sum += *p++;          //check_sum先加以后,p的指针才向后移
            nleft -= 2;
        }
    
        //奇数个长度
        if(nleft == 1){
            //利用char类型是8个字节,将剩下的一个字节压入unsigned short(16字节)的高八位
            *(unsigned char *)&temp = *(unsigned char *)p;
            check_sum += temp;
        }
    
        check_sum = (check_sum >> 16) + (check_sum & 0xffff);   //将之前计算结果的高16位和低16位相加
        check_sum += (check_sum >> 16);                         //防止上一步也出现溢出
        temp = ~check_sum;              //temp是最后的校验和
    
        return temp;
    }
    
    int Ping::GeneratePacket()
    {
        int pack_size;
        struct icmp * icmp_pointer;
        struct timeval * time_pointer;
    
        //将发送的char[]类型的send_pack直接强制转化为icmp结构体类型,方便修改数据
        icmp_pointer = (struct icmp *)send_pack;
    
        //type为echo类型且code为0代表回显应答(ping应答)
        icmp_pointer->icmp_type = ICMP_ECHO;
        icmp_pointer->icmp_code = 0;
        icmp_pointer->icmp_cksum = 0;           //计算校验和之前先要将校验位置0
        icmp_pointer->icmp_seq = send_pack_num + 1; //用send_pack_num作为ICMP包序列号
        icmp_pointer->icmp_id = getpid();       //用进程号作为ICMP包标志
    
        pack_size = PACK_SIZE;
    
        //将icmp结构体中的数据字段直接强制类型转化为timeval类型,方便将Unix时间戳赋值给icmp_data
        time_pointer = (struct timeval *)icmp_pointer->icmp_data;
    
        gettimeofday(time_pointer, NULL);
    
        icmp_pointer->icmp_cksum = CalculateCksum((unsigned short *)send_pack, pack_size);
    
        return pack_size;
    }
    
    void Ping::SendPacket() {
        int pack_size = GeneratePacket();
    
        if((sendto(sock_fd, send_pack, pack_size, 0, (const struct sockaddr *)&send_addr, sizeof(send_addr))) < 0){
            fprintf(stderr, "Sendto error:%s \n\a", strerror(errno));
            exit(1);
        }
    
        this->send_pack_num++;
    }
    
    //要对收到的IP数据包去IP报头操作,校验ICMP,提取时间戳
    int Ping::ResolvePakcet(int pack_size) {
        int icmp_len, ip_header_len;
        struct icmp * icmp_pointer;
        struct ip * ip_pointer = (struct ip *)recv_pack;
        double rtt;
        struct timeval * time_send;
    
        ip_header_len = ip_pointer->ip_hl << 2;                     //ip报头长度=ip报头的长度标志乘4
        icmp_pointer = (struct icmp *)(recv_pack + ip_header_len);  //pIcmp指向的是ICMP头部,因此要跳过IP头部数据
        icmp_len = pack_size - ip_header_len;                       //ICMP报头及ICMP数据报的总长度
    
        //收到的ICMP包长度小于报头
        if(icmp_len < 8) {
            printf("received ICMP pack lenth:%d(%d) is error!\n", pack_size, icmp_len);
            lost_pack_num++;
            return -1;
        }
        if((icmp_pointer->icmp_type == ICMP_ECHOREPLY) &&
            (backup_ip == inet_ntoa(recv_addr.sin_addr)) &&
            (icmp_pointer->icmp_id == getpid())){
    
            time_send = (struct timeval *)icmp_pointer->icmp_data;
    
            if((recv_time.tv_usec -= time_send->tv_usec) < 0) {
                --recv_time.tv_sec;
                recv_time.tv_usec += 10000000;
            }
    
            rtt = (recv_time.tv_sec - time_send->tv_sec) * 1000 + (double)recv_time.tv_usec / 1000.0;
    
            if(rtt > (double)max_wait_time * 1000)
                rtt = max_time;
    
            if(min_time == 0 | rtt < min_time)
                min_time = rtt;
            if(rtt > max_time)
                max_time = rtt;
    
            sum_time += rtt;
    
            printf("%d byte from %s : icmp_seq=%u ttl=%d time=%.1fms\n",
                   icmp_len,
                   inet_ntoa(recv_addr.sin_addr),
                   icmp_pointer->icmp_seq,
                   ip_pointer->ip_ttl,
                   rtt);
    
            recv_pack_num++;
        } else{
            printf("throw away the old package %d\tbyte from %s\ticmp_seq=%u\ticmp_id=%u\tpid=%d\n",
                   icmp_len, inet_ntoa(recv_addr.sin_addr), icmp_pointer->icmp_seq,
                   icmp_pointer->icmp_id, getpid());
    
            return -1;
        }
    
    }
    
    void Ping::RecvPacket() {
        int recv_size, fromlen;
        fromlen = sizeof(struct sockaddr);
    
        while(recv_pack_num + lost_pack_num < send_pack_num) {
            fd_set fds;
            FD_ZERO(&fds);              //每次循环都必须清空FD_Set
            FD_SET(sock_fd, &fds);      //将sock_fd加入集合
    
            int maxfd = sock_fd + 1;
            struct timeval timeout;
            timeout.tv_sec = this->max_wait_time;
            timeout.tv_usec = 0;
    
            //使用select实现非阻塞IO
            int n = select(maxfd, NULL, &fds, NULL, &timeout);
    
            switch(n) {
                case -1:
                    fprintf(stderr, "Select error:%s \n\a", strerror(errno));
                    exit(1);
                case 0:
                    printf("select time out, lost packet!\n");
                    lost_pack_num++;
                    break;
                default:
                    //判断sock_fd是否还在集合中
                    if(FD_ISSET(sock_fd, &fds)) {
                        //还在集合中则说明收到了回显的数据包
                        if((recv_size = recvfrom(sock_fd, recv_pack, sizeof(recv_pack),
                                0, (struct sockaddr *)&recv_addr, (socklen_t *)&fromlen)) < 0) {
                            fprintf(stderr, "packet error(size:%d):%s \n\a", recv_size, strerror(errno));
                            lost_pack_num++;
                        } else{
                            //收到了可能合适的数据包
                            gettimeofday(&recv_time, NULL);
    
                            ResolvePakcet(recv_size);
                        }
                    }
                    break;
            }
        }
    }
    
    void Ping::statistic() {
        double total_time;
        struct timeval final_time;
        gettimeofday(&final_time, NULL);
    
        if((final_time.tv_usec -= first_send_time.tv_usec) < 0) {
            --final_time.tv_sec;
            final_time.tv_usec += 10000000;
        }
        total_time = (final_time.tv_sec - first_send_time.tv_sec) * 1000 + (double)final_time.tv_usec / 1000.0;
    
        printf("\n--- %s ping statistics ---\n",input_domain.c_str());
        printf("%d packets transmitted, %d received, %.0f%% packet loss, time %.0f ms\n",
                send_pack_num, recv_pack_num, (double)(send_pack_num - recv_pack_num) / (double)send_pack_num,
                total_time);
        printf("rtt min/avg/max = %.3f/%.3f/%.3f ms\n", min_time, (double)sum_time / recv_pack_num, max_time);
    
    
    }
    

    4、CMakeLists.txt

    cmake_minimum_required(VERSION 3.10)
    project(MyPing)
    
    set(CMAKE_CXX_STANDARD 14)
    
    add_executable(MyPing main.cpp src/ping.cpp src/ping.h)
    

    5、编译及运行过程

    cmake .
    sudo make
    sudo chmod u+s MyPing
    ./MyPing www.baidu.com
    

    参考文献:
    https://blog.csdn.net/zhaorenjie93/article/details/72859715
    https://www.cnblogs.com/aspirant/p/4084127.html
    http://abcdxyzk.github.io/blog/2015/04/14/kernel-net-sock-raw/
    https://blog.csdn.net/zhj082/article/details/80518322
    https://blog.csdn.net/yzy1103203312/article/details/79799197


    相关文章

      网友评论

          本文标题:在Linux上用C++实现Ping

          本文链接:https://www.haomeiwen.com/subject/qfewvctx.html