/*注意:1,由于本人技术有限,所以如果你是技术的狂热追求者,我觉得你不用看我写的了,这是因为我很菜。。。我主要是帮助与我一样,想做一些事,但不知道从哪里做起的人的。
2,为了叙述简洁,以下内容可能并经不起推敲(可能还有错误),也有很多省略,所以,希望读者能够在看完本文后再看其他相应书籍,以对其深入的知识进行全面了解。
*/
一,前言
为了判断与对方主机的连通性,不论是Linux系统还是Windows,我们通常都会使用ping命令来进行判断,今天我的目标便是用尽量简单的描述教会有这方面需求的童鞋来实现自己的ping命令。
刚开始我在网上也找到了好多相关的教程,但是发现它们却并不友好,有好多我无法理解的,可以说是各种函数,技巧的堆积,对于一个想简单的实现ping命令或学习这方面的人来讲代价太大,所以经过我的实践,我想把自己的经验分享出来。以下内容的阅读不强制但建议你最好具备以下的技能:
1,计算机网络的相关知识
2,在Linux下接触过socket编程
二,内容
2.1 ICMP报文
在实现ping命令前我们首先需要知道它的大致原理。ICMP即网际控制报文协议,它主要有两个功能,一种是icmp差错控制报文,另一种是icmp请求应答报文,对于前者有兴趣的同学可以自行了解,今天我们所要了解的内容是针对第二种报文来展开的。
ICMP请求应答报文通俗地讲它是用来判断对方主机情况的。规定当一台主机向另一台主机发送icmp请求报文的时候,对方需要回一个icmp应答报文,但也有例外情况。
2.2 ICMP请求应答报文格式
(注:图片来自网络)
icmp请求应答报文前8个字节是固定格式。
其中类型为ICMP_ECHO(即为8),code为0时它代表请求回送报文,
当类型为ICMP_ECHOREPLY(即为0),code为0时它代表请求回应报文。
校验和:因为icmp报文是依靠IP数据报来传送的(IP首部+icmp报文),而IP协议又是不可靠的传输协议,所以通过检验和可以判断传输过来的数据是否发生变化(如何检验请自行学习),为什么要判断数据是否发生变化呢?这是因为,网络中有大量的数据,一台主机往往会收到很多数据,如果主机发现发送来的数据有错误,则可以直接丢弃,而不用花费多余的时间对错误的数据进行处理,这就提高了网络的效率。
标识符:用当前进程的pid号进行填充。
序号:一般从0开始,随着发送报文的增多依次递增1。2.3 ping命令的消息格式
如上所示,内容主要包括字节数,时间,TTL。
其中TTL(time to live),指经过的路由器数,它在IP数据报的ip_ttl字段有携带。
时间我们可以在icmp报文的数据字段里带上当前时间的信息,主要借助了timeval结构体,其内容如下:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
它在sys/time.h头文件里有定义,其中tv_sec指当前的秒数,tv_usec指当前的微妙,需要注意的是这里的时间代表的是自1970年1月1日0时起到现在所经过的时间,即时间戳。所以在计算经过的时间时你可以用两个时间戳进行相减,所得的时间即为经过的时间。
接下来看一个timeval的应用:
#include<stdio.h>
#include<sys/time.h>
int main(void)
{
struct timeval tal;
int i=0;
for(;i<3;i++)
{
gettimeofday(&tal,NULL);
printf("%d %d\n",tal.tv_sec,tal.tv_usec);
sleep(1);
}
return 0;
}
gettimeofday函数:
其原型为:int gettimeofday(struct timeval*tv,struct timezone *tz )
其功能是把当前时间信息放入tv所指向的结构体中,而tz则指向时区信息,由于我们并不需要,所以置为NULL;
当函数成功时会返回0,失败-1.
所以我们这里演示的代码为计算每过一秒tv结构体里所存放的信息。
运行结果:
我们每过一秒循环打印当前的时间戳,从它们之间的tv_sec区别,我们也可以看出相差一秒。
2.3 ICMP结构体与IP结构体
linux中
icmp报文结构定义在netinet/ip_icmp.h
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 */
};
struct icmp
{
u_int8_t icmp_type; /* 消息类型 */
u_int8_t icmp_code; /* 代码 */
u_int16_t icmp_cksum; /* 校验和 */
union
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* gateway address */
struct ih_idseq /* 显示数据报 */
{
u_int16_t icd_id; /*数据报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
};
我相信看到上面的定义许多人都一脸懵逼,我也不例外,虽然为了实现各种功能,Linux自带的结构体定义的十分完善,但我们并用不到所有的字段。所以接下来只讨论我们需要的地方。
IP数据报中我们只需要注意的是ip_ttl字段。
而icmp报文我们需要知道的是:
icmp_type:类型
icmp_code:代码
icmp_cksum:检验和
icmp_id:标识符
icmp_seq:序号
icmp_data:携带数据
2.4 实现步骤
1,构建一个icmp请求应答报文,并把当前的时间信息放入到数据字段里
2,接收应答报文。拿到报文后需要解包,因为icmp是依靠IP数据报来传送的,所以只需要根据ip_hl字段便能知道IP首部的长度,则去掉IP首部之后剩下的自然是我们想要的icmp报文。
3,当我们发送一个ICMP请求应答报文后,对方收到后也会回复相应的内容,只是类型字段变为了ICMP_ECHOREPLY。
三,代码实现
环境:centos
3.1 相应的头文件与函数声名
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<netinet/ip_icmp.h>
#include<signal.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<string.h>
#include<netdb.h>
#include<pthread.h> //使用多线程来收发数据报
#define PACK_SIZE 4096 //包的大小
char send_packet[PACK_SIZE]; //发的包
char recv_packet[PACK_SIZE]; //收的包
int sockfd;//套接字
pid_t pid;//进程号
int begin=0,end=0; //用begin来记录发的包,end用来记录收到的包,
//因为有可能因为网络问题发生丢包事件
int datalen=56; //icmp包数据的大小
struct timeval recv_time;
struct sockaddr_in from;
void* send_data(void* arg); //发数据
void* recv_data(void* arg);//收数据
unsigned short get_cksum(unsigned char*,int); //计算校验和
void time_sub(struct timeval*,struct timeval*); //计算时间差
阅读建议:其中检验和的计算,网上有好多,我是直接拿来使用的,其他函数我都进行了相应的注释。为什么使用多线程?是因为我想实现可以一直发,用户想停的时候再停,但因为我的知识水平有限,所以没有实现,具体的效果可以参照Linux下的ping命令。
另外有什么不了解的地方可以一个函数,一个函数的攻克,也可以先在自己的电脑上敲一遍,实现功能,然后再结合本文与相关资料进行深入了解。
全部代码如下:
/*
**time:2017.11.22
**author:奔跑的代码君
**功能:ping命令的模拟,实现直接ping ip,以及域名
**环境:centos
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<netinet/ip_icmp.h>
#include<signal.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<string.h>
#include<netdb.h>
#include<pthread.h>
#define PACK_SIZE 4096
char send_packet[PACK_SIZE]; //发的包
char recv_packet[PACK_SIZE]; //收的包
int sockfd;//套接字
pid_t pid;//进程号
int begin=0,end=0; //记录发收的包的数量
int datalen=56; //icmp数据长度
struct timeval recv_time; //记录某一刻的时间
struct sockaddr_in from;
void* send_data(void* arg); //发数据
void* recv_data(void* arg); //收数据
unsigned short get_cksum(unsigned char*,int); //校验和计算
void time_sub(struct timeval*,struct timeval*); //计算时间差
void* send_data(void* arg)
{
int packetsize;
int i=0;
for(;i<4;i++)
{
packetsize=pack(i); //构建4个包,把序号传过去
if(sendto(sockfd,send_packet,packetsize,0,(struct sockaddr*)&from,sizeof(from))<0) //发数据
{
//失败
perror("sendto");
continue;
}
else
{
begin++; //成功后,休眠1s
//printf("send is success\n");
sleep(1);
}
}
return NULL;
}
//收数据
void* recv_data(void* arg)
{
int n,fromlen;
fromlen=sizeof(from);
int i=0;
while(i<4)
{
if((n=recvfrom(sockfd,recv_packet,sizeof(recv_packet),0,(struct sockaddr*)&from,&fromlen))<0) //收数据
{
continue;
}
if(unpack(recv_packet,n)==-1) //解包失败
{
i++;
continue;
}
else
{
end++; //成功
}
}
}
//校验和计算
unsigned short get_cksum(unsigned char* data,int len) //求检验和
{
int sum=0;
int odd=(len&0x01);
while(len&0xfffe)
{
sum+=*(unsigned short*)data;
data+=2;
len-=2;
}
if(odd)
{
unsigned short tmp=((*data)<<8)&0xff00;
sum+=tmp;
}
sum=(sum>>16)+(sum&0xffff);
sum+=(sum>>16);
return ~sum;
}
//构建包
int pack(int pack_no)
{
int size;
struct icmp* myicmp;
myicmp=(struct icmp*)send_packet;
//对相应字段填充
myicmp->icmp_type=ICMP_ECHO;
myicmp->icmp_code=0;
myicmp->icmp_cksum=0;
myicmp->icmp_seq=pack_no;
myicmp->icmp_id=pid;
size=8+datalen; //总的icmp大小
//将时间信息放入data字段
struct timeval*send_time=(struct timeval*)myicmp->icmp_data;
gettimeofday(send_time,NULL);
//注意检验和放最后,因为数据也要进行校验
myicmp->icmp_cksum=get_cksum(send_packet,size);
return size;
}
//解包
int unpack(char* buf,int len)
{
int ip_len;
struct ip* myip;
struct icmp* myicmp;
myip=(struct ip*)buf;
ip_len=myip->ip_hl<<2; //IP首部长度*4
myicmp=(struct icmp*)(buf+ip_len); //获得ICMP报文
len-=ip_len;
if(len<8) //ICMP固定长度为8,小于即错误
{
printf("icmp packet is small than 8\n");
return -1;
}
// printf("i am coming\n");
if(myicmp->icmp_type==ICMP_ECHOREPLY && myicmp->icmp_id==pid) //判断是不是想要的包
{
// printf("i am coming\n");
gettimeofday(&recv_time,NULL);
struct timeval* sendTime=(struct timeval*)myicmp->icmp_data;
time_sub(&recv_time,sendTime); //计算时间差放到recv_time里面
double rtt=recv_time.tv_sec*1000+recv_time.tv_usec/1000; //以ms为单位
//打印相关信息
printf("%d byte from %s:icmp_seq=%u ttl=%d rtt=%fms\n",len,inet_ntoa(from.sin_addr),myicmp->icmp_seq,myip->ip_ttl,rtt);
}
else
//不是想要的包
return -1;
}
//时间差计算
void time_sub(struct timeval* end,struct timeval* begin)
{
end->tv_sec-=begin->tv_sec;
end->tv_usec-=begin->tv_usec;
}
//主函数
int main(int argc,char* argv[])
{
if(argc<2)
{
printf("usage:%s hostname/ip\n",argv[0]);
exit(-1);
}
if((sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))<0) //创建原始套接字
{
perror("socket");
exit(-1);
}
memset(&from,0,sizeof(from));
//此处内容可以看我上篇博客有详细的介绍
if((from.sin_addr.s_addr=inet_addr(argv[1]))==INADDR_NONE)//如果传入的不是ip
{
struct hostent* hptr=NULL;
//转换失败
if((hptr=gethostbyname(argv[1]))==NULL)
{
perror("gethostbyname");
exit(-1);
}
memcpy(&from.sin_addr,hptr->h_addr,sizeof(hptr->h_addr));
}
else
{
from.sin_addr.s_addr=inet_addr(argv[1]);
}
from.sin_family=AF_INET;
pid=getpid();
printf("ping %s,%d data in icmp packets\n",argv[1],datalen);
pthread_t id1,id2;
//创建线程
if(pthread_create(&id1,NULL,send_data,NULL)<0)
{
perror("pthread_create");
return -2;
}
if(pthread_create(&id2,NULL,recv_data,NULL)<0)
{
perror("pthread_create");
return -3;
}
//阻塞等待
pthread_join(id1,NULL);
pthread_join(id2,NULL);
close(sockfd); //记得关闭套接字
printf("ping %s is complete,%d was sended and %d was received\n",argv[1],begin,end);
return 0;
}
编译命令:gcc -o pingbyme pingbyme.c -lpthread
运行结果:
四,总结
看到这里希望你也能实现自己的ping命令吧。在ping命令的实现过程中我收获了很多知识,有些问题果然没有比自己敲一遍来的更快。另外在实现过程中我翻阅了许多大佬们的实现代码,然后在自己的理解下借鉴以及改进最后实现了这个“乞丐版”,它还有很多不足,也希望你能从中找到自己更好的理解吧。