今天利用C++实现一个自己的ping命令,首先我们在Linux 下运行下ping命令,看它完成了那些功能:
1.向目标IP发送一个64字节的ICMP请求包,然后收到目标IP的一个ICMP应答包,并逐个打印发送请求包到接收应答包的时间
2.最后打印出一共发送多少个ICMP包,是否有丢包,收发的总时间
下面就逐步实现我的一个ping命令,有不足的地方欢迎留言!
一、明确几个概念:
1.1ICMP报文作用?
ICMP是(Internet Control Message Protocol)Internet控制报文 协议。它是 TCP/IP协议族 的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指 网络通 不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
1.2ICMP的报文格式是如何?
ICMP报文包含在 IP 数据报中,属于 IP 的数据, IP 头部就在 ICMP 报文的前面, 一个ICMP报文包括IP 头部( 20 字节)、 ICMP 头部( 8 字节)和 ICMP 报文(即下图的选项数据)。
IP头部的Protocol值为1就说明这是一个ICMP报文,下面逐个介绍ICMP报文组成:
各种ICMP报文的前4个字节(32bits)都是三个长度固定的字段(见上图);
type 类型字段 (8 位 ) 、 code 代码字段 (8 位 ) 、 checksum 校验和字段 (16 位 );
8bits类型和8bits代码字段:一起决定了ICMP报文的类型。常见的有:
类型 8 、代码 0 :回显请求 ;类型 0 、代码 0 :回显应答;类型 11 、代码 0 :超时;
16bits校验和字段:包括数据在内的整个ICMP数据包的校验和,其计算方法和IP头部校验和的计算方法是一样的;
对于ICMP回显请求和应答报文来说(见上图),接下来是16bits标识符字段:一般用于标识本ICMP进程。最后是16bits序列号字段:用于判断回显应答数据报顺序。
在Linux中ICMP数据结构 (<netinet/ip_icmp.h>)定义如下:
<span style= "color:#555555;" > struct icmp { u_int8_t icmp_type; u_int8_t icmp_code; u_int16_t icmp_cksum; union { u_char ih_pptr; struct in_addr ih_gwaddr; struct ih_idseq { u_int16_t icd_id; u_int16_t icd_seq; } ih_idseq; u_int32_t ih_void; 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; } 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 }; </span>
ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。因此我们还需知道IP报文的格式。
在Linux中,IP报头格式数据结构 (<netinet/ip.h>)定义如下:
struct ip { #if __BYTE_ORDER == __LITTLE_ENDIAN unsigned int ip_hl:4; unsigned int ip_v:4; #endif #if __BYTE_ORDER == __BIG_ENDIAN unsigned int ip_v:4; unsigned int ip_hl:4; #endif u_int8_t ip_tos; u_short ip_len; u_short ip_id; u_short ip_off; #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; u_int8_t ip_p; u_short ip_sum; struct in_addr ip_src, ip_dst; };
其中ping程序只使用以下数据:
IP报头长度IHL(Internet Header Length)以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。 生存时间TTL(Time To Live)以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。
二、将程序的大致框架搭出来,将ICMP报文发送与接收的过程用程序的语言描述出来:
2.1新建文件ping.hpp,创建一个类
#ifndef _PING_H #define _PING_H #include <iostream> #include <string> #include <string.h> #include <netinet/ip_icmp.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/time.h> #include <arpa/inet.h> #include <unistd.h> #include <signal.h> #define PACKET_SIZE 4096 #define SEND_DATA_LEN 56 #define ERROR -1 #define SUCCESS 1 #define MAX_WAIT_TIME 3 #define MAX_NO_PACKETS 4 using namespace std; class Cping { public : Cping(const char * ip, int timeout); Cping(const Cping& orig); virtual ~Cping(); private : std::string m_strIp; std::string m_copy_Ip; int m_nSend; int m_nRecv; struct sockaddr_in m_dest_addr; struct sockaddr_in m_from_addr; char m_sendpacket[PACKET_SIZE]; char m_recvpacket[PACKET_SIZE]; struct timeval m_tvrecv; struct timeval m_begin_tvrecv; struct timeval m_end_tvrecv; double m_dTotalResponseTimes; int m_nSocketfd; int m_nMaxTimeWait; public : bool ping( int times); bool CreateSocket(); bool CloseSocket(); void send_packet( void ); void recv_packet( void ); int pack( int pack_no); int unpack( char *buf, int len); void tv_sub( struct timeval *out, struct timeval *in); void statistics( int sig); static unsigned short cal_chksum(unsigned short *addr, int len); }; #endif
2.2新建文件ping.cpp,将过程描述出来,实现ping函数,其他函数暂不实现
2.3新建文件main.cpp
#include "ping.h" int main( int argc, char *argv[]) { Cping Ping(argv[1],20); Ping.ping(3); }
2.4完成大体框架的搭建可以先编译运行,没有错误再去将每一个函数实现
三、将其他函数实现,下面贴出源代码
#include "ping.h" Cping::Cping(const char * ip, int timeout) { m_strIp = ip; m_copy_Ip = ip; m_nSend = 0; m_nRecv = 0; m_dTotalResponseTimes = 0; if (timeout > MAX_WAIT_TIME) { m_nMaxTimeWait = MAX_WAIT_TIME; } else { m_nMaxTimeWait = timeout; } } Cping::~Cping() { if (!CloseSocket()) { cout<<"CloseSocket failed!" <<endl; } } bool Cping::ping( int times) { if (!CreateSocket()) { printf("CreateSocket failed!!!\n" ); return false ; } printf("PING %s(%s): %d bytes data in ICMP packets.\n" , m_strIp.c_str(), m_copy_Ip.c_str(), SEND_DATA_LEN); while (times--) { send_packet(); recv_packet(); sleep(1); } statistics(SIGINT); return true ; } bool Cping::CreateSocket() { char buf[2048]; int errnop = 0; unsigned long inaddr; struct hostent hostinfo,*dest_phost; struct protoent *protocol; if ((protocol = getprotobyname( "icmp" )) == NULL) { printf("CreateSocket: getprotobyname failed:%d\n" ,errno); return false ; } if (-1 == (m_nSocketfd = socket(AF_INET,SOCK_RAW,protocol->p_proto))) { printf("CreateSocket: create socket failed:%d\n" ,errno); return false ; } setuid(getuid()); m_dest_addr.sin_family = AF_INET; bzero(&(m_dest_addr.sin_zero),8); if ((inaddr=inet_addr(m_strIp.c_str())) == INADDR_NONE) { if (gethostbyname_r(m_strIp.c_str(),&hostinfo,buf, sizeof (buf),&dest_phost,&errnop)) { printf("CreateSocket: gethostbyname error %s failed:%d\n" ,m_strIp.c_str(),errnop); return false ; } else { m_dest_addr.sin_addr = *((struct in_addr *)dest_phost->h_addr); } } else { m_dest_addr.sin_addr.s_addr = inaddr; } m_copy_Ip = inet_ntoa(m_dest_addr.sin_addr); return true ; } bool Cping::CloseSocket() { bool flag = false ; if (m_nSocketfd) { close(m_nSocketfd); flag = true ; } return flag; } void Cping::send_packet( void ) { int packetsize; //包的大小 packetsize = pack(m_nSend); if ((sendto(m_nSocketfd,m_sendpacket,packetsize,0,( const struct sockaddr*)&m_dest_addr, sizeof (m_dest_addr))) < 0) { printf("send_packet: send error :%d\n" ,errno); } m_nSend++; //发送一个IP包以后,发送次数加1 } void Cping::recv_packet( void ) { int fromlen,packetsize,n; while (m_nRecv < m_nSend) { struct timeval timeout; fd_set readfd; //定义一个文件集合 FD_ZERO(&readfd); //将该集合清空 FD_SET(m_nSocketfd,&readfd); //将套接字描述符添加到readfd集合中 int maxfd = m_nSocketfd + 1; //select监听的最大文件描述符 timeout.tv_sec = m_nMaxTimeWait; //设置select的超时等待时间 timeout.tv_usec = 0; n = select(maxfd,&readfd,NULL,NULL,&timeout); switch (n) { case 0: printf("recv_packet: select time out :%d\n" ,errno); break ; case -1: printf("recv_packet: select error :%d\n" ,errno); break ; default : if (FD_ISSET(m_nSocketfd,&readfd)) { if ((packetsize=recvfrom(m_nSocketfd,m_recvpacket, sizeof (m_recvpacket),0,( struct sockaddr *)&m_from_addr ,(socklen_t*)&fromlen)) < 0) { printf("packetsize = %d\n" ,packetsize); printf("recv_packet: recv error :%d\n" ,errno); return ; } gettimeofday(&m_tvrecv,NULL); m_end_tvrecv.tv_usec = m_tvrecv.tv_usec; m_end_tvrecv.tv_sec = m_tvrecv.tv_sec; if (unpack(m_recvpacket,packetsize) == -1) { continue ; } m_nRecv++; } break ; } } } int Cping::pack( int pack_number) { int packsize; struct icmp *pIcmp; struct timeval *pTime; pIcmp = (struct icmp*)m_sendpacket; pIcmp->icmp_type = ICMP_ECHO; pIcmp->icmp_code = 0; pIcmp->icmp_cksum = 0; pIcmp->icmp_seq = pack_number; pIcmp->icmp_id = getpid(); packsize = 8 + SEND_DATA_LEN; pTime = ( struct timeval *)pIcmp->icmp_data; gettimeofday(pTime, NULL); if (m_nSend == 0) { m_begin_tvrecv.tv_usec = pTime->tv_usec; m_begin_tvrecv.tv_sec = pTime->tv_sec; } <span style="white-space:pre" > </span>pIcmp->icmp_cksum = cal_chksum((unsigned short *)pIcmp,packsize); return packsize; } int Cping::unpack( char *buf, int len) { int i,iphdrlen; struct icmp *pIcmp; struct timeval *tvsend; struct ip* recv_ip = ( struct ip*)buf; double rtt; iphdrlen = recv_ip->ip_hl << 2; pIcmp = (struct icmp*)(buf + iphdrlen); len -= iphdrlen; if (len < 8) { printf( "ICMP packets\'s length is less than 8" ); return -1; } if ((pIcmp->icmp_type == ICMP_ECHOREPLY) && (m_copy_Ip == inet_ntoa(m_from_addr.sin_addr)) && (pIcmp->icmp_id = getpid()) ) { tvsend = (struct timeval *)pIcmp->icmp_data; tv_sub(&m_tvrecv,tvsend); rtt = m_tvrecv.tv_sec * 1000 + (double )m_tvrecv.tv_usec / 1000; printf( "%d byte from %s : icmp_seq=%u ttl=%d time=%.3fms\n" , len, inet_ntoa(m_from_addr.sin_addr), pIcmp->icmp_seq, recv_ip->ip_ttl, rtt); } else { printf( "throw away the old package %d\tbyte from %s\t: icmp_seq=%u\tttl=%d\trtt=%.3f\tms" , len, inet_ntoa(m_from_addr.sin_addr), pIcmp->icmp_seq, recv_ip->ip_ttl, rtt); return -1; } return 1; } void Cping::tv_sub( struct timeval *out, struct timeval *in) { if ((out->tv_usec -= in->tv_usec) < 0) { --out->tv_sec; out->tv_usec += 10000000; } out->tv_sec -= in->tv_sec; } void Cping::statistics( int sig) { tv_sub(&m_end_tvrecv,&m_begin_tvrecv); m_dTotalResponseTimes = m_end_tvrecv.tv_sec * 1000 + (double )m_end_tvrecv.tv_usec / 1000; printf("------statistics------\n" ); printf( "%d packets transmitted, %d received , %d%% lost,time:%.3lfms\n" , m_nSend, m_nRecv, (m_nSend - m_nRecv) / m_nSend * 100,m_dTotalResponseTimes); close(m_nSocketfd); } unsigned short Cping::cal_chksum(unsigned short *addr, int len) { int nleft=len; int sum=0; unsigned short *w=addr; unsigned short answer=0; while (nleft > 1) { sum += *w++; nleft -= 2; } if ( nleft == 1) { *(unsigned char *)(&answer) = *(unsigned char *)w; sum += answer; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return answer; }
四、在我的虚拟机上运行效果