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

来源:互联网 发布:access sql limit 编辑:程序博客网 时间:2024/06/14 23:54

距离上次搭建框架已经过去了一个星期,在反复测试后,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的模拟实现源码

原创粉丝点击