運用C語言編寫模擬常用網絡命令ping命令實現一個基於linux原始套接字和ICMP協議的ping程序。該程序能用於檢測主機或路由器工作是否正常。
程序中主要的函數
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 *); /*錯誤報告*/
頭文件:ping.h
- #define ICMP_ECHOREPLY 0 /* Echo應答*/
- #define ICMP_ECHO /*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 unsed;
- u16 mtu;
- }frag; /*pmtu實現*/
- }un;
- /*ICMP數據佔位符*/
- u8 data[0];
- #define icmp_id un.echo.id
- #define icmp_seq un.echo.sequence
- };
- #define ICMP_HSIZE sizeof(struct icmphdr)
- /*定義一個IP消息頭部結構體*/
- struct iphdr {
- u8 hlen:4, ver:4; /*定義4位首部長度,和IP版本號爲IPV4*/
- u8 tos; /*8位服務類型TOS*/
- u16 tot_len; /*16位總長度*/
- u16 id; /*16位標誌位*/
- u16 frag_off; /*3位標誌位*/
- u8 ttl; /*8位生存週期*/
- u8 protocol; /*8位協議*/
- u16 check; /*16位IP首部校驗和*/
- u32 saddr; /*32位源IP地址*/
- u32 daddr; /*32位目的IP地址*/
- };
- char *hostname; /*被ping的主機名*/
- int datalen = DEFAULT_LEN; /*ICMP消息攜帶的數據長度*/
- char sendbuf[BUFSIZE]; /*發送字符串數組*/
- char recvbuf[BUFSIZE]; /*接收字符串數組*/
- int nsent; /*發送的ICMP消息序號*/
- int nrecv; /*接收的ICMP消息序號*/
- 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;
- /*函數原型*/
- 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 *); /*錯誤報告*/
源程序2:ping.c
- #include<stdio.h>
- #include<stdlib.h>
- #include<sys/time.h> /*是Linux系統的日期時間頭文件*/
- #include<unistd.h> /* 是POSIX標準定義的unix類系統定義符號常量的頭文件,包含了許多UNIX系統服務的函數原型,例如read函數、write函數和getpid函數*/
- #include<string.h>
- #include<sys/socket.h> /*對與引用socket函數必須*/
- #include<sys/types.h>
- #include<netdb.h> /*定義了與網絡有關的結構,變量類型,宏,函數。函數gethostbyname()用*/
- #include<errno.h> /*sys/types.h中文名稱爲基本系統數據類型*/
- #include<arpa/inet.h> /*inet_ntoa()和inet_addr()這兩個函數,包含在 arpa/inet.h*/
- #include<signal.h> /*進程對信號進行處理*/
- #include<netinet/in.h> /*互聯網地址族*/
- #include"ping.h"
- #define IP_HSIZE sizeof(struct iphdr) /*定義IP_HSIZE爲ip頭部長度*/
- #define IPVERSION 4 /*定義IPVERSION爲4,指出用ipv4*/
- /*設置的時間是一個結構體,倒計時設置,重複倒時,超時值設爲1秒*/
- struct itimerval val_alarm={.it_interval.tv_sec = 1,
- .it_interval.tv_usec=0,
- .it_value.tv_sec=0,
- .it_value.tv_usec=1
- };
- int main(int argc,char **argv) /*argc表示隱形程序命令行中參數的數目,argv是一個指向字符串數組指針,其中每一個字符對應一個參數*/
- {
- 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);
- }
- setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on)); /*設置當前套接字選項特定屬性值,sockfd套接字,IPPROTO_IP協議層爲IP層,IP_HDRINCL套接字選項條目,套接字接收緩衝區指針,sizeof(on)緩衝區長度的長度*/
- setuid(getuid());/*getuid()函數返回一個調用程序的真實用戶ID,setuid()是讓普通用戶可以以root用戶的角色運行只有root帳號才能運行的程序或命令。*/
- pid=getpid(); /*getpid函數用來取得目前進程的進程識別碼*/
- 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(void)
- {
- struct iphdr *ip_hdr; /*iphdr爲IP頭部結構體*/
- struct icmphdr *icmp_hdr; /*icmphdr爲ICMP頭部結構體*/
- int len;
- int len1;
- /*ip頭部結構體變量初始化*/
- ip_hdr=(struct iphdr *)sendbuf; /*字符串指針*/
- ip_hdr->hlen=sizeof(struct iphdr)>>2; /*頭部長度*/
- ip_hdr->ver=IPVERSION; /*版本*/
- ip_hdr->tos=0; /*服務類型*/
- ip_hdr->tot_len=IP_HSIZE+ICMP_HSIZE+datalen; /*報文頭部加數據的總長度*/
- ip_hdr->id=0; /*初始化報文標識*/
- ip_hdr->frag_off=0; /*設置flag標記爲0*/
- ip_hdr->protocol=IPPROTO_ICMP;/*運用的協議爲ICMP協議*/
- ip_hdr->ttl=255; /*一個封包在網絡上可以存活的時間*/
- ip_hdr->daddr=dest.sin_addr.s_addr; /*目的地址*/
- len1=ip_hdr->hlen<<2; /*ip數據長度*/
- /*ICMP頭部結構體變量初始化*/
- icmp_hdr=(struct icmphdr *)(sendbuf+len1); /*字符串指針*/
- icmp_hdr->type=8; /*初始化ICMP消息類型type*/
- icmp_hdr->code=0; /*初始化消息代碼code*/
- icmp_hdr->icmp_id=pid; /*把進程標識碼初始給icmp_id*/
- icmp_hdr->icmp_seq=nsent++; /*發送的ICMP消息序號賦值給icmp序號*/
- memset(icmp_hdr->data,0xff,datalen); /*將datalen中前datalen個字節替換爲0xff並返回icmp_hdr-dat*/
- gettimeofday((struct timeval *)icmp_hdr->data,NULL); /* 獲取當前時間*/
- len=ip_hdr->tot_len; /*報文總長度賦值給len變量*/
- icmp_hdr->checksum=0; /*初始化*/
- icmp_hdr->checksum=checksum((u8 *)icmp_hdr,len); /*計算校驗和*/
- sendto(sockfd,sendbuf,len,0,(struct sockaddr *)&dest,sizeof (dest)); /*經socket傳送數據*/
- }
- /*接收程序發出的ping命令的應答*/
- void recv_reply()
- {
- int n,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);
- if(checksum((u8 *)icmp,ip_datalen)) /*計算校驗和*/
- return -1;
- if(icmp->icmp_id!=pid)
- 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->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消息*/
- }
程序執行:
1、程序編譯。在linux終端下執行如下命令:
gcc ping.h -o myping -std=gnu99
因爲原始套接字的創建需要root用戶權限,所以爲了能讓所有的其他用戶也可以使用該程序,需要通過如下命令設置myping的set-user-id位:
$sudo chmod u+s myping
結果爲: