PING(Packet InterNet Groper)中文名为因特网包探索器,是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。ping命令的工作原理是:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把回复报文传回给发送者,这有点象潜水艇声纳系统中使用的发声装置。所以,我们想知道我这台主机能不能和另一台进行通信,我们首先需要确认的是我们两台主机间的网络是不是通的,也就是我说的话能不能传到你那里,这是双方进行通信的前提。在Linux下使用指令ping的方法和现象如下:

PING的实现看起来并不复杂,我想自己写代码实现这个功能,需要些什么知识储备?我简单罗列了一下:

  • ICMP协议的理解
  • RAW套接字
  • 网络封包和解包技能

搭建这么一个ping程序的步骤如下:

  1. ICMP包的封装和解封
  2. 创建一个线程用于ICMP包的发送
  3. 创建一个线程用于ICMP包的接收
  4. 原始套接字编程
 
PING的流程如下:
 
 
一、ICMP包的封装和解封
(1) ICMP协议理解
要进行PING的开发,我们首先需要知道PING的实现是基于ICMP协议来开发的。要进行ICMP包的封装和解封,我们首先需要理解ICMP协议。ICMP位于网络层,允许主机或者路由器报告差错情况和提供有关异常情况的报告。ICMP报文是封装在IP数据报中,作为其中的数据部分。ICMP报文作为IP层数据报的数据,加上数据报头,组成IP数据报发送出去。ICMP报文格式如下:
ICMP报文的种类有两种,即ICMP差错报告报文和ICMP询问报文。PING程序使用的ICMP报文种类为ICMP询问报文。注意一下上面说到的ICMP报文格式中的“类型”字段,我们在组包的时候可以向该字段填写不同的值来标定该ICMP报文的类型。下面列出的是几种常用的ICMP报文类型。
我们的PING程序需要用到的ICMP的类型是回送请求(8)。
因为ICMP报文的具体格式会因为ICMP报文的类型而各不相同,我们ping包的格式是这样的:

(2) ICMP包的组装
  对照上面的ping包格式,我们封装ping包的代码可以这么写:
void icmp_pack(struct icmp* icmphdr, int seq, int length)
{
int i = ; icmphdr->icmp_type = ICMP_ECHO; //类型填回送请求
icmphdr->icmp_code = ;
icmphdr->icmp_cksum = ; //注意,这里先填写0,很重要!
icmphdr->icmp_seq = seq; //这里的序列号我们填1,2,3,4....
icmphdr->icmp_id = pid & 0xffff; //我们使用pid作为icmp_id,icmp_id只是2字节,而pid有4字节
for(i=;i<length;i++)
{
icmphdr->icmp_data[i] = i; //填充数据段,使ICMP报文大于64B
} icmphdr->icmp_cksum = cal_chksum((unsigned short*)icmphdr, length); //校验和计算
}
这里再三提醒一下,icmp_cksum 必须先填写为0再执行校验和算法计算,否则ping时对方主机会因为校验和计算错误而丢弃请求包,导致ping的失败。我一个同事曾经就因为这么一个错误而排查许久,血的教训请铭记。

这里简单介绍一下checksum(校验和)。

计算机网络通信时,为了检验在数据传输过程中数据是否发生了错误,通常在传输数据的时候连同校验和一块传输,当接收端接受数据时候会从新计算校验和,如果与原校验和不同就视为出错,丢弃该数据包,并返回icmp报文。

 
算法基本思路:

IP/ICMP/IGMP/TCP/UDP等协议的校验和算法都是相同的,采用的都是将数据流视为16位整数流进行重复叠加计算。为了计算检验和,首先把检验和字段置为0。然后,对有效数据范围内中每个16位进行二进制反码求和,结果存在检验和字段中,如果数据长度为奇数则补一字节0。当收到数据后,同样对有效数据范围中每个16位数进行二进制反码的求和。由于接收方在计算过程中包含了发送方存在首部中的检验和,因此,如果首部在传输过程中没有发生任何差错,那么接收方计算的结果应该为全0或全1(具体看实现了,本质一样) 。如果结果不是全0或全1,那么表示数据错误。

/*校验和算法*/
unsigned short cal_chksum(unsigned short *addr,int len)
{ int nleft=len;
int sum=;
unsigned short *w=addr;
unsigned short answer=; /*把ICMP报头二进制数据以2字节为单位累加起来*/
while(nleft>)
{
sum+=*w++;
nleft-=;
}
/*若ICMP报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加*/
if( nleft==)
{
*(unsigned char *)(&answer)=*(unsigned char *)w;
sum+=answer;
}
sum=(sum>>)+(sum&0xffff);
sum+=(sum>>);
answer=~sum;
return answer;
}
 
(3) ICMP包的解包
知道怎么封装包,那解包就也不难了,注意的是,收到一个ICMP包,我们不要就认为这个包就是我们发出去的ICMP回送回答包,我们需要加一层代码来判断该ICMP报文的id和seq字段是否符合我们发送的ICMP报文的设置,来验证ICMP回复包的正确性。
int icmp_unpack(char* buf, int len)
{
int iphdr_len;
struct timeval begin_time, recv_time, offset_time;
int rtt; //round trip time struct ip* ip_hdr = (struct ip *)buf;
iphdr_len = ip_hdr->ip_hl*;
struct icmp* icmp = (struct icmp*)(buf+iphdr_len); //使指针跳过IP头指向ICMP头
len-=iphdr_len; //icmp包长度
if(len < ) //判断长度是否为ICMP包长度
{
fprintf(stderr, "Invalid icmp packet.Its length is less than 8\n");
return -;
} //判断该包是ICMP回送回答包且该包是我们发出去的
if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == (pid & 0xffff)))
{
if((icmp->icmp_seq < ) || (icmp->icmp_seq > PACKET_SEND_MAX_NUM))
{
fprintf(stderr, "icmp packet seq is out of range!\n");
return -;
} ping_packet[icmp->icmp_seq].flag = ;
begin_time = ping_packet[icmp->icmp_seq].begin_time; //去除该包的发出时间
gettimeofday(&recv_time, NULL); offset_time = cal_time_offset(begin_time, recv_time);
rtt = offset_time.tv_sec* + offset_time.tv_usec/; //毫秒为单位 printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%d ms\n",
len, inet_ntoa(ip_hdr->ip_src), icmp->icmp_seq, ip_hdr->ip_ttl, rtt); }
else
{
fprintf(stderr, "Invalid ICMP packet! Its id is not matched!\n");
return -;
}
return ;
}
 
 
二、发包线程的搭建
根据PING程序的框架,我们需要建立一个线程用于ping包的发送,我的想法是这样的:使用sendto进行发包,发包速率我们维持在1秒1发,我们需要用一个全局变量记录第一个ping包发出的时间,除此之外,我们还需要一个全局变量来记录我们发出的ping包到底有几个,这两个变量用于后来收到ping包回复后的数据计算。
void ping_send()
{
char send_buf[];
memset(send_buf, , sizeof(send_buf));
gettimeofday(&start_time, NULL); //记录第一个ping包发出的时间
while(alive)
{
int size = ;
gettimeofday(&(ping_packet[send_count].begin_time), NULL);
ping_packet[send_count].flag = ; //将该标记为设置为该包已发送 icmp_pack((struct icmp*)send_buf, send_count, ); //封装icmp包
size = sendto(rawsock, send_buf, , , (struct sockaddr*)&dest, sizeof(dest));
send_count++; //记录发出ping包的数量
if(size < )
{
fprintf(stderr, "send icmp packet fail!\n");
continue;
} sleep();
}
}

三、收包线程的搭建
我们同样建立一个接收包的线程,这里我们采用select函数进行收包,并为select函数设置超时时间为200us,若发生超时,则进行下一个循环。同样地,我们也需要一个全局变量来记录成功接收到的ping回复包的数量。

void ping_recv()
{
struct timeval tv;
tv.tv_usec = ; //设置select函数的超时时间为200us
tv.tv_sec = ;
fd_set read_fd;
char recv_buf[];
memset(recv_buf, ,sizeof(recv_buf));
while(alive)
{
int ret = ;
FD_ZERO(&read_fd);
FD_SET(rawsock, &read_fd);
ret = select(rawsock+, &read_fd, NULL, NULL, &tv);
switch(ret)
{
case -:
fprintf(stderr,"fail to select!\n");
break;
case :
break;
default:
{
int size = recv(rawsock, recv_buf, sizeof(recv_buf), );
if(size < )
{
fprintf(stderr,"recv data fail!\n");
continue;
} ret = icmp_unpack(recv_buf, size); //对接收的包进行解封
if(ret == -) //不是属于自己的icmp包,丢弃不处理
{
continue;
}
recv_count++; //接收包计数
}
break;
} }
}
 
 
四、中断处理
我们规定了一次ping发送的包的最大值为64个,若超出该数值就停止发送。作为PING的使用者,我们一般只会发送若干个包,若有这几个包顺利返回,我们就crtl+c中断ping。这里的代码主要是为中断信号写一个中断处理函数,将alive这个全局变量设置为0,进而使发送ping包的循环停止而结束程序。
void icmp_sigint(int signo)
{
alive = ;
gettimeofday(&end_time, NULL);
time_interval = cal_time_offset(start_time, end_time);
} signal(SIGINT, icmp_sigint);
 
 
五、总体实现
各模块介绍完了,现在贴出完整代码。
 #include <stdio.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/time.h>
#include <string.h>
#include <netdb.h>
#include <pthread.h> #define PACKET_SEND_MAX_NUM 64 typedef struct ping_packet_status
{
struct timeval begin_time;
struct timeval end_time;
int flag; //发送标志,1为已发送
int seq; //包的序列号
}ping_packet_status; ping_packet_status ping_packet[PACKET_SEND_MAX_NUM]; int alive;
int rawsock;
int send_count;
int recv_count;
pid_t pid;
struct sockaddr_in dest;
struct timeval start_time;
struct timeval end_time;
struct timeval time_interval; /*校验和算法*/
unsigned short cal_chksum(unsigned short *addr,int len)
{ int nleft=len;
int sum=;
unsigned short *w=addr;
unsigned short answer=; /*把ICMP报头二进制数据以2字节为单位累加起来*/
while(nleft>)
{
sum+=*w++;
nleft-=;
}
/*若ICMP报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加*/
if( nleft==)
{
*(unsigned char *)(&answer)=*(unsigned char *)w;
sum+=answer;
}
sum=(sum>>)+(sum&0xffff);
sum+=(sum>>);
answer=~sum;
return answer;
} struct timeval cal_time_offset(struct timeval begin, struct timeval end)
{
struct timeval ans;
ans.tv_sec = end.tv_sec - begin.tv_sec;
ans.tv_usec = end.tv_usec - begin.tv_usec;
if(ans.tv_usec < ) //如果接收时间的usec小于发送时间的usec,则向sec域借位
{
ans.tv_sec--;
ans.tv_usec+=;
}
return ans;
} void icmp_pack(struct icmp* icmphdr, int seq, int length)
{
int i = ; icmphdr->icmp_type = ICMP_ECHO;
icmphdr->icmp_code = ;
icmphdr->icmp_cksum = ;
icmphdr->icmp_seq = seq;
icmphdr->icmp_id = pid & 0xffff;
for(i=;i<length;i++)
{
icmphdr->icmp_data[i] = i;
} icmphdr->icmp_cksum = cal_chksum((unsigned short*)icmphdr, length);
} int icmp_unpack(char* buf, int len)
{
int iphdr_len;
struct timeval begin_time, recv_time, offset_time;
int rtt; //round trip time struct ip* ip_hdr = (struct ip *)buf;
iphdr_len = ip_hdr->ip_hl*;
struct icmp* icmp = (struct icmp*)(buf+iphdr_len);
len-=iphdr_len; //icmp包长度
if(len < ) //判断长度是否为ICMP包长度
{
fprintf(stderr, "Invalid icmp packet.Its length is less than 8\n");
return -;
} //判断该包是ICMP回送回答包且该包是我们发出去的
if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == (pid & 0xffff)))
{
if((icmp->icmp_seq < ) || (icmp->icmp_seq > PACKET_SEND_MAX_NUM))
{
fprintf(stderr, "icmp packet seq is out of range!\n");
return -;
} ping_packet[icmp->icmp_seq].flag = ;
begin_time = ping_packet[icmp->icmp_seq].begin_time;
gettimeofday(&recv_time, NULL); offset_time = cal_time_offset(begin_time, recv_time);
rtt = offset_time.tv_sec* + offset_time.tv_usec/; //毫秒为单位 printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%d ms\n",
len, inet_ntoa(ip_hdr->ip_src), icmp->icmp_seq, ip_hdr->ip_ttl, rtt); }
else
{
fprintf(stderr, "Invalid ICMP packet! Its id is not matched!\n");
return -;
}
return ;
} void ping_send()
{
char send_buf[];
memset(send_buf, , sizeof(send_buf));
gettimeofday(&start_time, NULL); //记录第一个ping包发出的时间
while(alive)
{
int size = ;
gettimeofday(&(ping_packet[send_count].begin_time), NULL);
ping_packet[send_count].flag = ; //将该标记为设置为该包已发送 icmp_pack((struct icmp*)send_buf, send_count, ); //封装icmp包
size = sendto(rawsock, send_buf, , , (struct sockaddr*)&dest, sizeof(dest));
send_count++; //记录发出ping包的数量
if(size < )
{
fprintf(stderr, "send icmp packet fail!\n");
continue;
} sleep();
}
} void ping_recv()
{
struct timeval tv;
tv.tv_usec = ; //设置select函数的超时时间为200us
tv.tv_sec = ;
fd_set read_fd;
char recv_buf[];
memset(recv_buf, ,sizeof(recv_buf));
while(alive)
{
int ret = ;
FD_ZERO(&read_fd);
FD_SET(rawsock, &read_fd);
ret = select(rawsock+, &read_fd, NULL, NULL, &tv);
switch(ret)
{
case -:
fprintf(stderr,"fail to select!\n");
break;
case :
break;
default:
{
int size = recv(rawsock, recv_buf, sizeof(recv_buf), );
if(size < )
{
fprintf(stderr,"recv data fail!\n");
continue;
} ret = icmp_unpack(recv_buf, size); //对接收的包进行解封
if(ret == -) //不是属于自己的icmp包,丢弃不处理
{
continue;
}
recv_count++; //接收包计数
}
break;
} }
} void icmp_sigint(int signo)
{
alive = ;
gettimeofday(&end_time, NULL);
time_interval = cal_time_offset(start_time, end_time);
} void ping_stats_show()
{
long time = time_interval.tv_sec*+time_interval.tv_usec/;
/*注意除数不能为零,这里send_count有可能为零,所以运行时提示错误*/
printf("%d packets transmitted, %d recieved, %d%c packet loss, time %ldms\n",
send_count, recv_count, (send_count-recv_count)*/send_count, '%', time);
} int main(int argc, char* argv[])
{
int size = *;//128k
struct protoent* protocol = NULL;
char dest_addr_str[];
memset(dest_addr_str, , );
unsigned int inaddr = ;
struct hostent* host = NULL; pthread_t send_id,recv_id; if(argc < )
{
printf("Invalid IP ADDRESS!\n");
return -;
} protocol = getprotobyname("icmp"); //获取协议类型ICMP
if(protocol == NULL)
{
printf("Fail to getprotobyname!\n");
return -;
} memcpy(dest_addr_str, argv[], strlen(argv[])+); rawsock = socket(AF_INET,SOCK_RAW,protocol->p_proto);
if(rawsock < )
{
printf("Fail to create socket!\n");
return -;
} pid = getpid(); setsockopt(rawsock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); //增大接收缓冲区至128K bzero(&dest,sizeof(dest)); dest.sin_family = AF_INET; inaddr = inet_addr(argv[]);
if(inaddr == INADDR_NONE) //判断用户输入的是否为IP地址还是域名
{
//输入的是域名地址
host = gethostbyname(argv[]);
if(host == NULL)
{
printf("Fail to gethostbyname!\n");
return -;
} memcpy((char*)&dest.sin_addr, host->h_addr, host->h_length);
}
else
{
memcpy((char*)&dest.sin_addr, &inaddr, sizeof(inaddr));//输入的是IP地址
}
inaddr = dest.sin_addr.s_addr;
printf("PING %s, (%d.%d.%d.%d) 56(84) bytes of data.\n",dest_addr_str,
(inaddr&0x000000ff), (inaddr&0x0000ff00)>>,
(inaddr&0x00ff0000)>>, (inaddr&0xff000000)>>); alive = ; //控制ping的发送和接收 signal(SIGINT, icmp_sigint); if(pthread_create(&send_id, NULL, (void*)ping_send, NULL))
{
printf("Fail to create ping send thread!\n");
return -;
} if(pthread_create(&recv_id, NULL, (void*)ping_recv, NULL))
{
printf("Fail to create ping recv thread!\n");
return -;
} pthread_join(send_id, NULL);//等待send ping线程结束后进程再结束
pthread_join(recv_id, NULL);//等待recv ping线程结束后进程再结束 ping_stats_show(); close(rawsock);
return ; }
编译以及实验现象如下:
我的实验环境是两台服务器,发起ping的主机是172.0.5.183,被ping的主机是172.0.5.182,以下是我的两次实验现象(ping IP和ping 域名)。
 
特别注意: 

只有root用户才能利用socket()函数生成原始套接字,要让Linux的一般用户能执行以上程序,需进行如下的特别操作:用root登陆,编译以上程序gcc -lpthread -o ping ping.c

实验现象可以看出,PING是成功的,表明两主机间的网络是通的,发出的所有ping包都收到了回复。
 
下面是Linux系统自带的PING程序,我们可以对比一下我们设计的PING程序跟系统自带的PING程序有何不同。

Linux编程之PING的实现的更多相关文章

  1. Linux编程之ICMP洪水攻击

    我的上一篇文章<Linux编程之PING的实现>里使用ICMP协议实现了PING的程序,ICMP除了实现这么一个PING程序,还有哪些不为人知或者好玩的用途?这里我将介绍ICMP另一个很有 ...

  2. 【转】 linux编程之GDB调试

    GDB是一套字符界面的程序集,可以用它在linux上调试C和C++程序,它提供了以下的功能: 1 在程序中设置断点,当程序运行到断点处暂停 2 显示变量的值,可以打印或者监视某个变量,将某个变量的值显 ...

  3. Linux编程之UDP SOCKET全攻略

    这篇文章将对linux下udp socket编程重要知识点进行总结,无论是开发人员应知应会的,还是说udp socket的一些偏僻知识点,本文都会讲到.尽可能做到,读了一篇文章之后,大家对udp so ...

  4. Linux编程之epoll

    现在有这么一个场景:我是一个很忙的大老板,我有100个手机,手机来信息了,我的秘书就会告诉我"老板,你的手机来信息了."我很生气,我的秘书就是这样子,每次手机来信息就只告诉我来信息 ...

  5. Linux编程之fork函数

    在Linux中,fork函数的功能就是在一个进程中创建一个新的进程,当前调用fork函数的进程就是产生的新进程的父进程,新进程在以下也称为子进程.在新进程生成之后就会在系统中开始执行. 函数原型:pi ...

  6. 【转】Linux编程之UDP SOCKET全攻略

    转自:http://www.cnblogs.com/skyfsm/p/6287787.html?utm_source=itdadao&utm_medium=referral 这篇文章将对lin ...

  7. linux编程之GDB调试

    GDB是一套字符界面的程序集,可以用它在linux上调试C和C++程序,它提供了以下的功能: 1 在程序中设置断点,当程序运行到断点处暂停 2 显示变量的值,可以打印或者监视某个变量,将某个变量的值显 ...

  8. 网络编程之ping

    #include <sys/types.h>#include <netinet/ip.h>#include <netdb.h>#include<arpa/in ...

  9. Linux编程之select

    select系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读.可写和异常等事件. select 机制的优势 为什么会出现select模型? 先看一下下面的这句代码: int i ...

随机推荐

  1. CentOS tengine mysql 5.7 php 5.6

    CentOS 7.x 编译安装 LNMP L 版本是 CentOS 7.x  x64版本, N 我们使用tengine 的最新版本,主要原因是因为tengine 默认支持很多的模块. M 这里我们选用 ...

  2. arrayList LinkedList HashMap HashTable的区别

    ArrayList 采用的是数组形式来保存对象的,这种方式将对象放在连续的位置中,所以最大的缺点就是插入删除时非常麻烦 LinkedList 采用的将对象存放在独立的空间中,而且在每个空间中还保存下一 ...

  3. (简单) POJ 3264 Balanced Lineup,RMQ。

    Description For the daily milking, Farmer John's N cows (1 ≤ N ≤ 50,000) always line up in the same ...

  4. Quartz.NET总结(一)

    Quartz.NET总结(一) 前段时间,花了大量的时间,将原先的计划任务,切换到Quartz.NET来进行管理.原先的后台定时服务都是通过计划任务来实现的,但是随着业务增长,计划任务也越来越多,每个 ...

  5. MySQL 同步状态

    Exec_Master_Log_Pos: The position of the last event executed by the SQL thread from the master's bin ...

  6. photoshop如何快速切图

    作为业余爱好,之前都是用比较笨的方法切图,甚至用裁剪工具一张一张的切. 后来知道用切片工具,但也仅限于互不重叠的图片. 在工作中实际使用时才发现实在是太慢了,慢到上级自己说你不用做了,我来吧. 其实, ...

  7. mongodb 3.x connect with credential

    package mongoDb; import java.net.UnknownHostException; import java.util.ArrayList; import java.util. ...

  8. Android源码编译jar包BUILD_JAVA_LIBRARY 与BUILD_STATIC_JAVA_LIBRARY的区别(二)

    上文简单介绍了BUILD_JAVA_LIBRARY 与BUILD_STATIC_JAVA_LIBRARY编译出来jar包的区别, 那么你如果拿到了一个内容是dex格式的jar包,而你又偏偏需要这个ja ...

  9. LPC1768的iic通讯

    LPC1768有三路IIC,其中IIC0支持高速模式和plus模式,另外两路是普通IIC,使用IIC的过程如下 首先依然是打开IIC时钟,同时打开GPIO时钟 然后配置引脚为IIC功能 另外,因为ii ...

  10. UVa 483 - Word Scramble

    题目大意:给一个由单词组成的句子,只反转单词内部的次序而不改变单词之间的次序.如“I love you.”转换成“I evol .uoy”. #include <cstdio> #incl ...