欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【计算机网络】网络诊断工具ping的模拟实现之具体细节

程序员文章站 2024-03-17 10:14:22
...

距离上次搭建框架已经过去了一个星期,在反复测试后,ping终于可以按照我所期望的这样来运行了。在搭建框架的时候,以为这个小项目不是很难,但最后在很多细节上花费了很多时间。

下来,就每部分做个总结。首先说主函数部分。

int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        usage(argv[0]);
        return 1;
    }
    struct hostent *host=NULL;
    struct protoent *protocol=NULL;

    int size=128*K;
    protocol=getprotobyname("icmp");

    if(protocol==NULL)
    {
        perror("getprotobyname");
        return 2;
    }

    memcpy(dest_str,argv[1],strlen(argv[1])+1);
    memset(pingpacket,0,sizeof(pingm_packet)*128);

    rawsock=socket(AF_INET,SOCK_RAW,protocol->p_proto);
    if(rawsock<0)
    {
        perror("socket");
        return 3;
    }

    pid=getpid();
    setsockopt(rawsock,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size)); //增大接收缓冲区
    bzero(&dest,sizeof(&dest));
    dest.sin_family=AF_INET;

    unsigned long  inaddr=inet_addr(argv[1]);
    if(inaddr==INADDR_NONE)
    {
        host=gethostbyname(argv[1]);
        if(host==NULL)
        {
            perror("gethostname");
            return 4;
        }
        memcpy((char*)&dest.sin_addr,host->h_addr,host->h_length);
    }
    else
    {
    memcpy((char*)&dest.sin_addr,&inaddr,sizeof(inaddr));
    }

    inaddr=dest.sin_addr.s_addr;
    printf("PING %s 56(84) bytes of data.\n",dest_str);

    signal(SIGINT,icmp_sigint);

    alive=1;
    pthread_t send_id,recv_id;
    int err=0;
    err=pthread_create(&send_id,NULL,icmp_send,NULL);
    if(err<0)
    {
        return 5;
    }

    err=pthread_create(&recv_id,NULL,icmp_recv,NULL);
    if(err<0)
    {
        return 6;
    }

    pthread_join(send_id,NULL);
    pthread_join(recv_id,NULL);

    close(rawsock);
    icmp_statistics();
    return 0;
}

最开始我想用一个while死循环去不断发送接收,但是在这里就会出现一些问题,比如说在Linux下可能会出现多线程问题,在发送还未执行的时候,已经开始接收了,程序接收不到东西就会崩溃等等问题,整个程序的健壮性都不太好。最后在这里,我选择去创建两个线程,两个线程只负责自己的事,一个只发送一个只接收。如果,发送因为网络问题一直没有发送成功,接收的线程就会一直在判断是否接收到报文,如果没有接收到报文,就会继续等着接收。这样的话,程序更加健壮,而且更贴切ping命令。

下来接着说发送部分。

void *icmp_send()
{
    gettimeofday(&tv_begin,NULL);
    while(alive)
    {
        int size=0;
        struct timeval tv;
        gettimeofday(&tv,NULL);  //当前包发送的时间
        pingm_packet *packet=icmp_findpacket(-1);
        if(packet)
        {
            packet->seq=packet_send;
            packet->flag=1;
            gettimeofday(&packet->tv_begin,NULL);
        }
        icmp_pack((struct icmp*)send_buff,packet_send,&tv,64);

        size=sendto(rawsock,send_buff,64,0,(struct sockaddr*)&dest,sizeof(dest));
        if(size<0)
        {
            perror("sendto");
            continue;
        }
        packet_send++;
        sleep(1);
    }
}
void icmp_pack(struct icmp *icmp,int seq,struct timeval *tv,int length)
{
    unsigned char i=0;
    icmp->icmp_type=ICMP_ECHO;
    icmp->icmp_code=0;
    icmp->icmp_cksum=0;
    icmp->icmp_seq=seq;
    icmp->icmp_id=pid & 0xffff;
    for(i=0;i<length;i++)
    {
        icmp->icmp_data[i]=i;
    }

    icmp->icmp_cksum=icmp_cksum((unsigned char*)icmp,length);

//  printf("send: type: %d, code: %d, sum: %d, seq: %d, id: %d\n", \
            icmp->icmp_type, icmp->icmp_code, icmp->icmp_cksum, \
            icmp->icmp_seq, icmp->icmp_id);
//  fflush(stdout);
}

在发送部分,值得注意的是在我们发送一个icmp报文的时候,需要做的事情。第一是要按照icmp报文的格式去构建一个报文,将信息填充进去,在填充信息的时候,一定要明白发送的时候,type和code都是0,type是ICMP_ECHO。这部分报文的知识,可以去查看【计算机网络】网络诊断工具ping的模拟实现之基础知识

unsigned short icmp_cksum(unsigned char* data,int len)
{
    int sum=0;
    int old=len & 0x01;  //判断是否为奇数
    while(len & 0xfffe)  //将数据按照2个字节为单位累加
    {
        sum+=*(unsigned short*)data;
        data+=2;
        len-=2;
    }

    if(old)   //如果是奇数,则需要对最后的一个数据进行处理
    {
        unsigned short tmp=((*data)<<8)&0xff00;
        sum+=tmp;
    }

    sum=(sum>>16)+(sum & 0xffff);  //将高低位相加
    sum+=(sum>>16);  //将最高位相加

    return ~sum;
}

校验算法是采用的是TCP/IP校验最经典的,将所有位和累加计算,然后返回。

终于要说接收部分了,重中之重!

void *icmp_recv()
{
    struct timeval tv;
    tv.tv_usec=200;
    tv.tv_sec=0;
    fd_set readfd;

    while(alive)
    {
        int ret=0;
        FD_ZERO(&readfd);
        FD_SET(rawsock,&readfd);
        ret=select(rawsock+1,&readfd,NULL,NULL,&tv);
        switch(ret)
        {
            case -1: //发生错误
                break;
            case 0:  //超时
                break;
            default:  //收到一个包
                {
                    int fromlen=0;
                    struct sockaddr from;
                    int size=recv(rawsock,recv_buff,sizeof(recv_buff),0);
                    if(errno==EINTR)
                    {
                        perror("recv");
                        continue;
                    }
                    ret=icmp_unpack(recv_buff,size);
                    if(ret==-1)
                        continue;
                }
        break;
        }
    }
}

当有报文发送,我们就一直处于接收状态。采用select可以使所有可读的报文排队依次进行读取。然后我们依次进行读取信息。在经过解包处理,即可将报文信息展示出来。接下来就说说这个解包函数。

int icmp_unpack(char *buf,int len)
{
    int i=0;
    int iphl=0;
    struct ip *ip=NULL;
    struct icmp *icmp=NULL;
    int rtt=0;

    ip=(struct ip*)buf;
    iphl=ip->ip_hl<<2;
    icmp=(struct icmp*)(buf+iphl);
    len-=iphl;
    if(len<8)
    {
        printf("icmp packet length is less than 8\n");
        return -1;
    }

//  printf("recv: type: %d, code: %d, sum: %d, seq: %d, id: %d\n",\
            icmp->icmp_type, icmp->icmp_code, icmp->icmp_cksum,\
            icmp->icmp_seq, icmp->icmp_id);

//  fflush(stdout);
    if((icmp->icmp_type==ICMP_ECHOREPLY)&&(icmp->icmp_id==pid))
    {
        struct timeval tv_interval;
        struct timeval tv_send;
        struct timeval tv_recv;

        pingm_packet *packet=icmp_findpacket(icmp->icmp_seq);
        if(packet==NULL)
            return -1;

        packet->flag=0;
        tv_send=packet->tv_begin;

        gettimeofday(&tv_recv,NULL);
        tv_interval=icmp_tvsub(tv_recv,tv_send);
        rtt=tv_interval.tv_sec*1000+tv_interval.tv_usec/1000;

        tmp_rtt[packet_recv]=rtt;
        all_time+=rtt;
        packet_recv++;
        printf("%d byte from %s:icmp_seq=%u ttl=%d rtt=%d ms\n",len,inet_ntoa(ip->ip_src),icmp->icmp_seq,ip->ip_ttl,rtt);
    }
    else
        return -1;
}

在这里我们是将读取的信息存储在一个buf里,然后去分离报头部分,判断该报文是不是我们发送的,如果可以确定是我们发送的,就可以获取报文的信息了,通过报文计算往返时间、得到报文的长度、第几次报文等等信息。在这里,我建立了一个查找报文的函数icmp_findpacket,因为每次发送的报文信息,都被我们保存在一个结构体里,然后收到报文的时候,进行二次判断,确认报文是我们发送的第几次报文的回应,以防报文不是我们发送的。

pingm_packet *icmp_findpacket(int seq)  //查找报文
{
    int i=0;
    pingm_packet *found=NULL;
    if(seq==-1)   
    {
        for(i=0;i<128;i++)
        {
            if(pingpacket[i].flag==0)
            {
                found=&pingpacket[i];
                break;
            }
        }
    }
    else if(seq>=0)
    {
        for(i=0;i<128;i++)
        {
            if(pingpacket[i].seq==seq)
            {
                found=&pingpacket[i];
                break;
            }
        }
    }
    return found;
}


struct timeval icmp_tvsub(struct timeval end,struct timeval begin)   //计算往返时间
{
    struct timeval tv;
    tv.tv_sec=end.tv_sec-begin.tv_sec;
    tv.tv_usec=end.tv_usec-begin.tv_usec;

    if(tv.tv_usec<0)   //借位
    {
        tv.tv_sec-=1;
        tv.tv_usec+=1000000;
    }
    return tv;
}

整个代码的主要逻辑已经说完了,下面是几个显示函数,一个是计算最大最小平均时间,一个显示。

void icmp_statistics()
{
    long time=(tv_interval.tv_sec *1000)+(tv_interval.tv_usec/1000);
    cal_rtt();
    printf("---%s ping statistics---\n",dest_str);
    printf("%d packets transmitted,%d received,%d%c packet loss,time %ld ms.\n",\
            packet_send,packet_recv,(packet_send-packet_recv)*100/packet_send,'%',time);
    printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/%.3f ms\n",min,avg,max,mdev);
}

void cal_rtt()
{
    double sum_avg=0;
    int i=0;
    min=max=tmp_rtt[0];
    avg=all_time/(double)packet_recv;
    for(i=0;i<(double)packet_recv;i++)
    {
        if(tmp_rtt[i]<min)
            min=tmp_rtt[i];
        if(tmp_rtt[i]>max)
            max=tmp_rtt[i];

        if((tmp_rtt[i]-avg)>0)
            sum_avg+=tmp_rtt[i]-avg;
        else
            sum_avg+=avg-tmp_rtt[i];
    }
    mdev=sum_avg/packet_recv;
}

总结一下,在这个小项目中,很多地方都是非常基础的。但是也往往是最容易写错的,一定要把握好打包解包时报文的正确性。在测试的时候,先可以将报文的信息打印出来进行查看,看发送的报文和接收的报文信息是否一致,逐步一点一点去测试。最后看下分别ping自己和ping百度的效果。

【计算机网络】网络诊断工具ping的模拟实现之具体细节

源码查看下载地址:ping的模拟实现源码