icmp可以用于测试网络延时。在实际开发中,我们经常可以遇到网络是连接状态,但是不能连接外网,我们可以通过ICMP协议进行测试,测试的对象一般是比较稳定的服务器,比如说常见的DNS服务器,或者阿里的服务器等。
/*
* Copyright (C) 2021, 2021 huohongpeng
* Author: huohongpeng <1045338804@qq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Change logs:
* Date Author Notes
* 2021-06-09 huohongpeng 首次添加
*/
#include "icmp.h"
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
/*
* 114.114.114.114 这个网址不检测icmp的校验和,只要发送就有回复
* Ali 223.5.5.5.5 223.6.6.6
* Baidu 180.76.76.76
*/
#if 0
struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
socklen_t ai_addrlen; /* Length of socket address. */
struct sockaddr *ai_addr; /* Socket address for socket. */
char *ai_canonname; /* Canonical name for service location. */
struct addrinfo *ai_next; /* Pointer to next in list. */
};
#endif
static long long icmp_get_time_us(void)
{
struct timespec tm;
clock_gettime(CLOCK_MONOTONIC, &tm);
long long ret = tm.tv_sec;
ret = ret * 1000000 + (tm.tv_nsec / 1000);
return ret;
}
/*
* 此函数来源于busybox
*/
static uint16_t inet_cksum(uint16_t *addr, int nleft)
{
/*
* 注意不同平台的大小端问题
*/
# define BB_LITTLE_ENDIAN 1
/*
* Our algorithm is simple, using a 32 bit accumulator,
* 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.
*/
unsigned sum = 0;
while (nleft > 1) {
sum += *addr++;
nleft -= 2;
}
/* Mop up an odd byte, if necessary */
if (nleft == 1) {
if (BB_LITTLE_ENDIAN)
sum += *(uint8_t*)addr;
else
sum += *(uint8_t*)addr << 8;
}
/* Add 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 */
return (uint16_t)~sum;
}
/**
* 通过icmp协议测试网络状态
* @host: 待测试网址,如wwww.baidu.com 或者 114.114.114.114
* @timeout_ms: 执行单次icmp最大超时时间
* @cnt: 执行icmp总次数
* @failed_cnt(输出参数): 执行完成后失败的总次数
* @avg_delay_ms(输出参数): 执行完成后icmp平均时间,可用于评估网络延时
* @return: 返回成功次数
*/
int ping(const char *host, const long timeout_ms, const int cnt, int *failed_cnt, float *avg_delay_ms)
{
int ret;
struct addrinfo hint;
struct addrinfo *res;
memset(&hint, 0x00, sizeof(struct addrinfo));
hint.ai_family = AF_INET;
hint.ai_socktype = SOCK_STREAM;
/*
* host如果是域名,比如www.baidu.com, 如果网络不正常, 这里将阻塞10s,之后返回-3.
* 如果是ip地址,则不会阻塞,只是一个格式的转换.
* 所以如果想通过ping检查网络状态,建议host使用常见的dsn服务器,如114.114.114.114
*/
ret = getaddrinfo(host, NULL, &hint, &res);
if (ret < 0) {
fprintf(stderr, "[F: %s:%d]: ret: %d\n", __FUNCTION__, __LINE__, ret);
return -1;
}
struct sockaddr_in host_addr;
char host_ip[64];
memcpy(&host_addr, res->ai_addr, sizeof(struct sockaddr_in));
freeaddrinfo(res);
inet_ntop(AF_INET, &host_addr.sin_addr, host_ip, 64);
int sock;
unsigned short id = getpid() & 0xffff;
unsigned short sequence = 0;
char pack[128];
struct icmphdr *icmphdr;
struct iphdr *iphdr;
char *icmpdata;
long long send_ts_us;
long long send_ts_us_from;
long long recv_ts_us;
long long remain_us;
long long time_us;
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
int i;
fd_set readfds;
struct timeval timeout;
long long toatl_time_us = 0;
int success_cnt = 0;
for (i = 0; i < cnt; i++) {
sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
icmphdr = (struct icmphdr *)pack;
icmpdata = pack + sizeof(struct icmphdr);
memset(pack, 0x00, sizeof(pack));
send_ts_us = icmp_get_time_us();
memcpy(icmpdata, &send_ts_us, sizeof(send_ts_us));
icmphdr->type = ICMP_ECHO;
icmphdr->code = 0;
icmphdr->un.echo.id = id;
icmphdr->un.echo.sequence = sequence;
icmphdr->checksum = inet_cksum((uint16_t *)pack, 64);
ret = sendto(sock, pack, 64, 0, (struct sockaddr *)&host_addr, sizeof(struct sockaddr_in));
remain_us = timeout_ms * 1000;
while (1) {
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
timeout.tv_sec = 0;
timeout.tv_usec = remain_us;
ret = select(sock + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
ret = recvfrom(sock, pack, sizeof(pack), 0, (struct sockaddr *)&from, &fromlen);
recv_ts_us = icmp_get_time_us();
time_us = recv_ts_us - send_ts_us;
iphdr = (struct iphdr *)pack;
icmphdr = (struct icmphdr *)(pack + (iphdr->ihl<<2));
icmpdata = (pack + (iphdr->ihl<<2) + sizeof(struct icmphdr));
memcpy(&send_ts_us_from, icmpdata, sizeof(send_ts_us));
if (ret > 0) {
if(icmphdr->type == ICMP_ECHOREPLY && \
icmphdr->un.echo.id == id && \
send_ts_us_from == send_ts_us) {
/*
* 接收到了正确的应答包,统计总时间和成功次数,结束当前接收
*/
toatl_time_us += time_us;
success_cnt++;
break;
} else {
/*
* 如果收到的不是当前ping应答包,并且总的接收超时还没到,则继续接收;
*/
if (time_us < (timeout_ms*1000)) {
remain_us = timeout_ms*1000 - time_us;
} else {
/*
* 超时到了,继续循环,尝试一次接收(非阻塞),因为可能缓冲区里面还有已经接收到的数据
*/
remain_us = 0;
}
}
}
} else {
/*
* 如果直到超时也没有收到正确的应答,结束接收,总花费时间按最大超时时间统计
*/
toatl_time_us += timeout_ms * 1000;
fprintf(stderr, "recv timeout\n");
break;
}
}
close(sock);
sequence++;
}
*failed_cnt = cnt - success_cnt;
*avg_delay_ms = (toatl_time_us / cnt) / 1000.0;
fprintf(stderr, "*failed_cnt: %d\n", *failed_cnt);
fprintf(stderr, "*avg_delay_ms: %.3f\n", *avg_delay_ms);
return success_cnt;
}
void ping_test(char *ip)
{
int failed_cnt;
float time;
ping(ip, 300, 5, &failed_cnt, &time);
//ping("47.93.76.140", 1000, 10, &failed_cnt, &time);
//ping("114.114.114.114", 3000, 10, &failed_cnt, &time);
}