這個程式主要運用了ICMPv4協議(回應要求)來測試本機到某伺服器的網路是否連通,因為其中用到了原始通訊端,所以運行該程式需要管理員權限。
PS:本程式只支援一種輸入方式:./myping <hostname>,不支援其他參數。
思路:
1:根據hostname參數建立原始通訊端。
2:每隔1秒鐘向伺服器發送一個ICMP回應要求。
3:迴圈接收從伺服器返回的應答並處理其資料。
上代碼:
#include <signal.h>#include <stdio.h>#include <errno.h>#include <stdlib.h>#include <netinet/in_systm.h>#include <netinet/ip.h>#include <netinet/ip_icmp.h>#include <arpa/inet.h>#include <netdb.h>#include <sys/un.h>//各種緩衝區的長度#define BUFSIZE 1500//ICMP回應要求的長度#define DATA_LEN 56 struct proto {struct sockaddr *sasend; /* sockaddr{} for send, from getaddrinfo */struct sockaddr *sarecv; /* sockaddr{} for receiving */socklen_t salen; /* length of sockaddr{}s */int icmpproto; /* IPPROTO_xxx value for ICMP */};//全域變數pid_t g_pid;int g_sockfd;struct proto g_proto = { NULL, NULL, 0, IPPROTO_ICMP };//處理伺服器返回的ICMP回顯資訊void proc_msg(char *, ssize_t, struct msghdr *, struct timeval *);//發送ICMP回應要求void send_msg(void);//迴圈發送、接收資訊void readloop(void);//定時器入門函數,每隔一秒一次發送ICMP請求void sig_alrm(int);//計算兩個時間之間的間隔void tv_sub(struct timeval *, struct timeval *);//擷取伺服器的地址等資訊struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype);//根據伺服器資訊,得到伺服器的IP地址char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen);//計算校正和uint16_t in_cksum(uint16_t *addr, int len);//輸出錯誤資訊,退出程式void error_quit(const char *str);int main(int argc, char **argv){ int c;struct addrinfo *ai;struct sockaddr_in *sin;char *ip_address;char *host;//本程式只支援一種輸入方式:./myping <hostname>if( argc != 2 )error_quit("usage: myping <hostname>");host = argv[1];//將pid的高二位全置為0,ICMP的ID只有16位g_pid = getpid() & 0xffff; //設定定時器,每秒鐘向伺服器發送一次請求signal(SIGALRM, sig_alrm);//擷取伺服器的資訊(addrinfo結構)ai = host_serv(host, NULL, 0, 0);ip_address = sock_ntop_host(ai->ai_addr, ai->ai_addrlen);printf("PING %s (%s): %d data bytes\n",ai->ai_canonname ? ai->ai_canonname : ip_address,ip_address, DATA_LEN);//如果返回的協議簇不是AF_INET(IPv4),則退出if ( ai->ai_family != AF_INET )error_quit("unknown address family");//設定proto結構體g_proto.sasend = ai->ai_addr;g_proto.sarecv = calloc(1, ai->ai_addrlen);g_proto.salen = ai->ai_addrlen;//開始迴圈發送/接收請求readloop();return 0;}void readloop(void){int size;char recvbuf[BUFSIZE];char controlbuf[BUFSIZE];struct msghdr msg;struct iovec iov;ssize_t n;struct timeval tval;//建立一個IPv4的原始通訊端g_sockfd = socket(g_proto.sasend->sa_family, SOCK_RAW, g_proto.icmpproto);if( -1 == g_sockfd )error_quit("socket error");//放棄管理員權限//這個程式中,只用建立原始通訊端時需要管理員權限setuid(getuid());//設定socket的接收緩衝區size = 60 * 1024;setsockopt(g_sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));//發出第一個請求sig_alrm(SIGALRM);//為recvmsg調用設定msghdr結構iov.iov_base = recvbuf;iov.iov_len = sizeof(recvbuf);msg.msg_name = g_proto.sarecv;msg.msg_iov = &iov;msg.msg_iovlen = 1;msg.msg_control = controlbuf;//開始死迴圈,不斷讀取和處理從伺服器中返回的資訊while( 1 ){msg.msg_namelen = g_proto.salen;msg.msg_controllen = sizeof(controlbuf);n = recvmsg(g_sockfd, &msg, 0);if (n < 0){if (errno == EINTR)continue;elseerror_quit("recvmsg error");}//分析返回內容,產生輸出gettimeofday(&tval, NULL);proc_msg(recvbuf, n, &msg, &tval);}}void proc_msg(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv){int hlen1, icmplen;double rtt;struct ip *ip;struct icmp *icmp;struct timeval *tvsend;//將伺服器返回的字串強轉為ip結構ip = (struct ip *) ptr; //得到IP表頭的長度hlen1 = ip->ip_hl << 2; //如果不是ICMP的應答,則返回if (ip->ip_p != IPPROTO_ICMP)return;icmp = (struct icmp *) (ptr + hlen1); //長度不足,不是合法應答if ( (icmplen = len - hlen1) < 8)return;//不是回顯應答,返回if (icmp->icmp_type != ICMP_ECHOREPLY) return;//不是我們發出請求的應答,返回if (icmp->icmp_id != g_pid)return; //長度不足,非法應答if (icmplen < 16)return;//計算網路延時tvsend = (struct timeval *) icmp->icmp_data;tv_sub(tvrecv, tvsend);rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;//輸出資訊printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",icmplen, sock_ntop_host(g_proto.sarecv, g_proto.salen),icmp->icmp_seq, ip->ip_ttl, rtt);}void send_msg(void){int len;int res;struct icmp *icmp;char sendbuf[BUFSIZE];static int nsent = 0;//根據ICMPv4協議來設定發送資訊icmp = (struct icmp *) sendbuf;//ICMP回應要求icmp->icmp_type = ICMP_ECHO;icmp->icmp_code = 0;//ICMP標識符欄位為本進程的PIDicmp->icmp_id = g_pid;//ICMP序號欄位為不斷遞增的全域變數nsenticmp->icmp_seq = nsent++;//ICMP資料欄位為目前時間截,空白部分填充0xa5memset(icmp->icmp_data, 0xa5, DATA_LEN);gettimeofday((struct timeval *)icmp->icmp_data, NULL);//計算並填充校正和len = 8 + DATA_LEN;icmp->icmp_cksum = 0;icmp->icmp_cksum = in_cksum((u_short *) icmp, len);//發送資料res = sendto(g_sockfd, sendbuf, len, 0, g_proto.sasend, g_proto.salen);if( -1 == res )error_quit("sendto error");}void sig_alrm(int signo){send_msg();alarm(1);}void tv_sub(struct timeval *out, struct timeval *in){//將兩個時間相減,並把結果存入第一個參數中( out -= in )if ( (out->tv_usec -= in->tv_usec) < 0) { --out->tv_sec;out->tv_usec += 1000000;}out->tv_sec -= in->tv_sec;}struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype){int n;struct addrinfo hints, *res;memset(&hints, 0, sizeof(struct addrinfo));hints.ai_flags = AI_CANONNAME;hints.ai_family = family; hints.ai_socktype = socktype;n = getaddrinfo(host, serv, &hints, &res);if ( n != 0 )error_quit("getaddrinfo error");return res;}char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen){static char str[128];struct sockaddr_in *sin = (struct sockaddr_in *) sa;//本程式只支援IPv4協議if( sa->sa_family != AF_INET )error_quit("sock_ntop_host: the type must be AF_INET");if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)error_quit("inet_ntop error");return str;}//《UNIX網路編程》書上的源碼uint16_t in_cksum(uint16_t *addr, int len){int nleft = len;uint32_t sum = 0;uint16_t *w = addr;uint16_t answer = 0;/** Our algorithm is simple, using a 32 bit accumulator (sum), we add* sequential 16 bit words to it, and at the end, fold back all the* carry bits from the top 16 bits into the lower 16 bits.*/while (nleft > 1) {sum += *w++;nleft -= 2;}/* 4mop up an odd byte, if necessary */if (nleft == 1) {*(unsigned char *)(&answer) = *(unsigned char *)w ;sum += answer;}/* 4add back carry outs from top 16 bits to low 16 bits */sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */sum += (sum >> 16); /* add carry */answer = ~sum; /* truncate to 16 bits */return(answer);}void error_quit(const char *str){//輸出錯誤資訊,退出程式fprintf(stderr, "%s", str); if( errno != 0 ) fprintf(stderr, " : %s", strerror(errno)); fprintf(stderr, "\n"); exit(1); }
運行樣本:
qch@LinuxMint ~/program/tcode $ gcc myping.c -o myping
qch@LinuxMint ~/program/tcode $ sudo ./myping www.baidu.com
PING www.a.shifen.com (115.239.210.26): 56 data bytes
64 bytes from 115.239.210.26: seq=0, ttl=128, rtt=31.272 ms
64 bytes from 115.239.210.26: seq=1, ttl=128, rtt=34.722 ms
64 bytes from 115.239.210.26: seq=2, ttl=128, rtt=30.822 ms
64 bytes from 115.239.210.26: seq=3, ttl=128, rtt=31.273 ms
64 bytes from 115.239.210.26: seq=4, ttl=128, rtt=29.995 ms
...........................