【计算机网络】网络诊断工具ping的模拟实现之具体细节
距离上次搭建框架已经过去了一个星期,在反复测试后,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的模拟实现源码