Linux C++ 實現一個簡易版的ping (也就是ICMP協議)

xcywt發表於2022-03-29

背景:

想實現一個在沒外網的時候就自動重啟路由器的功能。

又不想用ping命令,因為在程式碼裡呼叫system("ping"); 可能會比較耗時,得單開執行緒。於是找了個實現ICMP協議的程式碼。

 

參考:https://blog.csdn.net/qivan/article/details/7237051

程式碼:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <netdb.h>
#include <string.h> 

#define PACKET_SIZE     4096
#define ERROR           0
#define SUCCESS         1
 
//效驗演算法(百度下有註釋,但是還是看不太明白)
unsigned short cal_chksum(unsigned short *addr, int len)
{
    int nleft=len;
    int sum=0;
    unsigned short *w=addr;
    unsigned short answer=0;
    
    while(nleft > 1)
    {
        sum += *w++;
        nleft -= 2;
    }
    
    if( nleft == 1)
    {       
        *(unsigned char *)(&answer) = *(unsigned char *)w;
        sum += answer;
    }
    
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    answer = ~sum;
    
    return answer;
}
// Ping函式
int ping( char *ips, int timeout)  
{  
    struct timeval *tval;        
    int maxfds = 0;  
    fd_set readfds;  
    
    struct sockaddr_in addr;  
    struct sockaddr_in from;  
    // 設定Ip資訊  
    bzero(&addr,sizeof(addr));  
    addr.sin_family = AF_INET;  

    addr.sin_addr.s_addr = inet_addr(ips);  

#if 1
    int sockfd;  
    // 取得socket  。  如果沒加sudo 這裡會報錯
    sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);  
    if (sockfd < 0)  
    {  
        printf("ip:%s,socket error\n",ips);  
        return ERROR;  
    }  
    
    struct timeval timeo;  
    // 設定TimeOut時間  
    timeo.tv_sec = timeout / 1000;  
    timeo.tv_usec = timeout % 1000;  
    
    if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo)) == -1)  
    {  
        printf("ip:%s,setsockopt error\n",ips);  
        return ERROR;  
    }  
 
    char sendpacket[PACKET_SIZE];  
    char recvpacket[PACKET_SIZE];  
    // 設定Ping包  
    memset(sendpacket, 0, sizeof(sendpacket));  
    
    pid_t pid;  
    // 取得PID,作為Ping的Sequence ID  
    pid=getpid();  
    
    struct ip *iph;  
    struct icmp *icmp;  
    
  
    icmp=(struct icmp*)sendpacket;  
    icmp->icmp_type=ICMP_ECHO;  //回顯請求
    icmp->icmp_code=0;  
    icmp->icmp_cksum=0;  
    icmp->icmp_seq=0;  
    icmp->icmp_id=pid; 
    tval= (struct timeval *)icmp->icmp_data;  
    gettimeofday(tval,NULL);  
    icmp->icmp_cksum=cal_chksum((unsigned short *)icmp,sizeof(struct icmp));  //校驗
    
    int n;  
    // 發包 。可以把這個發包挪到迴圈裡面去。 
    n = sendto(sockfd, (char *)&sendpacket, sizeof(struct icmp), 0, (struct sockaddr *)&addr, sizeof(addr));  
    if (n < 1)  
    {  
        printf("ip:%s,sendto error\n",ips);  
        return ERROR;  
    }  
    
    // 接受  
    // 由於可能接受到其他Ping的應答訊息,所以這裡要用迴圈  
    while(1)  
    {  
        // 設定TimeOut時間,這次才是真正起作用的  
        FD_ZERO(&readfds);  
        FD_SET(sockfd, &readfds);  
        maxfds = sockfd + 1;  
        n = select(maxfds, &readfds, NULL, NULL, &timeo);  
        if (n <= 0)  
        {              
        printf("ip:%s,Time out error\n",ips);  
            close(sockfd);  
            return ERROR;  
        }  
        
        // 接受  
        memset(recvpacket, 0, sizeof(recvpacket));  
        int fromlen = sizeof(from);  
        n = recvfrom(sockfd, recvpacket, sizeof(recvpacket), 0, (struct sockaddr *)&from, (socklen_t *)&fromlen);  
    printf("recvfrom Len:%d\n",n);
        if (n < 1) 
    {  
        return ERROR;  
        }          
     
        char *from_ip = (char *)inet_ntoa(from.sin_addr);  
        // 判斷是否是自己Ping的回覆  
        if (strcmp(from_ip,ips) != 0)  
        {  
            printf("NowPingip:%s Fromip:%s NowPingip is not same to Fromip,so ping wrong!\n",ips,from_ip);  
           return ERROR;
        }  
        
        iph = (struct ip *)recvpacket;  
        
        icmp=(struct icmp *)(recvpacket + (iph->ip_hl<<2));  
        
        printf("ip:%s,icmp->icmp_type:%d,icmp->icmp_id:%d\n",ips,icmp->icmp_type,icmp->icmp_id);  
        // 判斷Ping回覆包的狀態  
        if (icmp->icmp_type == ICMP_ECHOREPLY && icmp->icmp_id == pid)   //ICMP_ECHOREPLY回顯應答
        {  
            // 正常就退出迴圈 
        printf("icmp succecss .............  \n");
            break;  
        }  
        else  
        {  
            // 否則繼續等  
            continue;  
        }  
    } 
#endif
    return SUCCESS;
}
    
int main()
{
#if 1
    char cPing[16];
    printf("Please input ping IP:");
    scanf("%s",cPing);
#else
    char *cPing = "192.168.1.200";    
#endif
    if(ping(cPing,10000))
    {
        printf("Ping succeed!\n");
    }
    else
    {
        printf("Ping wrong!\n");
    }

    return 0;    
}

 

實際效果:

 

補充說明:

0)直接用參考連結上的程式碼時編譯不過,不知道是不是因為我用的是cpp,沒太深究。

1)實際使用的時候需要加上sudo,不然在建立套接字那個地方會報錯。我還沒想好怎麼在程式碼裡用sudo,(因為實際專案執行起來是不需要加sudo的)。

 

相關文章