1. 程式人生 > >Linux網路程式設計---ICMP協議分析及ping程式實現

Linux網路程式設計---ICMP協議分析及ping程式實現

一、IP協議

IP協議是TCP/IP協議族所依賴的傳送機制,提供無連線不可靠的資料報服務。IP的無連線特性意味著每個IP報文都是獨立尋徑的,因此當一個源主機發送多個報文給同一目的主機時,這些報文可能出現錯序,丟失或者部分報文產生錯誤等現象,因此為了保證資料傳送的可靠性,必須在IP層之上通過TCP協議提供有序,帶確認資料的傳輸服務。

1.IP協議格式

IP報文由報文頭部和資料兩部分構成,其中頭部資訊格式如下圖所示,頭部佔20-60個位元組,無選項option時,頭部為20位元組,最多可以攜帶40位元組選項,報文最大長度為65535位元組。


(1)版本(version)  4位元,定義了當前IP協議的版本,目前通常是數字4,即IPV4

(2)頭部長度(ihl)   4位元,按4位元組單位定義IP報文的頭部總長度,因此未攜帶任何選項的IP報文頭部長度為20位元組,則ihl值為5(5*4=20),當選項長度達到最大值40位元組時,ihl長度為15 (15*4=60)。

(3)服務型別(tos)  8位元,用於指示路由器將如何處理IP報文

(4)總長度(tot_len)16位元,報文頭部加資料的總長度,IP報文攜帶的上層資料長度為:資料長度=總長度-頭部長度=總長度-(ihl*4)。之所以需要總長度這個欄位,是因為在某些情況下底層協議為了滿足最小幀長的限制,會新增填充資料,例如以太協議要求每個資料幀最小必須為46位元組,當來自上層的IP報文總長度小於46位元組時,將新增填充資料以滿足最小幀長,於是必須通過總長度這個欄位來記錄實際IP層報文的總長度,參考如圖所示:


(5)報文標識(id)  16位元,用於標識多個IP分段所對應的原始IP分組的ID。

(6)分段標識(frag)3位元,用於宣告一個IP報文是否是某個原始報文的分段,或者宣告是否允許一個IP原始報文被分段。

(7)分段偏移(offset) 13位元,標識一個IP分段的資料在原始IP報文中的偏移值,注意該值,必須是8的整數倍。

(8)生存時間(ttl)   8位元, 一個IP報文在網上所允許的最大生存時間,該值實際為最大跳數,當源主機產生一個IP報文後,該欄位將填寫一個初始值,隨後該報文每經過一個路由器則路由器將對該欄位值進行減一操作,當該欄位值變成0後,路由器將丟棄此報文。

(9)協議(protocol) 8位元,用於標識IP報文承載的上層資料的協議型別,例如可以是TCP,UDP,ICMP和IGMP等。

(10)頭部校驗和(check) 16位元,IP頭部資料的檢驗和。

(11)源地址(saddr) 32位元,報文的源IP地址。

(12)目的地址(daddr)32位元,報文的目的IP地址。

(13)選項(option) 變長且最大不超過40位元組。

2.IP協議頭的c語言定義

struct iphdr
{
#if defined _LITTLE_ENDIAN_BITFIELD //小端機
    u8 hlen:4,
ver: 4;
#elif defined _BIG_ENFIAN_BITFELD  //大端機
    u8 ver:4,
hlen:4;
#endif
    
    u8 tos;
    u16 tot_len;
    u16 id;
    u16 frag_off;
    u8 ttl;
    u8 protocol;
    u16 check;
    u32 saddr;
    u32 daddr;
};

二、ICMP協議

(1)ICMP訊息型別

ICMP訊息分為兩大類,錯誤報告訊息和查詢訊息,這裡僅介紹查詢訊息,每個查詢訊息型別均包括一對請求和應答訊息。


(2)ICMP訊息通用格式

ICMP訊息包括8位元組的頭部和變長資料兩個部分,其中所有訊息型別頭部的前4個位元組均相同,頭部其餘4個位元組隨訊息的不同而不同。如圖所示:


ICMP訊息頭部的頭4個位元組分別是訊息型別tye,訊息程式碼code和校驗和checksum,其中checksum欄位包括頭部和資料兩部分,而並非僅頭部,查詢訊息的資料部分data包含了用於查詢所需要的額外資料。

(3)ICMP查詢請求和應答訊息格式

ICMP迴應請求(echo-request)和應答訊息(echo-reply)用於診斷兩個系統(主機或路由器)之間是否能夠進行通訊,當其中一方傳送迴應請求訊息給另一方時,接收到迴應請求訊息的主機或者路由器將以應答訊息進行應答,常用的網路ping命令就是基於此訊息型別的,如下圖所示 其中type欄位為8表示迴應請求,0表示應答,code欄位暫未是要你管,為0.


(4)ICMP訊息格式的C語言定義

struct icmphdr
{
    u8 type;
    u8 code;
    u16 checksum;
    union
    {
        struct
        {
            u16 id;
            u16 sequence;
        }echo;
        
        u32 gateway;
        struct
        {
            u16 unused;
            u16 mtu;
        }frag; //pmtu發現
    }un;
    
    //u32  icmp_timestamp[2];//時間戳
    //ICMP資料佔位符
    u8 data[0];
#define icmp_id un.echo.id
#define icmp_seq un.echo.sequence
};

ping程式實現:

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<unistd.h>   
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netdb.h>
#include<errno.h>
#include<arpa/inet.h>
#include<signal.h>
#include<netinet/in.h>

#ifndef _LITTLE_ENDIAN_BITFIELD
#define _LITTLE_ENDIAN_BITFIELD
#endif

#define IP_HSIZE sizeof(struct iphdr)   //定義IP_HSIZE為ip頭部長度
#define IPVERSION  4   //定義IPVERSION為4,指出用ipv4



#define ICMP_ECHOREPLY 0 //Echo應答
#define ICMP_ECHO      8 //Echo請求

#define BUFSIZE 1500     //傳送快取最大值
#define DEFAULT_LEN 56   //ping 訊息資料預設大小

//資料類型別名
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

//ICMP訊息頭部

struct icmphdr
{
    u8 type;
    u8 code;
    u16 checksum;
    union
    {
        struct
        {
            u16 id;
            u16 sequence;
        }echo;
        
        u32 gateway;
        struct
        {
            u16 unused;
            u16 mtu;
        }frag; //pmtu發現
    }un;
    u32  icmp_timestamp[2];//時間戳
    //ICMP資料佔位符
    u8 data[0];
#define icmp_id un.echo.id
#define icmp_seq un.echo.sequence
};

#define ICMP_HSIZE sizeof(struct icmphdr)
struct iphdr
{
#if defined _LITTLE_ENDIAN_BITFIELD
    u8 hlen:4,
ver: 4;
#elif defined _BIG_ENFIAN_BITFELD
    u8 ver:4,
hlen:4;
#endif
    
    u8 tos;
    u16 tot_len;
    u16 id;
    u16 frag_off;
    u8 ttl;
    u8 protocol;
    u16 check;
    u32 saddr;
    u32 daddr;
};
char hello[]="hello this is  a ping test.";
char *hostname; //被ping的主機
int  datalen=DEFAULT_LEN;//ICMP訊息攜帶的資料長度
char sendbuf[BUFSIZE];
char recvbuf[BUFSIZE];
int nsent;//傳送的ICMP訊息序號
int nrecv;
pid_t pid;//ping程式的程序pid
struct timeval recvtime; //收到ICMP應答的時間戳
int sockfd; //傳送和接收原始套接字
struct sockaddr_in dest;//被ping主機的ip
struct sockaddr_in from;//傳送ping應答訊息的主機ip

struct sigaction act_alarm;
struct sigaction act_int;


//設定的時間是一個結構體,倒計時設定,重複倒時,超時值設為1秒
struct itimerval val_alarm;

//函式原型
void alarm_handler(int);//SIGALRM處理程式
void int_handler(int);//SIGINT處理程式
void set_sighandler();//設定訊號處理程式
void send_ping();//傳送ping訊息
void recv_reply();//接收ping應答
u16 checksum(u8 *buf,int len);//計算校驗和
int handle_pkt();//ICMP應答訊息處理
void get_statistics(int ,int);//統計ping命令的檢測結果
void bail(const char *);//錯誤報告
int main(int argc,char **argv)  //argc表示隱形程式命令列中引數的數目,argv是一個指向字串陣列指標,其中每一個字元對應一個引數
{
    val_alarm.it_interval.tv_sec = 1;
    val_alarm .it_interval.tv_usec=0;
    val_alarm  .it_value.tv_sec=0;
    val_alarm  .it_value.tv_usec=1;
    struct hostent *host; //該結構體屬於include<netdb.h>
    int on =1;
    
    if((host=gethostbyname(argv[1]))==NULL)
    {    //gethostbyname()返回對應於給定主機名的包含主機名字和地址資訊的結構指標,
        perror("can not understand the host name");   //理解不了輸入的地址
        exit(1);
    }
    
    hostname=argv[1];//取出地址名
    
    memset(&dest,0,sizeof dest);  //將dest中前sizeof(dest)個位元組替換為0並返回s,此處為初始化,給最大記憶體清零
    dest.sin_family=PF_INET;  //PF_INET為IPV4,internet協議,在<netinet/in.h>中,地址族
    dest.sin_port=ntohs(0);   //埠號,ntohs()返回一個以主機位元組順序表達的數。
    dest.sin_addr=*(struct in_addr *)host->h_addr_list[0];//host->h_addr_list[0]是地址的指標.返回IP地址,初始化
    
    if((sockfd = socket(PF_INET,SOCK_RAW,IPPROTO_ICMP))<0)
    { //PF_INEI套接字協議族,SOCK_RAW套接字型別,IPPROTO_ICMP使用協議,呼叫socket函式來建立一個能夠進行網路通訊的套接字。這裡判斷是否建立成功
        perror("raw socket created error");
        exit(1);
    }
    
    setuid(getuid());//getuid()函式返回一個呼叫程式的真實使用者ID,setuid()是讓普通使用者可以以root使用者的角色執行只有root帳號才能執行的程式或命令。
    pid=getpid(); //getpid函式用來取得目前程序的程序識別碼
    printf("PID:%d\n",pid);
    set_sighandler();//對訊號處理
    printf("Ping %s(%s): %d bytes data in ICMP packets.\n",argv[1],inet_ntoa(dest.sin_addr),datalen);
    
    if((setitimer(ITIMER_REAL,&val_alarm,NULL))==-1) //定時函式
        bail("setitimer fails.");
    
    
    recv_reply();//接收ping應答
    
    return 0;
}
//傳送ping訊息
void send_ping()
{
    struct iphdr *ip_hdr;   //iphdr為IP頭部結構體
    struct icmphdr *icmp_hdr;   //icmphdr為ICMP頭部結構體
    int len;
    int len1;
    icmp_hdr=(struct icmphdr *)(sendbuf);  //字串指標
    icmp_hdr->type=ICMP_ECHO;    //初始化ICMP訊息型別type
    icmp_hdr->code=0;    //初始化訊息程式碼code
    icmp_hdr->icmp_id=pid;   //把程序標識碼初始給icmp_id
    icmp_hdr->icmp_seq=nsent++;  //傳送的ICMP訊息序號賦值給icmp序號
    gettimeofday((struct timeval *)icmp_hdr->icmp_timestamp,NULL); // 獲取當前時間
    memcpy(icmp_hdr->data, hello, strlen(hello));
    
    len=ICMP_HSIZE+strlen(hello);
    icmp_hdr->checksum=0;    //初始化
    icmp_hdr->checksum=checksum((u8 *)icmp_hdr,len);  //計算校驗和
    
  //  printf("The send pack checksum is:0x%x\n",icmp_hdr->checksum);
    sendto(sockfd,sendbuf,len,0,(struct sockaddr *)&dest,sizeof (dest)); //經socket傳送資料
}
//接收程式發出的ping命令的應答
void recv_reply()
{
    int n;
    socklen_t len;
    int errno;
    
    n=nrecv=0;
    len=sizeof(from);   //傳送ping應答訊息的主機IP
    
    while(nrecv<4)
    {
        if((n=recvfrom(sockfd,recvbuf,sizeof recvbuf,0,(struct sockaddr *)&from,&len))<0)
        { //經socket接收資料,如果正確接收返回接收到的位元組數,失敗返回0.
            if(errno==EINTR)  //EINTR表示訊號中斷
                continue;
            bail("recvfrom error");
        }
        
        gettimeofday(&recvtime,NULL);   //記錄收到應答的時間
        
        if(handle_pkt())    //接收到錯誤的ICMP應答資訊
            
            continue;
        nrecv++;
    }
    
    get_statistics(nsent,nrecv);     //統計ping命令的檢測結果
}
//計算校驗和
u16 checksum(u8 *buf,int len)
{
    u32 sum=0;
    u16 *cbuf;
    
    cbuf=(u16 *)buf;
    
    while(len>1)
    {
        sum+=*cbuf++;
        len-=2;
    }
    
    if(len)
        sum+=*(u8 *)cbuf;
    
    sum=(sum>>16)+(sum & 0xffff);
    sum+=(sum>>16);
    
    return ~sum;
}
//ICMP應答訊息處理
int handle_pkt()
{
    struct iphdr *ip;
    struct icmphdr *icmp;
    
    int ip_hlen;
    u16 ip_datalen; //ip資料長度
    double rtt; // 往返時間
    struct timeval *sendtime;
    
    ip=(struct iphdr *)recvbuf;
    
    ip_hlen=ip->hlen << 2;
    ip_datalen=ntohs(ip->tot_len)-ip_hlen;
    
    icmp=(struct icmphdr *)(recvbuf+ip_hlen);
    
    u16 sum=(u16)checksum((u8 *)icmp,ip_datalen);
   // printf("The recv pack checksum is:0x%x\n",sum);
    if(sum) //計算校驗和
        return -1;
    
    
    if(icmp->icmp_id!=pid)
        return -1;
    if(icmp->type!=ICMP_ECHOREPLY)
        return -1;
    
    sendtime=(struct timeval *)icmp->icmp_timestamp; //傳送時間
    rtt=((&recvtime)->tv_sec-sendtime->tv_sec)*1000+((&recvtime)->tv_usec-sendtime->tv_usec)/1000.0;// 往返時間
    //列印結果
    printf("%d bytes from %s:icmp_seq=%u ttl=%d rtt=%.3f ms\n",
           ip_datalen, //IP資料長度
           inet_ntoa(from.sin_addr),    //目的ip地址
           icmp->icmp_seq, //icmp報文序列號
           ip->ttl,  //生存時間
           rtt);    //往返時間
    
    return 0;
}
//設定訊號處理程式
void set_sighandler()
{
    act_alarm.sa_handler=alarm_handler;
    if(sigaction(SIGALRM,&act_alarm,NULL)==-1)  //sigaction()會依引數signum指定的訊號編號來設定該訊號的處理函式。引數signum指所要捕獲訊號或忽略的訊號,&act代表新設定的訊號共用體,NULL代表之前設定的訊號處理結構體。這裡判斷對訊號的處理是否成功。
        bail("SIGALRM handler setting fails.");
    
    act_int.sa_handler=int_handler;
    if(sigaction(SIGINT,&act_int,NULL)==-1)
        bail("SIGALRM handler setting fails.");
}
//統計ping命令的檢測結果
void get_statistics(int nsent,int nrecv)
{
    printf("--- %s ping statistics ---\n",inet_ntoa(dest.sin_addr)); //將網路地址轉換成“.”點隔的字串格式。
    printf("%d packets transmitted, %d received, %0.0f%% ""packet loss\n",
           nsent,nrecv,1.0*(nsent-nrecv)/nsent*100);
}
//錯誤報告
void bail(const char * on_what)
{
    fputs(strerror(errno),stderr);  //:向指定的檔案寫入一個字串(不寫入字串結束標記符‘\0’)。成功寫入一個字串後,檔案的位置指標會自動後移,函式返回值為0;否則返回EOR(符號常量,其值為-1)。
    fputs(":",stderr);
    fputs(on_what,stderr);
    fputc('\n',stderr); //送一個字元到一個流中
    exit(1);
}

//SIGINT(中斷訊號)處理程式
void int_handler(int sig)
{
    get_statistics(nsent,nrecv);    //統計ping命令的檢測結果
    close(sockfd);  //關閉網路套接字
    exit(1);
}
//SIGALRM(終止程序)處理程式
void alarm_handler(int signo)
{
    send_ping();    //傳送ping訊息
}

程式結果(注意這裡的PID):

下面我們在對我們寫的ping程式進行抓包分析

這是通過抓包抓到的我們發的ICMP請求包,當然還有ICMP應答報,可以先看看後面的id,和seq,後面我們會再次提到,首先我們對其中一個包展開分析:

這裡顯示的是IP報文欄位,可以看到我們前面所展示的IP報頭裡面所包含的東西,比較簡單,就不一一分析了。

然後我們在來看看我我們ICMP報文:

可以看到,搜下是型別type=8,說明是個ICMP請求,code=0,然後是校驗和,接下來是識別符號,這裡怎麼有兩個呢,其實一個是大端表示,一個是小端表示的結果,這裡的識別符號是我們的程序ID,可以看我前面ping的時候輸出了程序ID,然後是序列號,可以對照幾個包分析,開始序列號(大端和小端表示)是為0,然後一次遞增,這是我們在程式裡面所設定的。接下來是時間戳,開始的時候icmp結構裡面我沒有放時間戳,這樣系統會放在資料位置,我來我單獨放了一個時間戳結構這樣把它和data分開,關於時間戳大小我也是抓包分析得出來的,是8個位元組。接下來便是我們的資料部分,為了很好的區分資料區域和前面的頭,特別把資料部分拿出來分析:

可以看到資料部分恰好是從我們自己定義的資料地方開始的,資料前面就是時間戳,開始除錯的時候,資料總是被覆蓋,後來才找出了裡面的時間戳佔了8個位元組。所以調整後剛好合適。當然不同的型別和程式碼會導致後面的結構有些不一樣,這需要調整。

上面的程式碼中ICMP和IP都是我們自己定義的。系統中也有提供相應的結構,我們直接呼叫就可以了,不過必須先檢視裡面結構是如何定義的。在我這裡,ICMP中的資料是單獨定義在外面的,不是一起放在裡面的,所以,資料部分需要自己宣告一個結構,具體名稱可以檢視自己系統裡面多對應的標頭檔案。

我這上面是這樣的:

struct icmp_filter *icmp_data;

注意一下下面幾句:
    icmp_data=(struct icmp_filter*)(sendbuf+ICMP_HSIZE);
    gettimeofday((struct timeval *)icmp_data,NULL); // 獲取當前時間
    icmp_data=(struct icmp_filter*)(sendbuf+ICMP_HSIZE+sizeof(timeval)); //真正資料地方
    memcpy(icmp_data, hello, strlen(hello));
    icmp_data=(struct icmp_filter*)(sendbuf+ICMP_HSIZE); //恢復資料指標

首先找到資料開始相應的位置,然後在開始的地方放時間戳,然後在找到真正資料開始的地方,寫入資料,不要隨意移動資料頭指標,否則結果會有異常的。

下面是其實現:

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netdb.h>
#include<errno.h>
#include<arpa/inet.h>
#include<signal.h>
#include<netinet/in.h>
#include<linux/ip.h>
#include <linux/icmp.h>


#ifndef _LITTLE_ENDIAN_BITFIELD
#define _LITTLE_ENDIAN_BITFIELD
#endif

#define IP_HSIZE sizeof(struct iphdr)   //定義IP_HSIZE為ip頭部長度
#define IPVERSION  4   //定義IPVERSION為4,指出用ipv4
#define ICMP_HSIZE sizeof(struct icmphdr)


#define ICMP_ECHOREPLY 0 //Echo應答
#define ICMP_ECHO      8 //Echo請求

#define BUFSIZE 1500     //傳送快取最大值
#define DEFAULT_LEN 56   //ping 訊息資料預設大小

//資料類型別名
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

char hello[]="hello this is  a ping test.";
struct icmp_filter *icmp_data;

char *hostname; //被ping的主機
int  datalen=DEFAULT_LEN;//ICMP訊息攜帶的資料長度
char sendbuf[BUFSIZE];
char recvbuf[BUFSIZE];
int nsent;//傳送的ICMP訊息序號
int nrecv;
pid_t pid;//ping程式的程序pid
struct timeval recvtime; //收到ICMP應答的時間戳
int sockfd; //傳送和接收原始套接字
struct sockaddr_in dest;//被ping主機的ip
struct sockaddr_in from;//傳送ping應答訊息的主機ip

struct sigaction act_alarm;
struct sigaction act_int;


//設定的時間是一個結構體,倒計時設定,重複倒時,超時值設為1秒
struct itimerval val_alarm;

//函式原型
void alarm_handler(int);//SIGALRM處理程式
void int_handler(int);//SIGINT處理程式
void set_sighandler();//設定訊號處理程式
void send_ping();//傳送ping訊息
void recv_reply();//接收ping應答
u16 checksum(u8 *buf,int len);//計算校驗和
int handle_pkt();//ICMP應答訊息處理
void get_statistics(int ,int);//統計ping命令的檢測結果
void bail(const char *);//錯誤報告
int main(int argc,char **argv)  //argc表示隱形程式命令列中引數的數目,argv是一個指向字串陣列指標,其中每一個字元對應一個引數
{
    val_alarm.it_interval.tv_sec = 1;
    val_alarm .it_interval.tv_usec=0;
    val_alarm  .it_value.tv_sec=0;
    val_alarm  .it_value.tv_usec=1;
    struct hostent *host; //該結構體屬於include<netdb.h>
    int on =1;
    
    if(argc<2){      //判斷是否輸入了地址
        printf("Usage: %s hostname\n",argv[0]);
        exit(1);
    }
    
    if((host=gethostbyname(argv[1]))==NULL)
    {    //gethostbyname()返回對應於給定主機名的包含主機名字和地址資訊的結構指標,
        perror("can not understand the host name");   //理解不了輸入的地址
        exit(1);
    }
    
    hostname=argv[1];//取出地址名
    
    memset(&dest,0,sizeof dest);  //將dest中前sizeof(dest)個位元組替換為0並返回s,此處為初始化,給最大記憶體清零
    dest.sin_family=PF_INET;  //PF_INET為IPV4,internet協議,在<netinet/in.h>中,地址族
    dest.sin_port=ntohs(0);   //埠號,ntohs()返回一個以主機位元組順序表達的數。
    dest.sin_addr=*(struct in_addr *)host->h_addr_list[0];//host->h_addr_list[0]是地址的指標.返回IP地址,初始化
    
    if((sockfd = socket(PF_INET,SOCK_RAW,IPPROTO_ICMP))<0)
    { //PF_INEI套接字協議族,SOCK_RAW套接字型別,IPPROTO_ICMP使用協議,呼叫socket函式來建立一個能夠進行網路通訊的套接字。這裡判斷是否建立成功
        perror("raw socket created error");
        exit(1);
    }
    
    setuid(getuid());//getuid()函式返回一個呼叫程式的真實使用者ID,setuid()是讓普通使用者可以以root使用者的角色執行只有root帳號才能執行的程式或命令。
    pid=getpid(); //getpid函式用來取得目前程序的程序識別碼
    printf("PID:%d\n",pid);
    set_sighandler();//對訊號處理
    printf("Ping %s(%s): %d bytes data in ICMP packets.\n",argv[1],inet_ntoa(dest.sin_addr),datalen);
    
    if((setitimer(ITIMER_REAL,&val_alarm,NULL))==-1) //定時函式
        bail("setitimer fails.");
    
    
    recv_reply();//接收ping應答
    
    return 0;
}
//傳送ping訊息
void send_ping()
{
    
    struct ip *ip_hdr;   //iphdr為IP頭部結構體
    struct icmphdr *icmp_hdr;   //icmphdr為ICMP頭部結構體
    int len;
    int len1;
    icmp_hdr=(struct icmphdr *)(sendbuf);  //字串指標
    icmp_hdr->type=ICMP_ECHO;//初始化ICMP訊息型別type
    
    icmp_hdr->code=0;    //初始化訊息程式碼code
    icmp_hdr->un.echo.id=pid;   //把程序標識碼初始給icmp_id
    icmp_hdr->un.echo.sequence=nsent++;  //傳送的ICMP訊息序號賦值給icmp序號
    
    
    icmp_data=(struct icmp_filter*)(sendbuf+ICMP_HSIZE);
    gettimeofday((struct timeval *)icmp_data,NULL); // 獲取當前時間
    icmp_data=(struct icmp_filter*)(sendbuf+ICMP_HSIZE+sizeof(timeval)); //真正資料地方
    memcpy(icmp_data, hello, strlen(hello));
    icmp_data=(struct icmp_filter*)(sendbuf+ICMP_HSIZE); //恢復資料指標
    len=ICMP_HSIZE+sizeof(icmp_filter)+strlen(hello);
    icmp_hdr->checksum=0;    //初始化
    icmp_hdr->checksum=checksum((u8 *)icmp_hdr,len);  //計算校驗和
    
    printf("The send pack checksum is:0x%x\n",icmp_hdr->checksum);
    sendto(sockfd,sendbuf,len,0,(struct sockaddr *)&dest,sizeof (dest)); //經socket傳送資料
}
//接收程式發出的ping命令的應答
void recv_reply()
{
    int n;
    socklen_t len;
    int errno;
    
    n=nrecv=0;
    len=sizeof(from);   //傳送ping應答訊息的主機IP
    
    while(nrecv<4)
    {
        if((n=recvfrom(sockfd,recvbuf,sizeof recvbuf,0,(struct sockaddr *)&from,&len))<0)
        { //經socket接收資料,如果正確接收返回接收到的位元組數,失敗返回0.
            if(errno==EINTR)  //EINTR表示訊號中斷
                continue;
            bail("recvfrom error");
        }
        
        gettimeofday(&recvtime,NULL);   //記錄收到應答的時間
        
        if(handle_pkt())    //接收到錯誤的ICMP應答資訊
            
            continue;
        nrecv++;
    }
    
    get_statistics(nsent,nrecv);     //統計ping命令的檢測結果
}
//計算校驗和
u16 checksum(u8 *buf,int len)
{
    u32 sum=0;
    u16 *cbuf;
    
    cbuf=(u16 *)buf;
    
    while(len>1)
    {
        sum+=*cbuf++;
        len-=2;
    }
    
    if(len)
        sum+=*(u8 *)cbuf;
    
    sum=(sum>>16)+(sum & 0xffff);
    sum+=(sum>>16);
    
    return ~sum;
}
//ICMP應答訊息處理
int handle_pkt()
{
    struct iphdr *ip;
    struct icmphdr *icmp;
    
    int ip_hlen;
    u16 ip_datalen; //ip資料長度
    double rtt; // 往返時間
    struct timeval *sendtime;
    
    ip=(struct iphdr *)recvbuf;
    
    ip_hlen=ip->ihl << 2;
    ip_datalen=ntohs(ip->tot_len)-ip_hlen;
    
    icmp=(struct icmphdr *)(recvbuf+ip_hlen);
    
    u16 sum=(u16)checksum((u8 *)icmp,ip_datalen);
    printf("The recv pack checksum is:0x%x\n",sum);
    if(sum) //計算校驗和
        return -1;
    
    
    if(icmp->un.echo.id!=pid)
        return -1;
    if(icmp->type!=ICMP_ECHOREPLY)
        return -1;
    
    sendtime=(struct timeval *)icmp_data; //傳送時間
    rtt=((&recvtime)->tv_sec-sendtime->tv_sec)*1000+((&recvtime)->tv_usec-sendtime->tv_usec)/1000.0;// 往返時間
    //列印結果
    printf("%d bytes from %s:icmp_seq=%u ttl=%d rtt=%.3f ms\n",
           ip_datalen, //IP資料長度
           inet_ntoa(from.sin_addr),    //目的ip地址
           icmp->un.echo.sequence, //icmp報文序列號
           ip->ttl,  //生存時間
           rtt);    //往返時間
    
    return 0;
}
//設定訊號處理程式
void set_sighandler()
{
    act_alarm.sa_handler=alarm_handler;
    if(sigaction(SIGALRM,&act_alarm,NULL)==-1)  //sigaction()會依引數signum指定的訊號編號來設定該訊號的處理函式。引數signum指所要捕獲訊號或忽略的訊號,&act代表新設定的訊號共用體,NULL代表之前設定的訊號處理結構體。這裡判斷對訊號的處理是否成功。
        bail("SIGALRM handler setting fails.");
    
    act_int.sa_handler=int_handler;
    if(sigaction(SIGINT,&act_int,NULL)==-1)
        bail("SIGALRM handler setting fails.");
}
//統計ping命令的檢測結果
void get_statistics(int nsent,int nrecv)
{
    printf("--- %s ping statistics ---\n",inet_ntoa(dest.sin_addr)); //將網路地址轉換成“.”點隔的字串格式。
    printf("%d packets transmitted, %d received, %0.0f%% ""packet loss\n",
           nsent,nrecv,1.0*(nsent-nrecv)/nsent*100);
}
//錯誤報告
void bail(const char * on_what)
{
    fputs(strerror(errno),stderr);  //:向指定的檔案寫入一個字串(不寫入字串結束標記符‘\0’)。成功寫入一個字串後,檔案的位置指標會自動後移,函式返回值為0;否則返回EOR(符號常量,其值為-1)。
    fputs(":",stderr);
    fputs(on_what,stderr);
    fputc('\n',stderr); //送一個字元到一個流中
    exit(1);
}

//SIGINT(中斷訊號)處理程式
void int_handler(int sig)
{
    get_statistics(nsent,nrecv);    //統計ping命令的檢測結果
    close(sockfd);  //關閉網路套接字
    exit(1);
}
//SIGALRM(終止程序)處理程式
void alarm_handler(int signo)
{
    send_ping();    //傳送ping訊息
    
}

測試平臺:Ubuntu 14.04 +gcc 4.8.4

下面是改良版,刪除了沒必要的東西,重寫了部分程式碼,看著要好很多,也很好理解:

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netdb.h>
#include<errno.h>
#include<arpa/inet.h>
#include<signal.h>
#include<netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netinet/in_systm.h>
#define BUFSIZE 1500     //傳送快取最大值

//資料類型別名
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

char hello[]="hello this is  a ping test.";

char *hostname; //被ping的主機
int  datalen=56;//ICMP訊息攜帶的資料長度
char sendbuf[BUFSIZE];
char recvbuf[BUFSIZE];
int nsent;//傳送的ICMP訊息序號
int nrecv;
pid_t pid;//ping程式的程序pid
struct timeval recvtime; //收到ICMP應答的時間戳
int sockfd; //傳送和接收原始套接字
struct sockaddr_in dest;//被ping主機的ip
struct sockaddr_in from;//傳送ping應答訊息的主機ip

struct sigaction act_alarm;
struct sigaction act_int;


//設定的時間是一個結構體,倒計時設定,重複倒時,超時值設為1秒
struct itimerval val_alarm;

//函式原型
void alarm_handler(int);//SIGALRM處理程式
void int_handler(int);//SIGINT處理程式
void set_sighandler();//設定訊號處理程式
void send_ping();//傳送ping訊息
void recv_reply();//接收ping應答
u16 checksum(u8 *buf,int len);//計算校驗和
int handle_pkt(int len);//ICMP應答訊息處理
void get_statistics(int ,int);//統計ping命令的檢測結果
void bail(const char *);//錯誤報告
int main(int argc,char **argv)  //argc表示隱形程式命令列中引數的數目,argv是一個指向字串陣列指標,其中每一個字元對應一個引數
{
    val_alarm.it_interval.tv_sec = 1;
    val_alarm .it_interval.tv_usec=0;
    val_alarm  .it_value.tv_sec=0;
    val_alarm  .it_value.tv_usec=1;
    struct hostent *host; //該結構體屬於include<netdb.h>
    int on =1;
    
    if(argc<2){      //判斷是否輸入了地址
        printf("Usage: %s hostname\n",argv[0]);
        exit(1);
    }
    
    if((host=gethostbyname(argv[1]))==NULL)
    {    //gethostbyname()返回對應於給定主機名的包含主機名字和地址資訊的結構指標,
        perror("can not understand the host name");   //理解不了輸入的地址
        exit(1);
    }
    
    hostname=argv[1];//取出地址名
    
    memset(&dest,0,sizeof dest);  //將dest中前sizeof(dest)個位元組替換為0並返回s,此處為初始化,給最大記憶體清零
    dest.sin_family=PF_INET;  //PF_INET為IPV4,internet協議,在<netinet/in.h>中,地址族
    dest.sin_port=ntohs(0);   //埠號,ntohs()返回一個以主機位元組順序表達的數。
    dest.sin_addr=*(struct in_addr *)host->h_addr_list[0];//host->h_addr_list[0]是地址的指標.返回IP地址,初始化
    
    if((sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))<0)
    { //PF_INEI套接字協議族,SOCK_RAW套接字型別,IPPROTO_ICMP使用協議,呼叫socket函式來建立一個能夠進行網路通訊的套接字。這裡判斷是否建立成功
        perror("raw socket created error");
        exit(1);
    }
    
    setuid(getuid());//getuid()函式返回一個呼叫程式的真實使用者ID,setuid()是讓普通使用者可以以root使用者的角色執行只有root帳號才能執行的程式或命令。
    pid=getpid(); //getpid函式用來取得目前程序的程序識別碼
    printf("PID:%d\n",pid);
    set_sighandler();//對訊號處理
    printf("Ping %s(%s): %d bytes data in ICMP packets.\n",argv[1],inet_ntoa(dest.sin_addr),datalen);
    
    if((setitimer(ITIMER_REAL,&val_alarm,NULL))==-1) //定時函式
        bail("setitimer fails.");
    
    
    recv_reply();//接收ping應答
    
    return 0;
}
//傳送ping訊息
void send_ping()
{
    
    struct ip *ip;   //ip為IP頭部結構體
    struct icmp *icmp;   //icmp為ICMP頭部結構體
    int len;
    int len1;
    icmp=(struct icmp *)(sendbuf);  //字串指標
    icmp->icmp_type=ICMP_ECHO;//初始化ICMP訊息型別type
    
    icmp->icmp_code=0;    //初始化訊息程式碼code
    icmp->icmp_id=pid;   //把程序標識碼初始給icmp_id
    icmp->icmp_seq=nsent++;  //傳送的ICMP訊息序號賦值給icmp序號
    gettimeofday((struct timeval *)icmp->icmp_data,NULL); // 獲取當前時間
    memcpy(icmp->icmp_data+sizeof(timeval), hello, strlen(hello));
    len=8+sizeof(timeval)+strlen(hello);
    printf("%d\n",len);
    icmp->icmp_cksum=0;    //初始化
    icmp->icmp_cksum=checksum((u8 *)icmp,len);  //計算校驗和
    sendto(sockfd,sendbuf,len,0,(struct sockaddr *)&dest,sizeof (dest)); //經socket傳送資料
}
//接收程式發出的ping命令的應答
void recv_reply()
{
    int n;
    socklen_t len;
    int errno;
    n=nrecv=0;
    
    while(nrecv<4)
    {
        if((n=recvfrom(sockfd,recvbuf,sizeof recvbuf,0,(struct sockaddr *)&from,&len))<0)
        { //經socket接收資料,如果正確接收返回接收到的位元組數,失敗返回0.
            if(errno==EINTR)  //EINTR表示訊號中斷
                continue;
            bail("recvfrom error");
        }
        
        gettimeofday(&recvtime,NULL);   //記錄收到應答的時間
        
        if(handle_pkt(n))    //接收到錯誤的ICMP應答資訊
            
            continue;
        nrecv++;
    }
    
    get_statistics(nsent,nrecv);     //統計ping命令的檢測結果
}
//計算校驗和
u16 checksum(u8 *buf,int len)
{
    u32 sum=0;
    u16 *cbuf;
    
    cbuf=(u16 *)buf;
    
    while(len>1)
    {
        sum+=*cbuf++;
        len-=2;
    }
    
    if(len)
        sum+=*(u8 *)cbuf;
    
    sum=(sum>>16)+(sum & 0xffff);
    sum+=(sum>>16);
    
    return ~sum;
}
//ICMP應答訊息處理
int handle_pkt(int len)
{
    struct ip *ip;
    struct icmp *icmp;
    
    int ip_hlen,icmplen;
    double rtt; // 往返時間
    struct timeval *sendtime;
    
    ip=(struct ip *)recvbuf;
    
    ip_hlen=ip->ip_hl<< 2;
    icmp=(struct icmp *)(recvbuf+ip_hlen);
    icmplen=len-ip_hlen;
    if(icmp->icmp_id!=pid)
        return -1;
    if(icmp->icmp_type!=ICMP_ECHOREPLY)
        return -1;
    
    sendtime=(struct timeval *)icmp->icmp_data; //傳送時間
    
    if((recvtime.tv_usec-=sendtime->tv_usec)<0)
    {
        recvtime.tv_sec--;
        recvtime.tv_usec+=1000000;
    }
    recvtime.tv_sec-=sendtime->tv_sec;
    
    rtt=recvtime.tv_sec*1000.0+recvtime.tv_usec/1000.0;// 往返時間
    //列印結果
    printf("%d bytes from %s:icmp_seq=%u ttl=%d rtt=%.3f ms\n",
           icmplen, //icmp資料長度
           inet_ntoa(from.sin_addr),    //目的ip地址
           icmp->icmp_seq, //icmp報文序列號
           ip->ip_ttl,  //生存時間
           rtt);    //往返時間
    
    return 0;
}
//設定訊號處理程式
void set_sighandler()
{
    act_alarm.sa_handler=alarm_handler;
    if(sigaction(SIGALRM,&act_alarm,NULL)==-1)  //sigaction()會依引數signum指定的訊號編號來設定該訊號的處理函式。引數signum指所要捕獲訊號或忽略的訊號,&act代表新設定的訊號共用體,NULL代表之前設定的訊號處理結構體。這裡判斷對訊號的處理是否成功。
        bail("SIGALRM handler setting fails.");
    
    act_int.sa_handler=int_handler;
    if(sigaction(SIGINT,&act_int,NULL)==-1)
        bail("SIGALRM handler setting fails.");
}
//統計ping命令的檢測結果
void get_statistics(int nsent,int nrecv)
{
    printf("--- %s ping statistics ---\n",inet_ntoa(dest.sin_addr)); //將網路地址轉換成“.”點隔的字串格式。
    printf("%d packets transmitted, %d received, %0.0f%% ""packet loss\n",
           nsent,nrecv,1.0*(nsent-nrecv)/nsent*100);
}
//錯誤報告
void bail(const char * on_what)
{
    fputs(strerror(errno),stderr);  //:向指定的檔案寫入一個字串(不寫入字串結束標記符‘\0’)。成功寫入一個字串後,檔案的位置指標會自動後移,函式返回值為0;否則返回EOR(符號常量,其值為-1)。
    fputs(":",stderr);
    fputs(on_what,stderr);
    fputc('\n',stderr); //送一個字元到一個流中
    exit(1);
}

//SIGINT(中斷訊號)處理程式
void int_handler(int sig)
{
    get_statistics(nsent,nrecv);    //統計ping命令的檢測結果
    close(sockfd);  //關閉網路套接字
    exit(1);
}
//SIGALRM(終止程序)處理程式
void alarm_handler(int signo)
{
    send_ping();    //傳送ping訊息
    
}