大部分人用ping命令只是作为查看另一个系统的网络连接是否正常的一种简单方法。在这篇文章中,作者将介绍如何用C语言编写一个模拟ping命令功能的程序。
4 评论:
XML error: Please enter a value for the author element's jobtitle attribute, or the company-name element, or both. |
2001 年 10 月 01 日
ping命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。ping命令的工作原理是:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者,这有点象潜水艇声纳系统中使用的发声装置。
例如,在Linux终端上执行ping localhost命令将会看到以下结果:
PING localhost.localdomain (127.0.0.1) from 127.0.0.1 : 56(84) bytes of data. 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=0 ttl=255 time=112 usec 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=255 time=79 usec 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=255 time=78 usec 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=3 ttl=255 time=82 usec --- localhost.localdomain ping statistics --- 4 packets transmitted, 4 packets received, 0% packet loss round-trip min/avg/max/mdev = 0.078/0.087/0.112/0.018 ms
由上面的执行结果可以看到,ping命令执行后显示出被测试系统主机名和相应IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt(单位是毫秒,即千分之一秒)。要写一个模拟ping命令,这些信息有启示作用。
要真正了解ping命令实现原理,就要了解ping命令所使用到的TCP/IP协议。
ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。如下图所示
IP报头 |
---|
ICMP报头 |
ICMP数据报 |
IP报头格式
由于IP层协议是一种点对点的协议,而非端对端的协议,它提供无连接的数据报服务,没有端口的概念,因此很少使用bind()和connect()函数,若有使用也只是用于设置IP地址。发送数据使用sendto()函数,接收数据使用recvfrom()函数。IP报头格式如下图:
在Linux中,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 */ };
其中ping程序只使用以下数据:
- IP报头长度IHL(Internet Header Length)�D�D以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。
- 生存时间TTL(Time To Live)�D�D以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。
ICMP报头格式
ICMP报文分为两种,一是错误报告报文,二是查询报文。每个ICMP报头均包含类型、编码和校验和这三项内容,长度为8位,8位和16位,其余选项则随ICMP的功能不同而不同。
Ping命令只使用众多ICMP报文中的两种:"请求回送'(ICMP_ECHO)和"请求回应'(ICMP_ECHOREPLY)。在Linux中定义如下:
#define ICMP_ECHO 0 #define ICMP_ECHOREPLY 8
这两种ICMP类型报头格式如下:
在Linux中ICMP数据结构(<netinet/ip_icmp.h>)定义如下:
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报头为8字节,数据报长度最大为64K字节。
- 校验和算法�D�D这一算法称为网际校验和算法,把被校验的数据16位进行累加,然后取反码,若数据字节长度为奇数,则数据尾部补一个字节的0以凑成偶数。此算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和,更详细的信息请参考RFC1071,校验和字段为上述ICMP数据结构的icmp_cksum变量。
- 标识符�D�D用于唯一标识ICMP报文, 为上述ICMP数据结构的icmp_id宏所指的变量。
- 顺序号�D�Dping命令的icmp_seq便由这里读出,代表ICMP报文的发送顺序,为上述ICMP数据结构的icmp_seq宏所指的变量。
ICMP数据报
Ping命令中需要显示的信息,包括icmp_seq和ttl都已有实现的办法,但还缺rtt往返时间。为了实现这一功能,可利用ICMP数据报携带一个时间戳。使用以下函数生成时间戳:
#include int gettimeofday(struct timeval *tp,void *tzp) 其中timeval结构如下: struct timeval{ long tv_sec; long tv_usec; }
其中tv_sec为秒数,tv_usec微秒数。在发送和接收报文时由gettimeofday分别生成两个timeval结构,两者之差即为往返时间,即ICMP报文发送与接收的时间差,而timeval结构由ICMP数据报携带,tzp指针表示时区,一般都不使用,赋NULL值。
数据统计
系统自带的ping命令当它接送完所有ICMP报文后,会对所有发送和所有接收的ICMP报文进行统计,从而计算ICMP报文丢失的比率。为达此目的,定义两个全局变量:接收计数器和发送计数器,用于记录ICMP报文接受和发送数目。丢失数目=发送总数-接收总数,丢失比率=丢失数目/发送总数。
现给出模拟Ping程序功能的代码如下:
/*********************************************************** * 作者:梁俊辉 * * 时间:2001年10月 * * 名称:myping.c * * 说明:本程序用于演示ping命令的实现原理 * ***********************************************************/ #include <stdio.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 <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; struct timeval tvrecv; 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); void statistics(int signo) { printf("\n--------------------PING statistics-------------------\n"); printf("%d packets transmitted, %d received , %%%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报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加*/ if( nleft==1) { *(unsigned char *)(&answer)=*(unsigned char *)w; sum+=answer; } sum=(sum>>16)+(sum&0xffff); sum+=(sum>>16); answer=~sum; return answer; } /*设置ICMP报头*/ 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() { int packetsize; while( nsend<MAX_NO_PACKETS) { nsend++; packetsize=pack(nsend); /*设置ICMP报头*/ if( sendto(sockfd,sendpacket,packetsize,0, (struct sockaddr *)&dest_addr,sizeof(dest_addr) )<0 ) { perror("sendto error"); continue; } sleep(1); /*每隔一秒发送一个ICMP报文*/ } } /*接收所有ICMP报文*/ void recv_packet() { int n,fromlen; extern int errno; signal(SIGALRM,statistics); fromlen=sizeof(from); while( nreceived<nsend) { alarm(MAX_WAIT_TIME); if( (n=recvfrom(sockfd,recvpacket,sizeof(recvpacket),0, (struct sockaddr *)&from,&fromlen)) <0) { if(errno==EINTR)continue; perror("recvfrom error"); continue; } gettimeofday(&tvrecv,NULL); /*记录接收时间*/ if(unpack(recvpacket,n)==-1)continue; nreceived++; } } /*剥去ICMP报头*/ int unpack(char *buf,int len) { int i,iphdrlen; struct ip *ip; struct icmp *icmp; struct timeval *tvsend; double rtt; ip=(struct ip *)buf; iphdrlen=ip->ip_hl<<2; /*求ip报头长度,即ip报头的长度标志乘4*/ icmp=(struct icmp *)(buf+iphdrlen); /*越过ip报头,指向ICMP报头*/ len-=iphdrlen; /*ICMP报头及ICMP数据报的总长度*/ if( len<8) /*小于ICMP报头长度则不合理*/ { printf("ICMP packets\'s length is less than 8\n"); return -1; } /*确保所接收的是我所发的的ICMP的回应*/ if( (icmp->icmp_type==ICMP_ECHOREPLY) && (icmp->icmp_id==pid) ) { tvsend=(struct timeval *)icmp->icmp_data; tv_sub(&tvrecv,tvsend); /*接收和发送的时间差*/ rtt=tvrecv.tv_sec*1000+tvrecv.tv_usec/1000; /*以毫秒为单位计算rtt*/ /*显示相关信息*/ printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n", len, inet_ntoa(from.sin_addr), icmp->icmp_seq, ip->ip_ttl, rtt); } else return -1; } main(int argc,char *argv[]) { struct hostent *host; struct protoent *protocol; unsigned long inaddr=0l; int waittime=MAX_WAIT_TIME; int size=50*1024; if(argc<2) { printf("usage:%s hostname/IP address\n",argv[0]); exit(1); } if( (protocol=getprotobyname("icmp") )==NULL) { perror("getprotobyname"); exit(1); } /*生成使用ICMP的原始套接字,这种套接字只有root才能生成*/ if( (sockfd=socket(AF_INET,SOCK_RAW,protocol->p_proto) )<0) { perror("socket error"); exit(1); } /* 回收root权限,设置当前用户权限*/ setuid(getuid()); /*扩大套接字接收缓冲区到50K这样做主要为了减小接收缓冲区溢出的 的可能性,若无意中ping一个广播地址或多播地址,将会引来大量应答*/ setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size) ); bzero(&dest_addr,sizeof(dest_addr)); dest_addr.sin_family=AF_INET; /*判断是主机名还是ip地址*/ if( inaddr=inet_addr(argv[1])==INADDR_NONE) { if((host=gethostbyname(argv[1]) )==NULL) /*是主机名*/ { perror("gethostbyname error"); exit(1); } memcpy( (char *)&dest_addr.sin_addr,host->h_addr,host->h_length); } else /*是ip地址*/ memcpy( (char *)&dest_addr,(char *)&inaddr,host->h_length); /*获取main的进程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(); /*发送所有ICMP报文*/ recv_packet(); /*接收所有ICMP报文*/ statistics(SIGALRM); /*进行统计*/ return 0; } /*两个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; } /*------------- The End -----------*/
特别注意
只有root用户才能利用socket()函数生成原始套接字,要让Linux的一般用户能执行以上程序,需进行如下的特别操作:
用root登陆,编译以上程序:gcc -o myping myping.c,其目的有二:一是编译,二是让myping属于root用户。
再执行chmod u+s myping,目的是把myping程序设成SUID的属性。
退出root,用一般用户登陆,执行./myping www.cn.ibm.com,有以下执行结果:
PING www.cn.ibm.com(202.95.2.148): 56 bytes data in ICMP packets. 64 byte from 202.95.2.148: icmp_seq=1 ttl=242 rtt=3029.000 ms 64 byte from 202.95.2.148: icmp_seq=2 ttl=242 rtt=2020.000 ms 64 byte from 202.95.2.148: icmp_seq=3 ttl=242 rtt=1010.000 ms --------------------PING statistics------------------- 3 packets transmitted, 3 received , %0 lost
由于myping.c是发送完所有的ICMP报文才去接收,因此第一、第二和第三个ICMP报文的往返时间依此是3秒,2秒,1秒,上述结果中rtt信息正反映这一事实。
运用C语言编写模拟常用网络命令ping命令实现一个基于linux原始套接字和ICMP协议的ping程序。该程序能用于检测主机或路由器工作是否正常。
程序中主要的函数
void alarm_handler(int); /*SIGALRM处理程序*/
void int_handler(int); /*SIGINT处理程序*/
void set_sighandler(); /*设置信号处理程序*/
void send_ping(); /*发送ping消息*/
void recv_reply(); /*接收ping应答*/
u16 checksum(u8 *buf, int len); /*计算校验和*/
int handle_pkt(); /*ICMP应答消息处理*/
void get_statistics(int, int); /*统计ping命令的检测结果*/
void bail(const char *); /*错误报告*/
头文件:ping.h
#define ICMP_ECHOREPLY 0 /* Echo应答*/
#define ICMP_ECHO /*Echo请求*/
#define BUFSIZE 1500 /*发送缓存最大值*/
#define DEFAULT_LEN 56 /**ping消息数据默认大小/
/*数据类型别名*/
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
/*ICMP消息头部*/
struct icmphdr {
u8 type; /*定义消息类型*/
u8 code; /*定义消息代码*/
u16 checksum; /*定义校验*/
union{
struct{
u16 id;
u16 sequence;
}echo;
u32 gateway;
struct{
u16 unsed;
u16 mtu;
}frag; /*pmtu实现*/
}un;
/*ICMP数据占位符*/
u8 data[0];
#define icmp_id un.echo.id
#define icmp_seq un.echo.sequence
};
#define ICMP_HSIZE sizeof(struct icmphdr)
/*定义一个IP消息头部结构体*/
struct iphdr {
u8 hlen:4, ver:4; /*定义4位首部长度,和IP版本号为IPV4*/
u8 tos; /*8位服务类型TOS*/
u16 tot_len; /*16位总长度*/
u16 id; /*16位标志位*/
u16 frag_off; /*3位标志位*/
u8 ttl; /*8位生存周期*/
u8 protocol; /*8位协议*/
u16 check; /*16位IP首部校验和*/
u32 saddr; /*32位源IP地址*/
u32 daddr; /*32位目的IP地址*/
};
char *hostname; /*被ping的主机名*/
int datalen = DEFAULT_LEN; /*ICMP消息携带的数据长度*/
char sendbuf[BUFSIZE]; /*发送字符串数组*/
char recvbuf[BUFSIZE]; /*接收字符串数组*/
int nsent; /*发送的ICMP消息序号*/
int nrecv; /*接收的ICMP消息序号*/
pid_t pid; /*ping程序的进程PID*/
struct timeval recvtime; /*收到ICMP应答的时间戳*/
int sockfd; /*发送和接收原始套接字*/
struct sockaddr_in dest; /*被ping的主机IP*/
struct sockaddr_in from; /*发送ping应答消息的主机IP*/
struct sigaction act_alarm;
struct sigaction act_int;
/*函数原型*/
void alarm_handler(int); /*SIGALRM处理程序*/
void int_handler(int); /*SIGINT处理程序*/
void set_sighandler(); /*设置信号处理程序*/
void send_ping(); /*发送ping消息*/
void recv_reply(); /*接收ping应答*/
u16 checksum(u8 *buf, int len); /*计算校验和*/
int handle_pkt(); /*ICMP应答消息处理*/
void get_statistics(int, int); /*统计ping命令的检测结果*/
void bail(const char *); /*错误报告*/
源程序2:ping.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h> /*是Linux系统的日期时间头文件*/
#include<unistd.h> /* 是POSIX标准定义的unix类系统定义符号常量的头文件,包含了许多UNIX系统服务的函数原型,例如read函数、write函数和getpid函数*/
#include<string.h>
#include<sys/socket.h> /*对与引用socket函数必须*/
#include<sys/types.h>
#include<netdb.h> /*定义了与网络有关的结构,变量类型,宏,函数。函数gethostbyname()用*/
#include<errno.h> /*sys/types.h中文名称为基本系统数据类型*/
#include<arpa/inet.h> /*inet_ntoa()和inet_addr()这两个函数,包含在 arpa/inet.h*/
#include<signal.h> /*进程对信号进行处理*/
#include<netinet/in.h> /*互联网地址族*/
#include"ping.h"
#define IP_HSIZE sizeof(struct iphdr) /*定义IP_HSIZE为ip头部长度*/
#define IPVERSION 4 /*定义IPVERSION为4,指出用ipv4*/
/*设置的时间是一个结构体,倒计时设置,重复倒时,超时值设为1秒*/
struct itimerval val_alarm={.it_interval.tv_sec = 1,
.it_interval.tv_usec=0,
.it_value.tv_sec=0,
.it_value.tv_usec=1
};
int main(int argc,char **argv) /*argc表示隐形程序命令行中参数的数目,argv是一个指向字符串数组指针,其中每一个字符对应一个参数*/
{
struct hostent *host; /*该结构体属于include<netdb.h>*/
int on =1;
if(argc<2){ /*判断是否输入了地址*/
printf("Usage: %s hostname\n",argv[0]);
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL){ /*gethostbyname()返回对应于给定主机名的包含主机名字和地址信息的结构指针,*/
perror("can not understand the host name"); /*理解不了输入的地址*/
exit(1);
}
hostname=argv[1];/*取出地址名*/
memset(&dest,0,sizeof dest); /*将dest中前sizeof(dest)个字节替换为0并返回s,此处为初始化,给最大内存清零*/
dest.sin_family=PF_INET; /*PF_INET为IPV4,internet协议,在<netinet/in.h>中,地址族*/
dest.sin_port=ntohs(0); /*端口号,ntohs()返回一个以主机字节顺序表达的数。*/
dest.sin_addr=*(struct in_addr *)host->h_addr_list[0];/*host->h_addr_list[0]是地址的指针.返回IP地址,初始化*/
if((sockfd = socket(PF_INET,SOCK_RAW,IPPROTO_ICMP))<0){ /*PF_INEI套接字协议族,SOCK_RAW套接字类型,IPPROTO_ICMP使用协议,调用socket函数来创建一个能够进行网络通信的套接字。这里判断是否创建成功*/
perror("raw socket created error");
exit(1);
}
setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on)); /*设置当前套接字选项特定属性值,sockfd套接字,IPPROTO_IP协议层为IP层,IP_HDRINCL套接字选项条目,套接字接收缓冲区指针,sizeof(on)缓冲区长度的长度*/
setuid(getuid());/*getuid()函数返回一个调用程序的真实用户ID,setuid()是让普通用户可以以root用户的角色运行只有root帐号才能运行的程序或命令。*/
pid=getpid(); /*getpid函数用来取得目前进程的进程识别码*/
set_sighandler();/*对信号处理*/
printf("Ping %s(%s): %d bytes data in ICMP packets.\n",
argv[1],inet_ntoa(dest.sin_addr),datalen);
if((setitimer(ITIMER_REAL,&val_alarm,NULL))==-1) /*定时函数*/
bail("setitimer fails.");
recv_reply();/*接收ping应答*/
return 0;
}
/*发送ping消息*/
void send_ping(void)
{
struct iphdr *ip_hdr; /*iphdr为IP头部结构体*/
struct icmphdr *icmp_hdr; /*icmphdr为ICMP头部结构体*/
int len;
int len1;
/*ip头部结构体变量初始化*/
ip_hdr=(struct iphdr *)sendbuf; /*字符串指针*/
ip_hdr->hlen=sizeof(struct iphdr)>>2; /*头部长度*/
ip_hdr->ver=IPVERSION; /*版本*/
ip_hdr->tos=0; /*服务类型*/
ip_hdr->tot_len=IP_HSIZE+ICMP_HSIZE+datalen; /*报文头部加数据的总长度*/
ip_hdr->id=0; /*初始化报文标识*/
ip_hdr->frag_off=0; /*设置flag标记为0*/
ip_hdr->protocol=IPPROTO_ICMP;/*运用的协议为ICMP协议*/
ip_hdr->ttl=255; /*一个封包在网络上可以存活的时间*/
ip_hdr->daddr=dest.sin_addr.s_addr; /*目的地址*/
len1=ip_hdr->hlen<<2; /*ip数据长度*/
/*ICMP头部结构体变量初始化*/
icmp_hdr=(struct icmphdr *)(sendbuf+len1); /*字符串指针*/
icmp_hdr->type=8; /*初始化ICMP消息类型type*/
icmp_hdr->code=0; /*初始化消息代码code*/
icmp_hdr->icmp_id=pid; /*把进程标识码初始给icmp_id*/
icmp_hdr->icmp_seq=nsent++; /*发送的ICMP消息序号赋值给icmp序号*/
memset(icmp_hdr->data,0xff,datalen); /*将datalen中前datalen个字节替换为0xff并返回icmp_hdr-dat*/
gettimeofday((struct timeval *)icmp_hdr->data,NULL); /* 获取当前时间*/
len=ip_hdr->tot_len; /*报文总长度赋值给len变量*/
icmp_hdr->checksum=0; /*初始化*/
icmp_hdr->checksum=checksum((u8 *)icmp_hdr,len); /*计算校验和*/
sendto(sockfd,sendbuf,len,0,(struct sockaddr *)&dest,sizeof (dest)); /*经socket传送数据*/
}
/*接收程序发出的ping命令的应答*/
void recv_reply()
{
int n,len;
int errno;
n=nrecv=0;
len=sizeof(from); /*发送ping应答消息的主机IP*/
while(nrecv<4){
if((n=recvfrom(sockfd,recvbuf,sizeof recvbuf,0,(struct sockaddr *)&from,&len))<0){ /*经socket接收数据,如果正确接收返回接收到的字节数,失败返回0.*/
if(errno==EINTR) /*EINTR表示信号中断*/
continue;
bail("recvfrom error");
}
gettimeofday(&recvtime,NULL); /*记录收到应答的时间*/
if(handle_pkt()) /*接收到错误的ICMP应答信息*/
continue;
nrecv++;
}
get_statistics(nsent,nrecv); /*统计ping命令的检测结果*/
}
/*计算校验和*/
u16 checksum(u8 *buf,int len)
{
u32 sum=0;
u16 *cbuf;
cbuf=(u16 *)buf;
while(len>1){
sum+=*cbuf++;
len-=2;
}
if(len)
sum+=*(u8 *)cbuf;
sum=(sum>>16)+(sum & 0xffff);
sum+=(sum>>16);
return ~sum;
}
/*ICMP应答消息处理*/
int handle_pkt()
{
struct iphdr *ip;
struct icmphdr *icmp;
int ip_hlen;
u16 ip_datalen; /*ip数据长度*/
double rtt; /* 往返时间*/
struct timeval *sendtime;
ip=(struct iphdr *)recvbuf;
ip_hlen=ip->hlen << 2;
ip_datalen=ntohs(ip->tot_len)-ip_hlen;
icmp=(struct icmphdr *)(recvbuf+ip_hlen);
if(checksum((u8 *)icmp,ip_datalen)) /*计算校验和*/
return -1;
if(icmp->icmp_id!=pid)
return -1;
sendtime=(struct timeval *)icmp->data; /*发送时间*/
rtt=((&recvtime)->tv_sec-sendtime->tv_sec)*1000+((&recvtime)->tv_usec-sendtime->tv_usec)/1000.0;/* 往返时间*/
/*打印结果*/
printf("%d bytes from %s:icmp_seq=%u ttl=%d rtt=%.3f ms\n",
ip_datalen, /*IP数据长度*/
inet_ntoa(from.sin_addr), /*目的ip地址*/
icmp->icmp_seq, /*icmp报文序列号*/
ip->ttl, /*生存时间*/
rtt); /*往返时间*/
return 0;
}
/*设置信号处理程序*/
void set_sighandler()
{
act_alarm.sa_handler=alarm_handler;
if(sigaction(SIGALRM,&act_alarm,NULL)==-1) /*sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum指所要捕获信号或忽略的信号,&act代表新设置的信号共用体,NULL代表之前设置的信号处理结构体。这里判断对信号的处理是否成功。*/
bail("SIGALRM handler setting fails.");
act_int.sa_handler=int_handler;
if(sigaction(SIGINT,&act_int,NULL)==-1)
bail("SIGALRM handler setting fails.");
}
/*统计ping命令的检测结果*/
void get_statistics(int nsent,int nrecv)
{
printf("--- %s ping statistics ---\n",inet_ntoa(dest.sin_addr)); /*将网络地址转换成“.”点隔的字符串格式。*/
printf("%d packets transmitted, %d received, %0.0f%% ""packet loss\n",
nsent,nrecv,1.0*(nsent-nrecv)/nsent*100);
}
/*错误报告*/
void bail(const char * on_what)
{
fputs(strerror(errno),stderr); /*:向指定的文件写入一个字符串(不写入字符串结束标记符‘\0’)。成功写入一个字符串后,文件的位置指针会自动后移,函数返回值为0;否则返回EOR(符号常量,其值为-1)。*/
fputs(":",stderr);
fputs(on_what,stderr);
fputc('\n',stderr); /*送一个字符到一个流中*/
exit(1);
}
/*SIGINT(中断信号)处理程序*/
void int_handler(int sig)
{
get_statistics(nsent,nrecv); /*统计ping命令的检测结果*/
close(sockfd); /*关闭网络套接字*/
exit(1);
}
/*SIGALRM(终止进程)处理程序*/
void alarm_handler(int signo)
{
send_ping(); /*发送ping消息*/
}
程序执行:
1、程序编译。在linux终端下执行如下命令:
gcc ping.h -o myping -std=gnu99
因为原始套接字的创建需要root用户权限,所以为了能让所有的其他用户也可以使用该程序,需要通过如下命令设置myping的set-user-id位:
$sudo chmod u+s myping
结果为:
/********************************************************
* IP报头格式数据结构定义在<netinet/ip.h>中 *
* ICMP数据结构定义在<netinet/ip_icmp.h>中 *
* 套接字地址数据结构定义在<netinet/in.h>中 *
********************************************************/
#include <stdio.h>
#include <stdlib.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 <errno.h>
#define PACKET_SIZE 4096
#define MAX_WAIT_TIME 5
#define MAX_NO_PACKETS 10000
char *addr[];
char sendpacket[PACKET_SIZE];
char recvpacket[PACKET_SIZE];
int sockfd,datalen = 56;
int nsend = 0, nreceived = 0;
double temp_rtt[MAX_NO_PACKETS];
double all_time = 0;
double min = 0;
double max = 0;
double avg = 0;
double mdev = 0;
struct sockaddr_in dest_addr;
struct sockaddr_in from;
struct timeval tvrecv;
pid_t pid;
void statistics(int sig);
void send_packet(void);
void recv_packet(void);
void computer_rtt(void);
void tv_sub(struct timeval *out,struct timeval *in);
int pack(int pack_no);
int unpack(char *buf,int len);
unsigned short cal_checksum(unsigned short *addr,int len);
/*计算rtt最小、大值,平均值,算术平均数差*/
void computer_rtt()
{
double sum_avg = 0;
int i;
min = max = temp_rtt[0];
avg = all_time/nreceived;
for(i=0; i<nreceived; i++){
if(temp_rtt[i] < min)
min = temp_rtt[i];
else if(temp_rtt[i] > max)
max = temp_rtt[i];
if((temp_rtt[i]-avg) < 0)
sum_avg += avg - temp_rtt[i];
else
sum_avg += temp_rtt[i] - avg;
}
mdev = sum_avg/nreceived;
}
/****统计数据函数****/
void statistics(int sig)
{
computer_rtt(); //计算rtt
printf("\n------ %s ping statistics ------\n",addr[0]);
printf("%d packets transmitted,%d received,%d%% packet loss,time %.f ms\n",
nsend,nreceived,(nsend-nreceived)/nsend*100,all_time);
printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/%.3f ms\n",
min,avg,max,mdev);
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 check_sum = 0;
while(nleft>1) //ICMP包头以字(2字节)为单位累加
{
sum += *w++;
nleft -= 2;
}
if(nleft == 1) //ICMP为奇数字节时,转换最后一个字节,继续累加
{
*(unsigned char *)(&check_sum) = *(unsigned char *)w;
sum += check_sum;
}
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
check_sum = ~sum; //取反得到校验和
return check_sum;
}
/*设置ICMP报头*/
int pack(int pack_no)
{
int i,packsize;
struct icmp *icmp;
struct timeval *tval;
icmp = (struct icmp*)sendpacket;
icmp->icmp_type = ICMP_ECHO; //ICMP_ECHO类型的类型号为0
icmp->icmp_code = 0;
icmp->icmp_cksum = 0;
icmp->icmp_seq = pack_no; //发送的数据报编号
icmp->icmp_id = pid;
packsize = 8 + datalen; //数据报大小为64字节
tval = (struct timeval *)icmp->icmp_data;
gettimeofday(tval,NULL); //记录发送时间
//校验算法
icmp->icmp_cksum = cal_chksum((unsigned short *)icmp,packsize);
return packsize;
}
/****发送三个ICMP报文****/
void send_packet()
{
int packetsize;
if(nsend < MAX_NO_PACKETS)
{
nsend++;
packetsize = pack(nsend); //设置ICMP报头
//发送数据报
if(sendto(sockfd,sendpacket,packetsize,0,
(struct sockaddr *)&dest_addr,sizeof(dest_addr)) < 0)
{
perror("sendto error");
}
}
}
/****接受所有ICMP报文****/
void recv_packet()
{
int n,fromlen;
extern int error;
fromlen = sizeof(from);
if(nreceived < nsend)
{
//接收数据报
if((n = recvfrom(sockfd,recvpacket,sizeof(recvpacket),0,
(struct sockaddr *)&from,&fromlen)) < 0)
{
perror("recvfrom error");
}
gettimeofday(&tvrecv,NULL); //记录接收时间
unpack(recvpacket,n); //剥去ICMP报头
nreceived++;
}
}
/******剥去ICMP报头******/
int unpack(char *buf,int len)
{
int i;
int iphdrlen; //ip头长度
struct ip *ip;
struct icmp *icmp;
struct timeval *tvsend;
double rtt;
ip = (struct ip *)buf;
iphdrlen = ip->ip_hl << 2; //求IP报文头长度,即IP报头长度乘4
icmp = (struct icmp *)(buf + iphdrlen); //越过IP头,指向ICMP报头
len -= iphdrlen; //ICMP报头及数据报的总长度
if(len < 8) //小于ICMP报头的长度则不合理
{
printf("ICMP packet\'s length is less than 8\n");
return -1;
}
//确保所接收的是所发的ICMP的回应
if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid))
{
tvsend = (struct timeval *)icmp->icmp_data;
tv_sub(&tvrecv,tvsend); //接收和发送的时间差
//以毫秒为单位计算rtt
rtt = tvrecv.tv_sec*1000 + tvrecv.tv_usec/1000;
temp_rtt[nreceived] = rtt;
all_time += rtt; //总时间
//显示相关的信息
printf("%d bytes from %s: icmp_seq=%u ttl=%d time=%.1f ms\n",
len,inet_ntoa(from.sin_addr),
icmp->icmp_seq,ip->ip_ttl,rtt);
}
else return -1;
}
//两个timeval相减
void tv_sub(struct timeval *recvtime,struct timeval *sendtime)
{
long sec = recvtime->tv_sec - sendtime->tv_sec;
long usec = recvtime->tv_usec - sendtime->tv_usec;
if(usec >= 0){
recvtime->tv_sec = sec;
recvtime->tv_usec = usec;
}else{
recvtime->tv_sec = sec - 1;
recvtime->tv_usec = -usec;
}
}
/*主函数*/
main(int argc,char *argv[])
{
struct hostent *host;
struct protoent *protocol;
unsigned long inaddr = 0;
// int waittime = MAX_WAIT_TIME;
int size = 50 * 1024;
addr[0] = argv[1];
//参数小于两个
if(argc < 2)
{
printf("usage:%s hostname/IP address\n",argv[0]);
exit(1);
}
//不是ICMP协议
if((protocol = getprotobyname("icmp")) == NULL)
{
perror("getprotobyname");
exit(1);
}
//生成使用ICMP的原始套接字,只有root才能生成
if((sockfd = socket(AF_INET,SOCK_RAW,protocol->p_proto)) < 0)
{
perror("socket error");
exit(1);
}
//回收root权限,设置当前权限
setuid(getuid());
/*扩大套接字的接收缓存区导50K,这样做是为了减小接收缓存区溢出的
可能性,若无意中ping一个广播地址或多播地址,将会引来大量的应答*/
setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size));
bzero(&dest_addr,sizeof(dest_addr)); //初始化
dest_addr.sin_family = AF_INET; //套接字域是AF_INET(网络套接字)
//判断主机名是否是IP地址
if(inet_addr(argv[1]) == INADDR_NONE)
{
if((host = gethostbyname(argv[1])) == NULL) //是主机名
{
perror("gethostbyname error");
exit(1);
}
memcpy((char *)&dest_addr.sin_addr,host->h_addr,host->h_length);
}
else{ //是IP 地址
dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
}
pid = getpid();
printf("PING %s(%s):%d bytes of data.\n",argv[1],
inet_ntoa(dest_addr.sin_addr),datalen);
//当按下ctrl+c时发出中断信号,并开始执行统计函数
signal(SIGINT,statistics);
while(nsend < MAX_NO_PACKETS){
sleep(1); //每隔一秒发送一个ICMP报文
send_packet(); //发送ICMP报文
recv_packet(); //接收ICMP报文
}
return 0;
}
erro:getprotobyname: No such file or directory,protocol=getprotobyname("icmp") 返回的一直是空值