socket ICMP ping包发送与响应

项目中需要用到检测设备的外网连接状态,第一时间就想到了ping,平时在电脑上检测网络是否通也是使用ping的,比如:

虽然在C程序中可以直接创建子进程调用shell命令的ping并等待分析返回的打印信息,但是创建进程开销比较大,不适合频繁调用。于是决定使用socket实现ping的功能。

ping是发送ICMP数据包也接收响应数据,可以参考busybox中的ping.c的源码。

1、初始化socket

int sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
if(sockfd!=0){
    perror("RAW socket created error");
    return -1;
}
//不阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

2、发送ICMP数据包

//sendbuf malloc合适的大小
//datalen=56;
//pid_t pid = getpid();

/*发送ping消息*/    
int send_ping(char *sendbuf,int datalen,int sockfd,struct sockaddr_in *dest,pid_t pid)    
{      
    int len=0;
    struct icmp *icmp;
    icmp=(struct icmp*)sendbuf;
    icmp->icmp_type=ICMP_ECHO;   //设置类型为ICMP请求报文
    icmp->icmp_code=0;
    icmp->icmp_cksum=0;
    icmp->icmp_seq=0; //序号
    icmp->icmp_id=pid;            //设置当前进程ID为ICMP标示符

    len=ICMP_HEADSIZE+datalen;
    memset(icmp->icmp_data,0xff,datalen);
    gettimeofday((struct timeval *)icmp->icmp_data,NULL); /* 获取当前时间*/
    icmp->icmp_cksum=cal_chksum( (unsigned short *)icmp,len);

    if(sendto(sockfd,sendbuf,len,0,(struct sockaddr *)dest,sizeof (struct sockaddr))<0) /*经socket传送数据*/   
    {
        printf("error : ping sendto error\n");
        return -1;
    }
    return 0;
}

3、接收响应数据

//recvbuf malloc合适的大小
//timeout_ms 接收超时时间,单位毫米
//GetCurrentTimeStamp 是封装好的一个获取当前时间毫秒的函数,可以使用gettimeofday()函数实现

/*接收程序发出的ping命令的应答*/   
//return 0则表示收到响应数据,ping通了 
int recv_reply(char *recvbuf,int sockfd,struct sockaddr_in *from,struct timeval *recvtime,pid_t pid, int timeout_ms)    
{    
    int         n=0;    
    int         len=0;      
    //当前时间, 单位ms
    unsigned int  tNowTime = GetCurrentTimeStamp();
    unsigned int  tEndTime = tNowTime + timeout_ms;

    int nrecv = 1;   // 
    len = sizeof(struct sockaddr_in);   /*发送ping应答消息的主机IP*/    

    while(nrecv>0)  
    {
        tNowTime = GetCurrentTimeStamp();
        if(tNowTime>tEndTime){
            __ERR("ping time out\n");
            return -1;
        }
        /*经socket接收数据,如果正确接收返回接收到的字节数,失败返回0.*/  
        if((n=recvfrom(sockfd, recvbuf, PING_BUF_SIZE, 0, (struct sockaddr *)from, &len))<=0)  
        {            
            continue;   
        }    

        gettimeofday(recvtime, NULL);   /*记录收到应答的时间*/    

        if(handle_pkt(recvbuf,n,from,recvtime,pid)<0)    /*接收到错误的ICMP应答信息*/    
        {
            continue;    
        }

          nrecv--;   
        break;
    }    

    return 0;
} 

/*ICMP应答消息处理*/    
int handle_pkt(char *recvbuf,int datalen,struct sockaddr_in *from,struct timeval *recvtime,pid_t pid)    
{ 
    struct ip *ip=(struct ip *)recvbuf;
    int iphdrlen=ip->ip_hl<<2;        //求ip报头长度,即ip报头的长度标志乘4
    struct icmp *icmp=(struct icmp *)(recvbuf+iphdrlen);        //越过ip报头,指向ICMP报头
    datalen-=iphdrlen;        //ICMP报头及ICMP数据报的总长度
    if( datalen<8)
        return -1;       
    if((icmp->icmp_type!=ICMP_ECHOREPLY) || (icmp->icmp_id!=pid))
        return -1;    

    printf("recv icmp reply from %s,icmp_id=%d,icmp_seq=%d\n",inet_ntoa(from->sin_addr),icmp->icmp_id,icmp->icmp_seq);
    return 0;
}