ping命令的实现

/*注意:1,由于本人技术有限,所以如果你是技术的狂热追求者,我觉得你不用看我写的了,这是因为我很菜。。。我主要是帮助与我一样,想做一些事,但不知道从哪里做起的人的。
2,为了叙述简洁,以下内容可能并经不起推敲(可能还有错误),也有很多省略,所以,希望读者能够在看完本文后再看其他相应书籍,以对其深入的知识进行全面了解。
*/
一,前言

为了判断与对方主机的连通性,不论是Linux系统还是Windows,我们通常都会使用ping命令来进行判断,今天我的目标便是用尽量简单的描述教会有这方面需求的童鞋来实现自己的ping命令。
刚开始我在网上也找到了好多相关的教程,但是发现它们却并不友好,有好多我无法理解的,可以说是各种函数,技巧的堆积,对于一个想简单的实现ping命令或学习这方面的人来讲代价太大,所以经过我的实践,我想把自己的经验分享出来。以下内容的阅读不强制但建议你最好具备以下的技能:
1,计算机网络的相关知识
2,在Linux下接触过socket编程
二,内容
2.1 ICMP报文
在实现ping命令前我们首先需要知道它的大致原理。ICMP即网际控制报文协议,它主要有两个功能,一种是icmp差错控制报文,另一种是icmp请求应答报文,对于前者有兴趣的同学可以自行了解,今天我们所要了解的内容是针对第二种报文来展开的。
ICMP请求应答报文通俗地讲它是用来判断对方主机情况的。规定当一台主机向另一台主机发送icmp请求报文的时候,对方需要回一个icmp应答报文,但也有例外情况。

2.2 ICMP请求应答报文格式
这里写图片描述
(注:图片来自网络)
icmp请求应答报文前8个字节是固定格式。
其中类型为ICMP_ECHO(即为8),code为0时它代表请求回送报文,
当类型为ICMP_ECHOREPLY(即为0),code为0时它代表请求回应报文。
校验和:因为icmp报文是依靠IP数据报来传送的(IP首部+icmp报文),而IP协议又是不可靠的传输协议,所以通过检验和可以判断传输过来的数据是否发生变化(如何检验请自行学习),为什么要判断数据是否发生变化呢?这是因为,网络中有大量的数据,一台主机往往会收到很多数据,如果主机发现发送来的数据有错误,则可以直接丢弃,而不用花费多余的时间对错误的数据进行处理,这就提高了网络的效率。
标识符:用当前进程的pid号进行填充。
序号:一般从0开始,随着发送报文的增多依次递增1。

2.3 ping命令的消息格式
这里写图片描述
如上所示,内容主要包括字节数,时间,TTL。
其中TTL(time to live),指经过的路由器数,它在IP数据报的ip_ttl字段有携带。
时间我们可以在icmp报文的数据字段里带上当前时间的信息,主要借助了timeval结构体,其内容如下:

 struct timeval {
          time_t       tv_sec;     /* seconds */
          suseconds_t   tv_usec; /* microseconds */
    };

它在sys/time.h头文件里有定义,其中tv_sec指当前的秒数,tv_usec指当前的微妙,需要注意的是这里的时间代表的是自1970年1月1日0时起到现在所经过的时间,即时间戳。所以在计算经过的时间时你可以用两个时间戳进行相减,所得的时间即为经过的时间。
接下来看一个timeval的应用:

#include<stdio.h>
#include<sys/time.h>
int main(void)
{
 struct timeval tal;
 int i=0;
 for(;i<3;i++)
 {
  gettimeofday(&tal,NULL);
  printf("%d  %d\n",tal.tv_sec,tal.tv_usec);
  sleep(1);
 }   
 return 0;

}

gettimeofday函数:
其原型为:int gettimeofday(struct timeval*tv,struct timezone *tz )
其功能是把当前时间信息放入tv所指向的结构体中,而tz则指向时区信息,由于我们并不需要,所以置为NULL;
当函数成功时会返回0,失败-1.
所以我们这里演示的代码为计算每过一秒tv结构体里所存放的信息。
运行结果:
这里写图片描述
我们每过一秒循环打印当前的时间戳,从它们之间的tv_sec区别,我们也可以看出相差一秒。

2.3 ICMP结构体与IP结构体
linux中
icmp报文结构定义在netinet/ip_icmp.h
ip报文结构定义在netinet/ip.h

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
    u_int8_t ip_tos; /* type of service */
    u_short ip_len; /* total length */
    u_short ip_id; /* identification */
    u_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 */
    u_int8_t ip_ttl; /* time to live */
    u_int8_t ip_p; /* protocol */
    u_short ip_sum; /* checksum */
    struct in_addr ip_src, ip_dst; /* source and dest address */
  }; 

struct icmp
{
  u_int8_t  icmp_type;    /* 消息类型 */
  u_int8_t  icmp_code;    /* 代码 */
  u_int16_t icmp_cksum;    /* 校验和 */
  union
  {
    u_char ih_pptr;        /* ICMP_PARAMPROB */
    struct in_addr ih_gwaddr;    /* gateway address */
    struct ih_idseq        /* 显示数据报 */
    {
      u_int16_t icd_id; /*数据报ID*/
      u_int16_t icd_seq;/*数据报序号*/
    } ih_idseq;
    u_int32_t ih_void;

    /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
    struct ih_pmtu
    {
      u_int16_t ipm_void;
      u_int16_t ipm_nextmtu;
    } ih_pmtu;

    struct ih_rtradv
    {
      u_int8_t irt_num_addrs;
      u_int8_t irt_wpa;
      u_int16_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
    {
      u_int32_t its_otime;/*时间戳协议请求时间*/
      u_int32_t its_rtime;/*时间戳协议接收时间*/
      u_int32_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;
    u_int32_t   id_mask;/*子网掩码的子网掩码*/
    u_int8_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
};

我相信看到上面的定义许多人都一脸懵逼,我也不例外,虽然为了实现各种功能,Linux自带的结构体定义的十分完善,但我们并用不到所有的字段。所以接下来只讨论我们需要的地方。
IP数据报中我们只需要注意的是ip_ttl字段。
而icmp报文我们需要知道的是:
icmp_type:类型
icmp_code:代码
icmp_cksum:检验和
icmp_id:标识符
icmp_seq:序号
icmp_data:携带数据

2.4 实现步骤
1,构建一个icmp请求应答报文,并把当前的时间信息放入到数据字段里
2,接收应答报文。拿到报文后需要解包,因为icmp是依靠IP数据报来传送的,所以只需要根据ip_hl字段便能知道IP首部的长度,则去掉IP首部之后剩下的自然是我们想要的icmp报文。
3,当我们发送一个ICMP请求应答报文后,对方收到后也会回复相应的内容,只是类型字段变为了ICMP_ECHOREPLY。

三,代码实现
环境:centos
3.1 相应的头文件与函数声名

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<netinet/ip_icmp.h>
#include<signal.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<string.h>
#include<netdb.h>
#include<pthread.h>  //使用多线程来收发数据报

#define PACK_SIZE 4096  //包的大小
char send_packet[PACK_SIZE]; //发的包
char recv_packet[PACK_SIZE]; //收的包
int sockfd;//套接字
pid_t pid;//进程号
int begin=0,end=0; //用begin来记录发的包,end用来记录收到的包,
//因为有可能因为网络问题发生丢包事件
int datalen=56; //icmp包数据的大小
struct timeval recv_time;
struct sockaddr_in from;
void* send_data(void* arg); //发数据
void* recv_data(void* arg);//收数据
unsigned short get_cksum(unsigned char*,int); //计算校验和
void time_sub(struct timeval*,struct timeval*); //计算时间差

阅读建议:其中检验和的计算,网上有好多,我是直接拿来使用的,其他函数我都进行了相应的注释。为什么使用多线程?是因为我想实现可以一直发,用户想停的时候再停,但因为我的知识水平有限,所以没有实现,具体的效果可以参照Linux下的ping命令。
另外有什么不了解的地方可以一个函数,一个函数的攻克,也可以先在自己的电脑上敲一遍,实现功能,然后再结合本文与相关资料进行深入了解。
全部代码如下:

/*
**time:2017.11.22
**author:奔跑的代码君
**功能:ping命令的模拟,实现直接ping ip,以及域名
**环境:centos
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<netinet/ip_icmp.h>
#include<signal.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<string.h>
#include<netdb.h>
#include<pthread.h>
#define PACK_SIZE 4096
char send_packet[PACK_SIZE]; //发的包
char recv_packet[PACK_SIZE]; //收的包
int sockfd;//套接字
pid_t pid;//进程号
int begin=0,end=0; //记录发收的包的数量
int datalen=56; //icmp数据长度
struct timeval recv_time; //记录某一刻的时间
struct sockaddr_in from; 
void* send_data(void* arg); //发数据
void* recv_data(void* arg); //收数据
unsigned short get_cksum(unsigned char*,int); //校验和计算
void time_sub(struct timeval*,struct timeval*); //计算时间差
void* send_data(void* arg) 
{
 int packetsize;
   int i=0;       
   for(;i<4;i++)
   {       
    packetsize=pack(i); //构建4个包,把序号传过去     
    if(sendto(sockfd,send_packet,packetsize,0,(struct sockaddr*)&from,sizeof(from))<0) //发数据
   {
    //失败    
    perror("sendto");
    continue;
   }
   else 
    {
     begin++; //成功后,休眠1s 
     //printf("send is success\n");
     sleep(1);
    }
  }
 return NULL;
}
//收数据
void* recv_data(void* arg)
 {
  int n,fromlen;
  fromlen=sizeof(from);
   int i=0;
  while(i<4)           
  {
   if((n=recvfrom(sockfd,recv_packet,sizeof(recv_packet),0,(struct sockaddr*)&from,&fromlen))<0) //收数据
   {
   continue;
   }
   if(unpack(recv_packet,n)==-1) //解包失败
        {
          i++;
          continue;  
        } 

   else
   {
     end++; //成功
   }
  }   

 }      
 //校验和计算
unsigned short get_cksum(unsigned char* data,int len) //求检验和
 {
  int sum=0;
  int odd=(len&0x01);
  while(len&0xfffe)
  {
   sum+=*(unsigned short*)data;
   data+=2;
   len-=2;
  }
  if(odd)
  {
   unsigned short tmp=((*data)<<8)&0xff00;
   sum+=tmp;
  }   
  sum=(sum>>16)+(sum&0xffff);
  sum+=(sum>>16);
  return ~sum; 
 }
//构建包
int pack(int pack_no)
 {
  int size;      
  struct icmp* myicmp;
  myicmp=(struct icmp*)send_packet;
  //对相应字段填充
  myicmp->icmp_type=ICMP_ECHO;
  myicmp->icmp_code=0;
  myicmp->icmp_cksum=0;
  myicmp->icmp_seq=pack_no;
  myicmp->icmp_id=pid;
  size=8+datalen; //总的icmp大小
  //将时间信息放入data字段
  struct timeval*send_time=(struct timeval*)myicmp->icmp_data;
  gettimeofday(send_time,NULL);
  //注意检验和放最后,因为数据也要进行校验
  myicmp->icmp_cksum=get_cksum(send_packet,size);
  return size;
 }

//解包
int unpack(char* buf,int len)
{
 int ip_len;
 struct ip* myip;
 struct icmp* myicmp;
 myip=(struct ip*)buf;
 ip_len=myip->ip_hl<<2; //IP首部长度*4
 myicmp=(struct icmp*)(buf+ip_len); //获得ICMP报文
 len-=ip_len;
 if(len<8)  //ICMP固定长度为8,小于即错误
 {
  printf("icmp packet is small than 8\n");
  return -1;
 }   
// printf("i am coming\n");
 if(myicmp->icmp_type==ICMP_ECHOREPLY && myicmp->icmp_id==pid) //判断是不是想要的包
  {
//  printf("i am coming\n");          
   gettimeofday(&recv_time,NULL);
   struct timeval* sendTime=(struct timeval*)myicmp->icmp_data;
   time_sub(&recv_time,sendTime); //计算时间差放到recv_time里面
   double rtt=recv_time.tv_sec*1000+recv_time.tv_usec/1000; //以ms为单位
   //打印相关信息
   printf("%d byte from %s:icmp_seq=%u ttl=%d rtt=%fms\n",len,inet_ntoa(from.sin_addr),myicmp->icmp_seq,myip->ip_ttl,rtt);
  }
 else
    //不是想要的包
   return -1;        
}
//时间差计算
void time_sub(struct timeval* end,struct timeval* begin)
{
 end->tv_sec-=begin->tv_sec;
 end->tv_usec-=begin->tv_usec;
}

//主函数
int main(int argc,char* argv[])
{
 if(argc<2)
  {
   printf("usage:%s hostname/ip\n",argv[0]);
   exit(-1);
  }      
 if((sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))<0) //创建原始套接字
 {
  perror("socket");
  exit(-1);
 } 
 memset(&from,0,sizeof(from));
 //此处内容可以看我上篇博客有详细的介绍
 if((from.sin_addr.s_addr=inet_addr(argv[1]))==INADDR_NONE)//如果传入的不是ip
 {
  struct hostent* hptr=NULL;
  //转换失败
  if((hptr=gethostbyname(argv[1]))==NULL)
  {
   perror("gethostbyname");
   exit(-1);
  } 
  memcpy(&from.sin_addr,hptr->h_addr,sizeof(hptr->h_addr));
 }
 else 
 {
  from.sin_addr.s_addr=inet_addr(argv[1]);
 }
 from.sin_family=AF_INET;
 pid=getpid();
 printf("ping %s,%d data in icmp packets\n",argv[1],datalen);
 pthread_t id1,id2;
 //创建线程
 if(pthread_create(&id1,NULL,send_data,NULL)<0)
 {
  perror("pthread_create");
  return -2;
 }
 if(pthread_create(&id2,NULL,recv_data,NULL)<0)
 {
  perror("pthread_create");
  return -3;
 }
 //阻塞等待      
 pthread_join(id1,NULL);
 pthread_join(id2,NULL);
 close(sockfd); //记得关闭套接字
 printf("ping %s is complete,%d was sended and %d was received\n",argv[1],begin,end);
 return 0; 
}   

编译命令:gcc -o pingbyme pingbyme.c -lpthread

运行结果:
这里写图片描述

四,总结
看到这里希望你也能实现自己的ping命令吧。在ping命令的实现过程中我收获了很多知识,有些问题果然没有比自己敲一遍来的更快。另外在实现过程中我翻阅了许多大佬们的实现代码,然后在自己的理解下借鉴以及改进最后实现了这个“乞丐版”,它还有很多不足,也希望你能从中找到自己更好的理解吧。

  • 6
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以使用ping命令来测试网络的连通情况,具体操作如下: 1. 打开命令行窗口(Windows系统按下Win+R,输入cmd,按回车键即可打开命令行窗口)。 2. 在命令行窗口中输入ping命令和需要测试的目标IP地址(或域名),例如: ``` ping www.baidu.com ``` 3. 按下回车键执行命令,等待命令执行结果。 4. 如果网络连通正常,命令行窗口中会显示类似如下的结果: ``` Reply from 14.215.177.38: bytes=32 time=19ms TTL=51 Reply from 14.215.177.38: bytes=32 time=18ms TTL=51 Reply from 14.215.177.38: bytes=32 time=19ms TTL=51 Reply from 14.215.177.38: bytes=32 time=19ms TTL=51 Ping statistics for 14.215.177.38: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 18ms, Maximum = 19ms, Average = 18ms ``` 上面的结果中,Reply from后面的IP地址表示目标主机的IP地址,bytes表示发送的数据包大小,time表示往返时间,TTL表示生存时间,Ping statistics中的Sent、Received和Lost分别表示发送的数据包数量、接收到的数据包数量和丢失的数据包数量,Approximate round trip times中的Minimum、Maximum和Average分别表示最小往返时间、最大往返时间和平均往返时间。 5. 如果网络连通异常,命令行窗口中会显示类似如下的结果: ``` Request timed out. Request timed out. Request timed out. Request timed out. Ping statistics for 14.215.177.38: Packets: Sent = 4, Received = 0, Lost = 4 (100% loss), ``` 上面的结果中,Request timed out表示请求超时,Ping statistics中的Sent、Received和Lost分别表示发送的数据包数量、接收到的数据包数量和丢失的数据包数量,100% loss表示全部丢失。这种情况可能是网络故障、目标主机关闭或者目标主机防火墙阻止了ping请求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值