大部分人用ping命令只是作为查看另一个系统的网络连接是否正常的一种简单方法。在这篇文章中,作者将介绍如何用c语言编写一个模拟ping命令功能的程序。
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报头格式数据结构()定义如下:
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数据结构()定义如下:
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_seqicmp_hun.ih_idseq.icd_seq
#define icmp_void icmp_hun.ih_void
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
#define icmp_nextmtuicmp_hun.ih_pmtu.ipm_nextmtu
#define icmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs
#define icmp_wpaicmp_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_tid_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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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(
--------------------ping statistics-------------------
);
printf(%d packets transmitted, %d received , %%%d lost
,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
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
if( (n=recvfrom(sockfd,recvpacket,sizeof(recvpacket),0,
(struct sockaddr *)&from,&fromlen)) ip_hl<<2;/*求ip报头长度,即ip报头的长度标志乘4*/
icmp=(struct icmp *)(buf+iphdrlen); /*越过ip报头,指向icmp报头*/
len-=iphdrlen;/*icmp报头及icmp数据报的总长度*/
if( lenicmp_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
,
len,
inet_ntoa(from.sin_addr),
icmp->icmp_seq,
ip->ip_ttl,
rtt);
}
elsereturn -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(argcp_proto) )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.
,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)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信息正反映这一事实。
IBM通过AI和开源ML项目吸引开发人员
台积电董事会核准2000亿元新台币扩充产能与发展先进制程
传感器仪表元器件发展分析
槽开光电开关的基本原理
网友:库克的套路!不让中国抢生意!印度要拉上苹果建设高铁
Ping程序的C语言编程
电源管理ic是什么意思
基于GlobalEye的机载预警和控制解决方案
电源芯片启动过冲有哪些影响?怎么测试电源芯片的启动过冲?
2018年车载集成电路市场规模有望达到323亿美元,同比增长18.5%
iPhone 8续航时间更长将成为今年另一卖点
太阳能电池板汇流模块 应用于智能光伏汇流箱
近红外、远红外和微光技术的差异
备份谷歌或其他浏览器插件
2022年嵌入式系统创新解决方案
自动小型气象站厂家技术参数有哪些?
华为麦芒6今日北京发布会,中端机的价格高端机的颜值
快讯:iPhone15预计有15项变化 成龙买华为手机被网友偶遇
汽车制造产业链“寒冬”将席卷全国
【启扬方案】科技点亮课堂,智能黑板解决方案