ping报文的原理就是先向特定的ip地址发送一个ping请求消息即ping echo,然后如果对应ip地址的服务器收到这个请求的话就会发送ping回应消息即ping reply。
通过抓包,还可以看到,如果本地主机的arp表里没有对应目的地址的表项,底下网络层会发送arp报文查询目的主机的mac地址,等收到对端的arp响应后,再发送ping echo请求,否则也会ping失败。
正常的一个ping过程代码如下,首先是定义一个icmp报文头结构:
//icmp报文头定义
typedef struct {
unsigned char i_type; //类型
unsigned char i_code; //代码
unsigned short i_cksum; //校验和
unsigned short i_id; //标识符
unsigned short i_seq; //序列号
unsigned long timestamp; //时间戳
} IcmpHead;
然后是实现代码:
bool ping (unsigned long dstIp)
{
//创建raw socket,使用icmp协议
int sockRaw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
//存放ping请求数据的缓存
char icmp_data[1024];
memset(icmp_data, 0, sizeof(IcmpHead));
//构造请求ping数据
IcmpHead *pHead = (IcmpHead*)icmp_data;
pHead->i_type = ICMP_ECHO;
pHead->i_code = 0;
pHead->i_id=getpid();
pHead->i_seq = 1;
pHead->timestamp = time(0);
pHead->i_cksum = checksum(icmp_data, sizeof(IcmpHead));
//构造目的地址结构
struct sockaddr_in dstAddr = {0};
dstAddr.sin_add.s_addr = dstIp;
dstAddr.sin_family = AF_INET;
//发送ping请求报文
sendto(sockRaw, icmp_data, sizeof(IcmpHead), 0, (struct sockaddr*)&dstAddr, sizeof(sockaddr_in));
//接受ping回应数据
char icmp_reply_data[1024];
struct sockaddr_in srcAddr = {0};
int iRead = recvfrom(sockRaw, icmp_reply_data, 1024,0,&srcAddr, sizeof(sockaddr_in));
if (iRead <=0)
{
return false;
}
//解析回应数据
IcmpHead *pReply = (IcmpHead*)icmp_reply_data;
if ((pReply->i_type == ICMP_ECHOREPLY) &&
(pReply->i_id == getpid() &&
(pReply->i_seq == pHead->i_seq))
{
return true;
}
return false;
}
这样一个发送请求和等待回应的过程,根据网络情况都会有一定的延时,那如何能提高ping的效率以达到对20000个设备每10秒钟就ping一次要求呢。
观察上面的代码,可以知道,等待的时间都阻塞在recvfrom系统调用上,如果对端响应慢了或者完全不响应ping请求,我们还需要给其设定一个超时时间参数,
具体的做法就是在recvfrom调用前,先通过select系统调用并设定好超时时间,来获取对应socket上的读事件,事件到达后再执行recvfrom,否则如果超时时间到了就返回ping失败。
如果要求一次ping大量设备,一个个串行ping过来肯定是不行的,那就并行进行就可以了,并行的方法很多,常用的是多线程方式,即对每个设备单独起线程来ping。
但是设备多的话,对应的线程开销也不可忽略,其实对于ping这种操作来说,完全可以一个线程来完成一组设备的并行ping,采用类似cpu流水线的方法,将一个ping过程拆分成如下几个步骤:
创建socket -> 构造ping请求 -> 发送ping请求 -> 接收ping响应 -> 解析ping响应
我们可以分析下多个ping任务的话,如何让上面几个步骤如何并行
首先是创建socket,这个是可以所有ping任务重用的,即可以用同一个socket来完成全部的ping报文发送和接收操作。
构造ping请求完全是内存中操作,串行进行时间可以忽略
发送ping请求由于icmp基于ip协议,不需要面向连接,所以也不会阻塞
接收ping响应必须等待对端返回ping回应报文,这个操作最费时,因此应该并行完成
解析ping响应也是内存操作,很快的
通过上面分析,我们的ping操作就需要将等待ping回应这个过程给并行起来,而并行的办法就是一次就发送一组ping请求,然后进入等待所有目的服务器的ping回应。
即过程改造如下:
创建socket -> 构造ping请求1 -> 发送ping请求1 -> 接收ping响应 -> 解析ping响应
构造ping请求2 -> 发送ping请求2 ^ │
............. └──────────┘
构造ping请求n -> 发送ping请求n
在第二步和第三步完成了对所有ping请求的发送,由于是不面向连接的icmp,所以这里不会阻塞,
第四步进入等待任意的ping响应报文到达并立即接收处理,完成后返回继续等待剩下的ping回应,这样整组设备ping的io等待时间就完全并行起来了。
实现时需要注意的是,在一次ping的一组设备数目过大时,比如我们说的20000个设备,可能会出现ping响应风暴,解决的办法是把socket的io缓存开大,以防止缓存溢出导致的丢包:
int hold = 128*1024;
setsockopt(sockRaw, SOL_SOCKET, SO_RECVBUF, &hold, sizeof(int));