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訊息
}