本文转载自:http://www.ibm.com/developerworks/cn/linux/network/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 usec64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=255 time=79 usec64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=255 time=78 usec64 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 lossround-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程序只使用以下数据:
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_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字节。
ICMP数据报
Ping命令中需要显示的信息,包括icmp_seq和ttl都已有实现的办法,但还缺rtt往返时间。为了实现这一功能,可利用ICMP数据报携带一个时间戳。使用以下函数生成时间戳:
#include int gettimeofday(struct timeval *tp,void *tzp)其中timeval结构如下: struct timeval{ long tv_sec; long tv_usec; }
数据统计
系统自带的ping命令当它接送完所有ICMP报文后,会对所有发送和所有接收的ICMP报文进行统计,从而计算ICMP报文丢失的比率。为达此目的,定义两个全局变量:接收计数器和发送计数器,用于记录ICMP报文接受和发送数目。丢失数目=发送总数-接收总数,丢失比率=丢失数目/发送总数。
现给出模拟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 3char 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/*设置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( nreceivedif( (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 ms64 byte from 202.95.2.148: icmp_seq=2 ttl=242 rtt=2020.000 ms64 byte from 202.95.2.148: icmp_seq=3 ttl=242 rtt=1010.000 ms--------------------PING statistics-------------------3 packets transmitted, 3 received , %0 lost
同时,另一种C语言的实现方法如下:
/*只有root用户才能利用socket()函数生成原始套接字,要让Linux的一般用户能执行以上程序,需进行如下的特别操作:用root登陆,编译以上程序:gcc -o myping myping.c,其目的有二:一是编译,二是让myping属于root用户。再执行chmod u+s myping,目的是把myping程序设成SUID的属性。退出root,用一般用户登陆,执行./myping www.cn.ibm.com,有以下执行结果:*/#include #include #include #include #include #include #include #include #include #include //各种缓冲区的长度 #define BUFSIZE 1500 //ICMP回显请求的长度 #define DATA_LEN 56 struct proto { struct sockaddr *sasend; /* sockaddr{} for send, from getaddrinfo */ struct sockaddr *sarecv; /* sockaddr{} for receiving */ socklen_t salen; /* length of sockaddr{}s */ int icmpproto; /* IPPROTO_xxx value for ICMP */ }; //全局变量 pid_t g_pid; int g_sockfd; struct proto g_proto = { NULL, NULL, 0, IPPROTO_ICMP }; //处理服务器返回的ICMP回显信息 void proc_msg(char *, ssize_t, struct msghdr *, struct timeval *); //发送ICMP回显请求 void send_msg(void); //循环发送、接收信息 void readloop(void); //定时器入门函数,每隔一秒一次发送ICMP请求 void sig_alrm(int); //计算两个时间之间的间隔 void tv_sub(struct timeval *, struct timeval *); //获取服务器的地址等信息 struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype); //根据服务器信息,得到服务器的IP地址 char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen); //计算校验和 uint16_t in_cksum(uint16_t *addr, int len); //输出错误信息,退出程序 void error_quit(const char *str); int main(int argc, char **argv) { int c; struct addrinfo *ai; struct sockaddr_in *sin; char *ip_address; char *host; //本程序只支持一种输入方式:./myping if( argc != 2 ) error_quit("usage: myping " ); host = argv[1]; //将pid的高二位全置为0,ICMP的ID只有16位 g_pid = getpid() & 0xffff; //设置定时器,每秒钟向服务器发送一次请求 signal(SIGALRM, sig_alrm); //获取服务器的信息(addrinfo结构) ai = host_serv(host, NULL, 0, 0); ip_address = sock_ntop_host(ai->ai_addr, ai->ai_addrlen); printf("PING %s (%s): %d data bytes\n", ai->ai_canonname ? ai->ai_canonname : ip_address, ip_address, DATA_LEN); //如果返回的协议簇不是AF_INET(IPv4),则退出 if ( ai->ai_family != AF_INET ) error_quit("unknown address family"); //设置proto结构体 g_proto.sasend = ai->ai_addr; g_proto.sarecv = calloc(1, ai->ai_addrlen); g_proto.salen = ai->ai_addrlen; //开始循环发送/接收请求 readloop(); return 0; } void readloop(void) { int size; char recvbuf[BUFSIZE]; char controlbuf[BUFSIZE]; struct msghdr msg; struct iovec iov; ssize_t n; struct timeval tval; //创建一个IPv4的原始套接字 g_sockfd = socket(g_proto.sasend->sa_family, SOCK_RAW, g_proto.icmpproto); if( -1 == g_sockfd ) error_quit("socket error"); //放弃管理员权限 //这个程序中,只用创建原始套接字时需要管理员权限 setuid(getuid()); //设置socket的接收缓冲区 size = 60 * 1024; setsockopt(g_sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); //发出第一个请求 sig_alrm(SIGALRM); //为recvmsg调用设置msghdr结构 iov.iov_base = recvbuf; iov.iov_len = sizeof(recvbuf); msg.msg_name = g_proto.sarecv; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = controlbuf; //开始死循环,不断读取和处理从服务器中返回的信息 while( 1 ) { msg.msg_namelen = g_proto.salen; msg.msg_controllen = sizeof(controlbuf); n = recvmsg(g_sockfd, &msg, 0); if (n < 0) { if (errno == EINTR) continue; else error_quit("recvmsg error"); } //分析返回内容,产生输出 gettimeofday(&tval, NULL); proc_msg(recvbuf, n, &msg, &tval); } } void proc_msg(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv) { int hlen1, icmplen; double rtt; struct ip *ip; struct icmp *icmp; struct timeval *tvsend; //将服务器返回的字符串强转为ip结构 ip = (struct ip *) ptr; //得到IP表头的长度 hlen1 = ip->ip_hl << 2; //如果不是ICMP的应答,则返回 if (ip->ip_p != IPPROTO_ICMP) return; icmp = (struct icmp *) (ptr + hlen1); //长度不足,不是合法应答 if ( (icmplen = len - hlen1) < 8) return; //不是回显应答,返回 if (icmp->icmp_type != ICMP_ECHOREPLY) return; //不是我们发出请求的应答,返回 if (icmp->icmp_id != g_pid) return; //长度不足,非法应答 if (icmplen < 16) return; //计算网络延时 tvsend = (struct timeval *) icmp->icmp_data; tv_sub(tvrecv, tvsend); rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0; //输出信息 printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n", icmplen, sock_ntop_host(g_proto.sarecv, g_proto.salen), icmp->icmp_seq, ip->ip_ttl, rtt); } void send_msg(void) { int len; int res; struct icmp *icmp; char sendbuf[BUFSIZE]; static int nsent = 0; //根据ICMPv4协议来设置发送信息 icmp = (struct icmp *) sendbuf; //ICMP回显请求 icmp->icmp_type = ICMP_ECHO; icmp->icmp_code = 0; //ICMP标识符字段为本进程的PID icmp->icmp_id = g_pid; //ICMP序列号字段为不断递增的全局变量nsent icmp->icmp_seq = nsent++; //ICMP数据字段为当前时间截,空白部分填充0xa5 memset(icmp->icmp_data, 0xa5, DATA_LEN); gettimeofday((struct timeval *)icmp->icmp_data, NULL); //计算并填充校验和 len = 8 + DATA_LEN; icmp->icmp_cksum = 0; icmp->icmp_cksum = in_cksum((u_short *) icmp, len); //发送数据 res = sendto(g_sockfd, sendbuf, len, 0, g_proto.sasend, g_proto.salen); if( -1 == res ) error_quit("sendto error"); } void sig_alrm(int signo) { send_msg(); alarm(1); } void tv_sub(struct timeval *out, struct timeval *in) { //将两个时间相减,并把结果存入第一个参数中( out -= in ) if ( (out->tv_usec -= in->tv_usec) < 0) { --out->tv_sec; out->tv_usec += 1000000; } out->tv_sec -= in->tv_sec; } struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype) { int n; struct addrinfo hints, *res; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_flags = AI_CANONNAME; hints.ai_family = family; hints.ai_socktype = socktype; n = getaddrinfo(host, serv, &hints, &res); if ( n != 0 ) error_quit("getaddrinfo error"); return res; } char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen) { static char str[128]; struct sockaddr_in *sin = (struct sockaddr_in *) sa; //本程序只支持IPv4协议 if( sa->sa_family != AF_INET ) error_quit("sock_ntop_host: the type must be AF_INET"); if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL) error_quit("inet_ntop error"); return str; } uint16_t in_cksum(uint16_t *addr, int len) { int nleft = len; uint32_t sum = 0; uint16_t *w = addr; uint16_t answer = 0; /* * Our algorithm is simple, using a 32 bit accumulator (sum), we add * sequential 16 bit words to it, and at the end, fold back all the * carry bits from the top 16 bits into the lower 16 bits. */ while (nleft > 1) { sum += *w++; nleft -= 2; } /* 4mop up an odd byte, if necessary */ if (nleft == 1) { *(unsigned char *)(&answer) = *(unsigned char *)w ; sum += answer; } /* 4add back carry outs from top 16 bits to low 16 bits */ sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */ sum += (sum >> 16); /* add carry */ answer = ~sum; /* truncate to 16 bits */ return(answer); } void error_quit(const char *str) { //输出错误信息,退出程序 fprintf(stderr, "%s", str); if( errno != 0 ) fprintf(stderr, " : %s", strerror(errno)); fprintf(stderr, "\n"); exit(1); }
再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow