Linux应用编程之ICMP协议(ping实现)

 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);
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值