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——————————————————————