异步ping的实现,如何在10秒内ping完20000个设备

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));

点赞