使用linux的ICMP实现ping功能

1.ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报协议由ICMP报头和IP报文封装而成。
2.IP层协议是一种点对点的协议,而非端对端协议,它提供无连接数据报服务,极少使用bind()和connect(),发送使用sendto()函数,接收使用recvfrom()函数

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 */
  };
其中,ping程序只使用以下数据:

IP报头长度IHL(Internet Header Length)以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。
生存时间TTL(Time To Live)以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。

3.ICMP报文分为两种:一种是错误报告报文,二是查询报文。每个ICMP报文包含类型、编码和校验和三个内容。Ping命令只使用众多ICMP报文的两种:请求回送(ICMP_ECHO)和请求回应(ICMP_ECHOREPLY)。

struct icmp
{
  u_int8_t  icmp_type;  /* type of message, see below */
  u_int8_t  icmp_code;  /* type sub code */
  u_int16_t icmp_cksum; /* ones complement checksum of struct */
  union
  {
    u_char ih_pptr;     /* ICMP_PARAMPROB */
    struct in_addr ih_gwaddr;   /* gateway address */
    struct ih_idseq     /* echo datagram */
    {
      u_int16_t icd_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
};
其中,ICMP报文的校验和算法使用二进制反码相加。对一个无符号的数,先求其反码,然后从低位到高位,按位相加,有溢出则向高位进1(跟一般的二进制加法规则一样),若最高位有进位,则向最低位进1。关于二进制反码求和运算需要说明的一点是,先取反后相加与先相加后取反,得到的结果是一样的!
4.数据统计:系统自带的Ping会对接受完的所有ICM报文后,对所有的发送和接收的ICMP报文进行统计,从而计算ICMP报文丢失率。
5.在linux中,有一些函数可以实现主机名和地址的转化,最常见的有gethostbyname()、gethostbyaddr()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()则是逆操作,是将IP地址转化为主机名。
struct hostent
        {
            char *h_name;       /*正式主机名*/
            char **h_aliases;   /*主机别名*/
            int h_addrtype;     /*主机IP地址类型 IPv4为AF_INET*/
            int h_length;       /*主机IP地址字节长度,对于IPv4是4字节,即32位*/
            char **h_addr_list; /*主机的IP地址列表*/
        }

7、Protocolent

struct protoent { 
        char *       p_name; //名称
        char *               p_aliases; //别名
        short *       p_proto; //编号
       }  

格 式: struct protoent * getprotobyname( const char *name );
  参 数: name   通讯协定名称
  传回值: 成功 - 一指向 struct protoent 的指针
失败 - NULL  
说明: 利用通讯协定的名称来得知该通讯协定的别名、编号等资料。
getprotobynumber():依照通讯协定的编号来获取该通讯协定的其他资料。
  格 式: struct protoent * getprotobynumber( int number );
  参 数: number  以 host 排列方式的通讯协定编号
  传回值: 成功 - 一指向 struct protoent 的指针
       失败 - NULL
  说明: 利用通讯协定的编号来得知该通讯协定的名称、别名等资料。
根据协议名字然后匹配“/etc/protocols”,匹配成功,返回struct protoent指针,失败返回空 。

最终,linux上实现的Ping功能,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <setjmp.h>
#include <error.h>
#include <errno.h>

#define  PACKET_SIZE    4096
#define  MAX_WAIT_TIME  5
#define  MAX_NO_PACKETS 3

char SendPacket[PACKET_SIZE];
char RecvPacket[PACKET_SIZE];
int  sockfd,datalen=56;
int  nsend=0,nreceived=0;
struct sockaddr_in dest_addr;
pid_t  pid;                      /*进程类型*/
struct sockaddr_in from_addr;
struct timeval timerecv;

void statistics(int signo);
unsigned short cal_chksum(unsigned short *addr,int len);
int pack(int pack_no);
void send_packet(void);
void recv_packet(void);
int unpack(char *buf,int len);
void tv_sub(struct timeval *out,struct timeval *in);

int main(int argc,char* argv[])
{
    if(argc<2)
    {
        printf("error_argc!\n");
        exit(1);
    }
    struct hostent *host;
    struct protoent *proto;
    unsigned long inaddr=01;
    int waittime=MAX_WAIT_TIME;
    int size=50*1024;
    if((proto=getprotobyname("icmp"))==NULL)
    {
        perror("getprotobyname!");
        exit(1);
    }
    /*生成能使用ICMP的原始套接字,只能在root才能生成*/
    if(-1 == (sockfd=socket(AF_INET,SOCK_RAW,proto->p_proto)))  /*用于新的网络协议实现的测试等*/
    {
        perror("socket!");
        exit(1);
    }

    /*回收root权限,设置当前用户权限*/
    setuid(getuid());
    /*扩大套接字接收缓冲区到50k,减少缓冲区溢出的可能性,若无意ping一个广播地址,则引来大量应答*/
    setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size));/*返回的是实际发送出去的字节和在socket缓冲区的字节*/
    bzero(&dest_addr,sizeof(dest_addr));
    dest_addr.sin_family=AF_INET;
    /*判断是主机名还是ip地址*/
    if(inaddr = inet_addr(argv[1])==INADDR_NONE) /*返回的是网络字节数地址,一个long数据格式*//*如果传入的字符串不是一个合法的Internet地址,如“a.b.c.d”地址中任一项超过255,那么inet_addr()返回INADDR_NONE。*/
    {
        if((host=gethostbyname(argv[1]))==NULL)/*是主机名,将域名或主机名转化为IP地址*/
        {
            perror("gethostbyname!");
            exit(1);
        }
        memcpy((char *)&dest_addr.sin_addr,host->h_addr,host->h_length);
    }
    else
    {
        dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
        //memcpy((char *)&dest_addr.sin_addr,argv[1],sizeof(argv[1]));
    }
    /*获取id进程,用于设置ICMP标志符*/
    pid=getpid();
    printf("PING %s(%s): %d bytes data in ICMP packets.\n",argv[1],
                          inet_ntoa(dest_addr.sin_addr),datalen);
    send_packet();
    recv_packet();
    statistics(SIGALRM);
    return 0;
}

/*统计收发计数*/
void statistics(int signo)
{
    printf("\n-------------PING statistics------------\n");
    printf("%d packets transmitted,%d receive,%%%d lost\n,",nsend,nreceived,                                      (nsend-nreceived)/nsend*100);
    close(sockfd);
    exit(1);
}

/*校验和算法*/
unsigned short cal_chksum(unsigned short *addr,int len)
{
    int nleft = len;
    int sum = 0;
    unsigned short *w = addr;
    unsigned short answer = 0;

    /*把ICMP报头二进制数据以2字节为单位累加*/
    while(nleft>1)
    {
        sum+=*w++;
        nleft-=2;
    }

    /*把ICMP报头为奇数时,把最后一个字节视为一个两字节的高位,低位补‘0’字节*/
    if(1 == nleft)
    {
        *(unsigned char *)(&answer) = *(unsigned char *)w;
        sum+=answer;
    }
    sum=(sum>>16)+(sum&0xFFFF);
    sum+=(sum>>16);
    answer=~sum;
    return answer;
}

/*设置帧头*/
int pack(int pack_no)
{
    int i,packsize;
    struct icmp *icmp;
    struct timeval *tval;
    icmp=(struct icmp*)(SendPacket);
    icmp->icmp_type=ICMP_ECHO;
    icmp->icmp_code=0;
    icmp->icmp_cksum=0;
    icmp->icmp_seq=pack_no;
    icmp->icmp_id=pid;
    packsize=8+datalen;
    tval=(struct timeval*)(icmp->icmp_data);
    gettimeofday(tval,NULL);   /*记录发送时间*/
    icmp->icmp_cksum=cal_chksum((unsigned short*)icmp,packsize);
    return packsize;
}

/*发送三个ICMP报文*/
void send_packet(void)
{
    int packetsize;
    while(nsend<MAX_NO_PACKETS)
    {
        nsend++;
        packetsize=pack(nsend);
        if(sendto(sockfd,SendPacket,packetsize,0,(struct sockaddr *)&dest_addr,sizeof(dest_addr))<0)
        {
            perror("sendto!");
            continue;
        }
        sleep(1);   /*每隔一秒后发送一个ICMP报文*/
    }   
}

/*接受所有的ICMP报文*/
void recv_packet(void)
{
    int n,fromlen;
    extern int errno;
    signal(SIGALRM,statistics); /*设置s定时器*/
    fromlen=sizeof(from_addr);
    while(nreceived<nsend)
    {
        alarm(MAX_WAIT_TIME); /*超过设定值,将触发SIGALRM信号*/
        if((n=recvfrom(sockfd,RecvPacket,sizeof(RecvPacket),0,(struct sockaddr *)&from_addr,&fromlen))<0)
        {
            if(errno==EINTR) continue; /*由于信号中断返回,没有任何数据可用*/
            perror("recvfrom!");
            continue;
        }
        gettimeofday(&timerecv,NULL);/*记录接收时间*/
        if(-1==unpack(RecvPacket,n))
        {
            continue;       
        }
            nreceived++;
    }
}

/*剥去ICMP报头*/
int unpack(char *buf,int len)
{
    int i,iphdrlen;
    struct ip *ip;
    struct icmp *icmp;
    struct timeval *timesend;
    double rtt;
    ip=(struct ip*)buf;
    iphdrlen=ip->ip_hl<<2;/*求ip报头长度,即ip报头的长度标志乘以2*/
    icmp=(struct icmp*)(buf+iphdrlen);
    len-=iphdrlen;
    if(len<8)
    {
        printf("ICMP packet\'s length is less than 8\n");
        return -1;
    }
    /*确保所接收到的是ICMP的回应*/
    if( (icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid))
    {
        timesend=(struct timeval*)(icmp->icmp_data);
        tv_sub(&timerecv,timesend);/*接收和发送时间差*/
        rtt=timerecv.tv_sec*1000+timerecv.tv_usec/1000;/*以ms为单位*/
        printf("%d byte from %s:icmp_seq=%u ttl=%d rtt=%.3f ms\n",
                 len,
                inet_ntoa(from_addr.sin_addr),
                icmp->icmp_seq,
                ip->ip_ttl,
                rtt);
    }
    else
    {
        return -1;
    }
}

/*两个timeval结构相减*/ 
void tv_sub(struct timeval *out,struct timeval *in)
{
    if((out->tv_usec-=in ->tv_usec)<0)
    {
        --out->tv_sec;
        out->tv_usec+=1000000;
    }
    out->tv_sec-=in->tv_sec;
}

实现结果:

 ./MyPing.exe www.baidu.com
PING www.baidu.com(183.232.231.173): 56 bytes data in ICMP packets.
64 byte from 183.232.231.173:icmp_seq=1 ttl=53 rtt=3002.000 ms
64 byte from 183.232.231.173:icmp_seq=2 ttl=53 rtt=2053.000 ms
64 byte from 183.232.231.173:icmp_seq=3 ttl=53 rtt=1053.000 ms

-------------PING statistics------------
3 packets transmitted,3 receive,%0 lost

特别提示:只有root用户才能利用socket()函数生成原始套接字。

———————————-END——————————————————————

猜你喜欢

转载自blog.csdn.net/Feng_8071/article/details/80892481