首先我们在terminal上使用ping命令并用wireshark软件抓包,看看实现ping命令需要那些协议,以及ping的数据包由那些内容构成。
用wireshark抓包后,发现ping命令发送的请求报文和收到的应答报文都是ICMP(Internet Control Message Protocol)网际控制报文协议。我们再仔细解析请求报文和应答报文:
发现ICMP报文是装在IP数据报中的,并且ICMP报文具有以下字段:Type(8位)、Code(8位)、Cheksum(16位)、Identifier(16位)、Sequence(16位)、Timestamp(8位)、Data。
结合以上对ping命令的分析,我们可以想到,实现ping命令,需要用到ICMP协议的相关内容。想要实现对IMCP报文自定义构建,我们还需要用到SOCK_RAW原始套接字的相关内容。在Linux上申请原始套接字需要root权限,但是ping命令可以被普通用户正常运行,因此我们还需要用到在Linux上以普通用户运行特权指令的相关内容(在Linux上以普通用户运行特权指令在我的上一篇博客有详细的说明,本文不在赘述)。此外,我们可以观察到ping命令是通过捕获Ctrl+C指令后才结束的,但是在结束之前,还对整个发送接收情况做了总结,因此,实现Ping命令还需要用到Linux上的信号机制。
ICMP报文
IMCP协议用于在IP主机、路由器之间传递控制消息,允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP协议不是高层协议(看起来好像是高层协议,因为ICMP报文是装在IP数据报中,作为其中的数据部分),而是IP层的协议。ICMP报文作为IP层数据报的数据,加上数据报的首部,组成IP数据报发送出去。ICMP报文格式如下图所示:
在谢希仁编著的第7版计算机网络教材中,ICMP报文的格式与上图相同,但是从上文的抓包情况来看,真正的ICMP报文在16位序列号数据之后,Data数据之前还加入了8位的时间戳数据。
仔细对比上文对请求报文和应答报文的抓包数据,发现除了ICMP报文的序列号(seq)因包而异以外,Type字段也不相同。查阅文献后知道ICMP报文类型是根据Type字段和Code字段的组合来确定的。Type = 0,Code = 0
代表回送请求(Echo Request),Type = 8,Code = 0
代表回送应答(Echo Reply),分别对应ping命令的请求报文和应答报文。
对于ICMP报文的校验和(Checksum)字段的计算,只需要以下几个步骤:
- 将校验和字段置零。
- 将每两个字节(16位)相加(二进制求和)直到最后得出结果,若出现最后还剩一个字节继续与前面结果相加。
- (溢出)将高16位与低16位相加,直到高16位为0为止。
- 将最后的结果(二进制)取反。
用C/C++对ICMP报文数据的构造,可以直接利用ip_icmp.h
头文件中有关ICMP报文的内容进行构造。struct icmp
结构如下:
1 | struct icmp |
2 | { |
3 | uint8_t icmp_type; /* type of message, see below */ |
4 | uint8_t icmp_code; /* type sub code */ |
5 | uint16_t icmp_cksum; /* ones complement checksum of struct */ |
6 | union |
7 | { |
8 | unsigned char ih_pptr; /* ICMP_PARAMPROB */ |
9 | struct in_addr ih_gwaddr; /* gateway address */ |
10 | struct ih_idseq /* echo datagram */ |
11 | { |
12 | uint16_t icd_id; |
13 | uint16_t icd_seq; |
14 | } ih_idseq; |
15 | uint32_t ih_void; |
16 | |
17 | /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */ |
18 | struct ih_pmtu |
19 | { |
20 | uint16_t ipm_void; |
21 | uint16_t ipm_nextmtu; |
22 | } ih_pmtu; |
23 | |
24 | struct ih_rtradv |
25 | { |
26 | uint8_t irt_num_addrs; |
27 | uint8_t irt_wpa; |
28 | uint16_t irt_lifetime; |
29 | } ih_rtradv; |
30 | } icmp_hun; |
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | union |
42 | { |
43 | struct |
44 | { |
45 | uint32_t its_otime; |
46 | uint32_t its_rtime; |
47 | uint32_t its_ttime; |
48 | } id_ts; |
49 | struct |
50 | { |
51 | struct ip idi_ip; |
52 | /* options and then 64 bits of data */ |
53 | } id_ip; |
54 | struct icmp_ra_addr id_radv; |
55 | uint32_t id_mask; |
56 | uint8_t id_data[1]; |
57 | } icmp_dun; |
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | }; |
上述结构体中我们只需要关注icmp_type、icmp_code、icmp_cksum、icmp_seq、icmp_id、icmp_data字段即可。我们可以直接在内存中利用指针对icmp结构体的指针指向数据块进行构造,以达到对整个IMCP报文的构建。例如:
1 | icmp_pointer->icmp_type = ICMP_ECHO; |
2 | icmp_pointer->icmp_code = 0; |
3 | icmp_pointer->icmp_cksum = 0; //计算校验和之前先要将校验位置零 |
4 | icmp_pointer->icmp_seq = send_pack_num + 1; //用send_pack_num作为ICMP包序列号 |
5 | icmp_pointer->icmp_id = getpid(); //用进程号作为ICMP包标志 |
IP数据报
在ping命令中,我们使用recvfrom()
函数接收到的回应报文是IP数据报,并且我们需要用到以下字段:
- IP报头长度IHL(Internet Header Length)以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。
- 生存时间TTL(Time To Live)以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。
使用方法与上述ICMP报文结构体使用方法一致,这里给出struct ip
的定义,不在过多说明:
1 | struct ip |
2 | { |
3 |
|
4 | unsigned int ip_hl:4; /* header length */ |
5 | unsigned int ip_v:4; /* version */ |
6 |
|
7 |
|
8 | unsigned int ip_v:4; /* version */ |
9 | unsigned int ip_hl:4; /* header length */ |
10 |
|
11 | uint8_t ip_tos; /* type of service */ |
12 | unsigned short ip_len; /* total length */ |
13 | unsigned short ip_id; /* identification */ |
14 | unsigned short ip_off; /* fragment offset field */ |
15 |
|
16 |
|
17 |
|
18 |
|
19 | uint8_t ip_ttl; /* time to live */ |
20 | uint8_t ip_p; /* protocol */ |
21 | unsigned short ip_sum; /* checksum */ |
22 | struct in_addr ip_src, ip_dst; /* source and dest address */ |
23 | }; |
SOCK_RAW原始套接字
实际上,我们常用的网络编程都是在应用层的手法操作,也就是大多数程序员接触到的流式套接字(SOCK_STREAM)和数据包式套接字(SOCK_DGRAM)。而这些数据包都是由系统提供的协议栈实现,用户只需要填充应用层报文即可,由系统完成底层报文头的填充并发送。然而Ping命令的实现中需要执行更底层的操作,这个时候就需要使用原始套接字(SOCK_RAW)来实现。
原始套接字(SOCK_RAW)是一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心。原始套接字可以实现普通套接字无法处理ICMP、IGMP等网络报文,原始套接字也可以处理特殊的IPv4报文,此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。总体来说,原始套接字可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据。
创建原始套接字的方法如下:
1 | socket(AF_INET, SOCK_RAW, protocol); |
这里的重点在于protocol
字段,使用原始套接字之后,这个字段就不能简单的置零了。在头文件netinet/in.h
中定义了系统中该字段目前能取的值,注意:有些系统中不一定实现了netinet/in.h
中的所有协议。源代码的linux/in.h
中和netinet/in.h
中的内容一样。我们常见的有IPPROTO_TCP
,IPPROTO_UDP
和IPPROTO_ICMP
。我们可以通过一下方法创建需要的ICMP协议的原始套接字:
1 | struct protoent * protocol; //获取协议用 |
2 | //通过协议名称获取协议编号 |
3 | if((protocol = getprotobyname("icmp")) == NULL){ |
4 | fprintf(stderr, "Get protocol error:%s \n\a", strerror(errno)); |
5 | exit(1); |
6 | } |
7 | //创建原始套接字,这里需要root权限,申请完成之后应该降权处理 |
8 | if((sock_fd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) == -1){ |
9 | fprintf(stderr, "Greate RAW socket error:%s \n\a", strerror(errno)); |
10 | exit(1); |
11 | } |
12 | //降权处理,使该进程的EUID,SUID的值变成RUID的值 |
13 | setuid(getuid()); |
用这种方式我就可以得到原始的IP包了,然后就可以自定义IP所承载的具体协议类型,如TCP,UDP或ICMP,并手动对每种承载在IP协议之上的报文进行填充。
Linux上的捕获Ctrl+C信号
在Linux C/C++程序中,如果程序一直以死循环的状态运行,以Ctrl+C结束,并且希望在结束时输出一些统计数据帮助用户分析,我们就需要用到信号处理机制。通过捕获到的需要的信号后,执行信号处理函数。为此我们需要用到sigaction()
函数,该函数的的功能是检查或修改与制定信号相关联的处理动作,其原型为:
1 | int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
signum参数用于指定需要捕获的型号类型,act参数指定新的型号处理方式,oldact参数输出先前型号的处理方式(如果不为NULL的话)。这个函数的关键在于对struct sigaction
结构体的设置,我们首先找到该结构体的定义:
1 | struct sigaction |
2 | { |
3 | /* Signal handler. */ |
4 |
|
5 | union |
6 | { |
7 | /* Used if SA_SIGINFO is not set. */ |
8 | __sighandler_t sa_handler; |
9 | /* Used if SA_SIGINFO is set. */ |
10 | void (*sa_sigaction) (int, siginfo_t *, void *); |
11 | } |
12 | __sigaction_handler; |
13 |
|
14 |
|
15 |
|
16 | __sighandler_t sa_handler; |
17 |
|
18 | |
19 | /* Additional set of signals to be blocked. */ |
20 | __sigset_t sa_mask; |
21 | |
22 | /* Special flags. */ |
23 | int sa_flags; |
24 | |
25 | /* Restore handler. */ |
26 | void (*sa_restorer) (void); |
27 | }; |
注意到有如下几个字段:
- sa_handler 与 sa_sigaction这两个字段为联合体,因此只能同时设置一个,这两个字段的作用都是用来存储信号处理函数的指针,但是sa_sigaction作为信号处理函数,可以传入自定义的参数,而sa_handler不行
- sa_mask用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
- sa_flags用来设置信号处理的其他相关操作,有下列数值可用,用OR运算组合:
A_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来
SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction
因此,实现ping命令过程中,对Ctrl+C的捕获及处理的具体实现方法如下:
1 | void SingnalHandler(int signo) { //信号处理函数 |
2 | //处理过程 |
3 | ... |
4 | exit(0); |
5 | } |
6 | |
7 | int main(int argc, char * argv[]) { |
8 | struct sigaction action; //sigaction结构体 |
9 | |
10 | action.sa_handler = SingnalHandler; |
11 | sigemptyset(&action.sa_mask); |
12 | action.sa_flags = 0; |
13 | |
14 | sigaction(SIGINT,&action,NULL); //SIGINT = 2捕获Ctrl+C |
15 | |
16 | while(1) |
17 | { |
18 | //死循环 |
19 | ... |
20 | sleep(1); |
21 | } |
22 | } |
最终实现代码
1、main.cpp
1 |
|
2 |
|
3 | |
4 | Ping * p; |
5 | |
6 | void SingnalHandler(int signo) { |
7 | |
8 | p->statistic(); |
9 | |
10 | exit(0); |
11 | } |
12 | |
13 | int main(int argc, char * argv[]) { |
14 | struct sigaction action; |
15 | |
16 | action.sa_handler = SingnalHandler; |
17 | sigemptyset(&action.sa_mask); |
18 | action.sa_flags = 0; |
19 | |
20 | sigaction(SIGINT,&action,NULL); |
21 | |
22 | Ping ping(argv[1], 1); |
23 | p = &ping; |
24 | ping.CreateSocket(); |
25 | while(1) |
26 | { |
27 | ping.SendPacket(); |
28 | ping.RecvPacket(); |
29 | sleep(1); |
30 | } |
31 | } |
2、ping.h(在src目录下)
1 | // |
2 | // Created by mylord on 2019/9/26. |
3 | // |
4 | |
5 |
|
6 |
|
7 | |
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | |
21 |
|
22 | |
23 | class Ping { |
24 | private: |
25 | std::string input_domain; //用来存储通过main函数的参数传入的域名或者ip |
26 | std::string backup_ip; //通过输入的域名或者ip转化成为的ip备份 |
27 | |
28 | int sock_fd; |
29 | |
30 | int max_wait_time; //最大等待时间 |
31 | |
32 | int send_pack_num; //发送的数据包数量 |
33 | int recv_pack_num; //收到的数据包数量 |
34 | int lost_pack_num; //丢失的数据包数量 |
35 | |
36 | struct sockaddr_in send_addr; //发送到目标的套接字结构体 |
37 | struct sockaddr_in recv_addr; //接受来自目标的套接字结构体 |
38 | |
39 | char send_pack[PACK_SIZE]; //用于保存发送的ICMP包 |
40 | char recv_pack[PACK_SIZE + 20]; //用于保存接收的ICMP包 |
41 | |
42 | struct timeval first_send_time; //第一次发送ICMP数据包时的UNIX时间戳 |
43 | struct timeval recv_time; //接收ICMP数据包时的UNIX时间戳 |
44 | |
45 | double min_time; |
46 | double max_time; |
47 | double sum_time; |
48 | |
49 | |
50 | int GeneratePacket(); |
51 | int ResolvePakcet(int pack_szie); |
52 | |
53 | unsigned short CalculateCksum(unsigned short * send_pack, int pack_size); |
54 | |
55 | public: |
56 | Ping(const char * ip, int max_wait_time); |
57 | ~Ping(); |
58 | |
59 | void CreateSocket(); |
60 | |
61 | void SendPacket(); |
62 | void RecvPacket(); |
63 | |
64 | void statistic(); |
65 | }; |
66 | |
67 | |
68 |
|
3、ping.cpp(在src目录下)
1 | // |
2 | // Created by mylord on 2019/9/26. |
3 | // |
4 | |
5 |
|
6 | |
7 | Ping::Ping(const char * ip, int max_wait_time){ |
8 | this->input_domain = ip; |
9 | |
10 | this->max_wait_time = max_wait_time < 3 ? max_wait_time : 3; |
11 | |
12 | this->send_pack_num = 0; |
13 | this->recv_pack_num = 0; |
14 | this->lost_pack_num = 0; |
15 | |
16 | this->min_time = 0; |
17 | this->max_time = 0; |
18 | this->sum_time = 0; |
19 | } |
20 | |
21 | Ping::~Ping() { |
22 | if(close(sock_fd) == -1) { |
23 | fprintf(stderr, "Close socket error:%s \n\a", strerror(errno)); |
24 | exit(1); |
25 | } |
26 | } |
27 | |
28 | void Ping::CreateSocket(){ |
29 | struct protoent * protocol; //获取协议用 |
30 | unsigned long in_addr; //用来保存网络字节序的二进制地址 |
31 | struct hostent host_info, * host_pointer; //用于gethostbyname_r存放IP信息 |
32 | char buff[2048]; //gethostbyname_r函数临时的缓冲区,用来存储过程中的各种信息 |
33 | int errnop = 0; //gethostbyname_r函数存储错误码 |
34 | |
35 | //通过协议名称获取协议编号 |
36 | if((protocol = getprotobyname("icmp")) == NULL){ |
37 | fprintf(stderr, "Get protocol error:%s \n\a", strerror(errno)); |
38 | exit(1); |
39 | } |
40 | |
41 | //创建原始套接字,这里需要root权限,申请完成之后应该降权处理 |
42 | if((sock_fd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) == -1){ |
43 | fprintf(stderr, "Greate RAW socket error:%s \n\a", strerror(errno)); |
44 | exit(1); |
45 | } |
46 | |
47 | //降权处理,使该进程的EUID,SUID的值变成RUID的值 |
48 | setuid(getuid()); |
49 | |
50 | //设置send_addr结构体 |
51 | send_addr.sin_family = AF_INET; |
52 | |
53 | //判断用户输入的点分十进制的ip地址还是域名,如果是域名则将其转化为ip地址,并备份 |
54 | //inet_addr()将一个点分十进制的IP转换成一个长整数型数 |
55 | if((in_addr = inet_addr(input_domain.c_str())) == INADDR_NONE){ |
56 | //输入的不是点分十进制的ip地址 |
57 | if(gethostbyname_r(input_domain.c_str(), &host_info, buff, sizeof(buff), &host_pointer, &errnop)){ |
58 | //非法域名 |
59 | fprintf(stderr, "Get host by name error:%s \n\a", strerror(errno)); |
60 | exit(1); |
61 | } else{ |
62 | //输入的是域名 |
63 | this->send_addr.sin_addr = *((struct in_addr *)host_pointer->h_addr); |
64 | } |
65 | } else{ |
66 | //输入的是点分十进制的地址 |
67 | this->send_addr.sin_addr.s_addr = in_addr; |
68 | } |
69 | |
70 | //将ip地址备份下来 |
71 | this->backup_ip = inet_ntoa(send_addr.sin_addr); |
72 | |
73 | printf("PING %s (%s) %d(%d) bytes of data.\n", input_domain.c_str(), |
74 | backup_ip.c_str(), PACK_SIZE - 8, PACK_SIZE + 20); |
75 | |
76 | gettimeofday(&first_send_time, NULL); |
77 | } |
78 | |
79 | unsigned short Ping::CalculateCksum(unsigned short * send_pack, int pack_size){ |
80 | int check_sum = 0; //校验和 |
81 | int nleft = pack_size; //还未计算校验和的数据长度 |
82 | unsigned short * p = send_pack; //用来做临时指针 |
83 | unsigned short temp; //用来处理字节长度为奇数的情况 |
84 | |
85 | while(nleft > 1){ |
86 | check_sum += *p++; //check_sum先加以后,p的指针才向后移 |
87 | nleft -= 2; |
88 | } |
89 | |
90 | //奇数个长度 |
91 | if(nleft == 1){ |
92 | //利用char类型是8个字节,将剩下的一个字节压入unsigned short(16字节)的高八位 |
93 | *(unsigned char *)&temp = *(unsigned char *)p; |
94 | check_sum += temp; |
95 | } |
96 | |
97 | check_sum = (check_sum >> 16) + (check_sum & 0xffff); //将之前计算结果的高16位和低16位相加 |
98 | check_sum += (check_sum >> 16); //防止上一步也出现溢出 |
99 | temp = ~check_sum; //temp是最后的校验和 |
100 | |
101 | return temp; |
102 | } |
103 | |
104 | int Ping::GeneratePacket() |
105 | { |
106 | int pack_size; |
107 | struct icmp * icmp_pointer; |
108 | struct timeval * time_pointer; |
109 | |
110 | //将发送的char[]类型的send_pack直接强制转化为icmp结构体类型,方便修改数据 |
111 | icmp_pointer = (struct icmp *)send_pack; |
112 | |
113 | //type为echo类型且code为0代表回显应答(ping应答) |
114 | icmp_pointer->icmp_type = ICMP_ECHO; |
115 | icmp_pointer->icmp_code = 0; |
116 | icmp_pointer->icmp_cksum = 0; //计算校验和之前先要将校验位置0 |
117 | icmp_pointer->icmp_seq = send_pack_num + 1; //用send_pack_num作为ICMP包序列号 |
118 | icmp_pointer->icmp_id = getpid(); //用进程号作为ICMP包标志 |
119 | |
120 | pack_size = PACK_SIZE; |
121 | |
122 | //将icmp结构体中的数据字段直接强制类型转化为timeval类型,方便将Unix时间戳赋值给icmp_data |
123 | time_pointer = (struct timeval *)icmp_pointer->icmp_data; |
124 | |
125 | gettimeofday(time_pointer, NULL); |
126 | |
127 | icmp_pointer->icmp_cksum = CalculateCksum((unsigned short *)send_pack, pack_size); |
128 | |
129 | return pack_size; |
130 | } |
131 | |
132 | void Ping::SendPacket() { |
133 | int pack_size = GeneratePacket(); |
134 | |
135 | if((sendto(sock_fd, send_pack, pack_size, 0, (const struct sockaddr *)&send_addr, sizeof(send_addr))) < 0){ |
136 | fprintf(stderr, "Sendto error:%s \n\a", strerror(errno)); |
137 | exit(1); |
138 | } |
139 | |
140 | this->send_pack_num++; |
141 | } |
142 | |
143 | //要对收到的IP数据包去IP报头操作,校验ICMP,提取时间戳 |
144 | int Ping::ResolvePakcet(int pack_size) { |
145 | int icmp_len, ip_header_len; |
146 | struct icmp * icmp_pointer; |
147 | struct ip * ip_pointer = (struct ip *)recv_pack; |
148 | double rtt; |
149 | struct timeval * time_send; |
150 | |
151 | ip_header_len = ip_pointer->ip_hl << 2; //ip报头长度=ip报头的长度标志乘4 |
152 | icmp_pointer = (struct icmp *)(recv_pack + ip_header_len); //pIcmp指向的是ICMP头部,因此要跳过IP头部数据 |
153 | icmp_len = pack_size - ip_header_len; //ICMP报头及ICMP数据报的总长度 |
154 | |
155 | //收到的ICMP包长度小于报头 |
156 | if(icmp_len < 8) { |
157 | printf("received ICMP pack lenth:%d(%d) is error!\n", pack_size, icmp_len); |
158 | lost_pack_num++; |
159 | return -1; |
160 | } |
161 | if((icmp_pointer->icmp_type == ICMP_ECHOREPLY) && |
162 | (backup_ip == inet_ntoa(recv_addr.sin_addr)) && |
163 | (icmp_pointer->icmp_id == getpid())){ |
164 | |
165 | time_send = (struct timeval *)icmp_pointer->icmp_data; |
166 | |
167 | if((recv_time.tv_usec -= time_send->tv_usec) < 0) { |
168 | --recv_time.tv_sec; |
169 | recv_time.tv_usec += 10000000; |
170 | } |
171 | |
172 | rtt = (recv_time.tv_sec - time_send->tv_sec) * 1000 + (double)recv_time.tv_usec / 1000.0; |
173 | |
174 | if(rtt > (double)max_wait_time * 1000) |
175 | rtt = max_time; |
176 | |
177 | if(min_time == 0 | rtt < min_time) |
178 | min_time = rtt; |
179 | if(rtt > max_time) |
180 | max_time = rtt; |
181 | |
182 | sum_time += rtt; |
183 | |
184 | printf("%d byte from %s : icmp_seq=%u ttl=%d time=%.1fms\n", |
185 | icmp_len, |
186 | inet_ntoa(recv_addr.sin_addr), |
187 | icmp_pointer->icmp_seq, |
188 | ip_pointer->ip_ttl, |
189 | rtt); |
190 | |
191 | recv_pack_num++; |
192 | } else{ |
193 | printf("throw away the old package %d\tbyte from %s\ticmp_seq=%u\ticmp_id=%u\tpid=%d\n", |
194 | icmp_len, inet_ntoa(recv_addr.sin_addr), icmp_pointer->icmp_seq, |
195 | icmp_pointer->icmp_id, getpid()); |
196 | |
197 | return -1; |
198 | } |
199 | |
200 | } |
201 | |
202 | void Ping::RecvPacket() { |
203 | int recv_size, fromlen; |
204 | fromlen = sizeof(struct sockaddr); |
205 | |
206 | while(recv_pack_num + lost_pack_num < send_pack_num) { |
207 | fd_set fds; |
208 | FD_ZERO(&fds); //每次循环都必须清空FD_Set |
209 | FD_SET(sock_fd, &fds); //将sock_fd加入集合 |
210 | |
211 | int maxfd = sock_fd + 1; |
212 | struct timeval timeout; |
213 | timeout.tv_sec = this->max_wait_time; |
214 | timeout.tv_usec = 0; |
215 | |
216 | //使用select实现非阻塞IO |
217 | int n = select(maxfd, NULL, &fds, NULL, &timeout); |
218 | |
219 | switch(n) { |
220 | case -1: |
221 | fprintf(stderr, "Select error:%s \n\a", strerror(errno)); |
222 | exit(1); |
223 | case 0: |
224 | printf("select time out, lost packet!\n"); |
225 | lost_pack_num++; |
226 | break; |
227 | default: |
228 | //判断sock_fd是否还在集合中 |
229 | if(FD_ISSET(sock_fd, &fds)) { |
230 | //还在集合中则说明收到了回显的数据包 |
231 | if((recv_size = recvfrom(sock_fd, recv_pack, sizeof(recv_pack), |
232 | 0, (struct sockaddr *)&recv_addr, (socklen_t *)&fromlen)) < 0) { |
233 | fprintf(stderr, "packet error(size:%d):%s \n\a", recv_size, strerror(errno)); |
234 | lost_pack_num++; |
235 | } else{ |
236 | //收到了可能合适的数据包 |
237 | gettimeofday(&recv_time, NULL); |
238 | |
239 | ResolvePakcet(recv_size); |
240 | } |
241 | } |
242 | break; |
243 | } |
244 | } |
245 | } |
246 | |
247 | void Ping::statistic() { |
248 | double total_time; |
249 | struct timeval final_time; |
250 | gettimeofday(&final_time, NULL); |
251 | |
252 | if((final_time.tv_usec -= first_send_time.tv_usec) < 0) { |
253 | --final_time.tv_sec; |
254 | final_time.tv_usec += 10000000; |
255 | } |
256 | total_time = (final_time.tv_sec - first_send_time.tv_sec) * 1000 + (double)final_time.tv_usec / 1000.0; |
257 | |
258 | printf("\n--- %s ping statistics ---\n",input_domain.c_str()); |
259 | printf("%d packets transmitted, %d received, %.0f%% packet loss, time %.0f ms\n", |
260 | send_pack_num, recv_pack_num, (double)(send_pack_num - recv_pack_num) / (double)send_pack_num, |
261 | total_time); |
262 | printf("rtt min/avg/max = %.3f/%.3f/%.3f ms\n", min_time, (double)sum_time / recv_pack_num, max_time); |
263 | |
264 | |
265 | } |
4、CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.10) |
2 | project(MyPing) |
3 | |
4 | set(CMAKE_CXX_STANDARD 14) |
5 | |
6 | add_executable(MyPing main.cpp src/ping.cpp src/ping.h) |
5、编译及运行过程
1 | cmake . |
2 | sudo make |
3 | sudo chmod u+s MyPing |
4 | ./MyPing www.baidu.com |
参考文献:
https://blog.csdn.net/zhaorenjie93/article/details/72859715
https://www.cnblogs.com/aspirant/p/4084127.html
http://abcdxyzk.github.io/blog/2015/04/14/kernel-net-sock-raw/
https://blog.csdn.net/zhj082/article/details/80518322
https://blog.csdn.net/yzy1103203312/article/details/79799197