ping的原理的解析

ICMP协议, 网络工具

Posted by chensong on 2019-08-30 08::00::55

前言

最近和同事测试网络带宽问题, 分析网络稳定性的问题时,在网上没有找到合适工具, 我发现网上测试网络带宽的原理都是ping一样的原理就研究一下

ping工具一直是我们使用测量网络是否相通。它的应用有很多,比如我们经常测试网络的带宽,网络安全,使用ICMP攻击使服务器繁忙,DOS攻击

正文

一, ping的原理介绍

ping使用协议在网络ISO中那一层

ping使用ICMP在网络层, 有IP头

里面有一个首部校验

这样我们就可以定义出IP头结构体

typedef struct IPhead
{
    //这里使用了C语言的位域,也就是说像version变量它的大小在内存中是占4bit,而不是8bit
    uint8_t     version : 4; //IP协议版本
    uint8_t     headLength : 4;//首部长度
    uint8_t     serverce;//区分服务
    uint16_t    totalLength;//总长度
    uint16_t    flagbit;//标识
    uint16_t    flag : 3;//标志
    uint16_t    fragmentOffset : 13;//片偏移
    char        timetoLive;//生存时间(跳数)
    uint8_t     protocol;//使用协议
    uint16_t    headcheckSum;//首部校验和
    uint32_t    srcIPadd;//源IP
    uint32_t    dstIPadd;//目的IP
    //可选项和填充我就不定义了
} IPhead;

我们由最上面图可知道还需要ICMP协议

icmp协议分为请求包和响应包

ICMP协议结构体的定义

//ICMP头
typedef struct ICMPhead
{
    uint8_t type;//类型
    uint8_t code;//代码
    uint16_t checkSum;//校验和
    uint16_t ident;//进程标识符
    uint16_t seqNum;//序号
} ICMPhead;
//ICMP回显请求报文(发送用)
typedef struct ICMP
{
    ICMPhead icmphead;//头部
    uint32_t timeStamp;//时间戳
    char     data[1000];//数据
};
//ICMP应答报文(接收用)
typedef struct ICMPReply
{
    IPhead iphead;//IP头
    ICMP icmpanswer;//ICMP报文
    char data[1024];//应答报文携带的数据缓冲区
} ICMPReply;

二, 实现ping

#include <stdio.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <sys/time.h>
#include <poll.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <math.h>
#include <fstream>
#include <ctime>
std::ofstream			g_fd;

int time64_datetime_format(time_t time, char* out, char date_conn, char datetime_conn, char time_conn)
{
	int nCount = 0;
	tm now_tm;
	::gmtime_r(&time, &now_tm);
	if (date_conn > 0)
	{
		nCount += sprintf(out + nCount, "%04d%c%02d%c%02d", now_tm.tm_year + 1900, date_conn
			, now_tm.tm_mon + 1, date_conn, now_tm.tm_mday);
	}
	else if(date_conn == 0)
	{
		nCount += sprintf(out + nCount, "%04d%02d%02d", now_tm.tm_year + 1900, now_tm.tm_mon + 1
			, now_tm.tm_mday);
	}

	if (datetime_conn > 0)
	{
		out[nCount] = datetime_conn;
		++nCount;
		out[nCount] = '\0';
	}

	if (time_conn > 0)
	{
		nCount += sprintf(out + nCount, "%02d%c%02d%c%02d", now_tm.tm_hour, time_conn
			, now_tm.tm_min, time_conn, now_tm.tm_sec);
	}
	else if (time_conn == 0)
	{
		nCount += sprintf(out + nCount, "%02d%02d%02d", now_tm.tm_hour, now_tm.tm_min, now_tm.tm_sec);
	}
	return nCount;
}

typedef struct IPhead
{
    //这里使用了C语言的位域,也就是说像version变量它的大小在内存中是占4bit,而不是8bit
    uint8_t     version : 4; //IP协议版本
    uint8_t     headLength : 4;//首部长度
    uint8_t     serverce;//区分服务
    uint16_t    totalLength;//总长度
    uint16_t    flagbit;//标识
    uint16_t    flag : 3;//标志
    uint16_t    fragmentOffset : 13;//片偏移
    char        timetoLive;//生存时间(跳数)
    uint8_t     protocol;//使用协议
    uint16_t    headcheckSum;//首部校验和
    uint32_t    srcIPadd;//源IP
    uint32_t    dstIPadd;//目的IP
    //可选项和填充我就不定义了
} IPhead;

//ICMP头
typedef struct ICMPhead
{
    uint8_t type;//类型
    uint8_t code;//代码
    uint16_t checkSum;//校验和
    uint16_t ident;//进程标识符
    uint16_t seqNum;//序号
} ICMPhead;
//ICMP回显请求报文(发送用)
typedef struct ICMP
{
    ICMPhead icmphead;//头部
    uint32_t timeStamp;//时间戳
    char     data[1000];//数据
};
//ICMP应答报文(接收用)
typedef struct ICMPReply
{
    IPhead iphead;//IP头
    ICMP icmpanswer;//ICMP报文
    char data[1024];//应答报文携带的数据缓冲区
} ICMPReply;


static uint16_t getCheckSum(void * protocol)
{
    uint32_t checkSum = 0;
    uint16_t* word = (uint16_t*)protocol;
    uint32_t size = sizeof(ICMP);

    while (size > 1)//用32位变量来存是因为要存储16位数相加可能发生的溢出情况,将溢出的最高位最后加到16位的最低位上
    {
        checkSum += *word++;
        // printf("[%s:%d]checkSum:0x%x\n", __FUNCTION__, __LINE__, checkSum);
        size -=sizeof(uint16_t);
    }
    if (size)
    {
        checkSum += *(uint8_t*)word;
        // printf("[%s:%d]checkSum:0x%x\n", __FUNCTION__, __LINE__, checkSum);
    }
    //二进制反码求和运算,先取反在相加和先相加在取反的结果是一样的,所以先全部相加在取反
    //计算加上溢出后的结果
    // while (checkSum >> 16) {
    //     checkSum = (checkSum >> 16) + (checkSum & 0xffff);
    // }
    checkSum =(checkSum >> 16) + (checkSum & 0xffff);
    // printf("[%s:%d]checkSum:0x%x\n", __FUNCTION__, __LINE__, checkSum);
    checkSum +=(checkSum >> 16);
    // printf("[%s:%d]checkSum:0x%x\n", __FUNCTION__, __LINE__, checkSum);
    //取反
    return (~checkSum);
}

static bool sendICMPReq(int mysocket, sockaddr_in &dstAddr,unsigned int num)
{
    struct timeval tv;
    //创建ICMP请求回显报文
    //设置回显请求
    ICMP myIcmp;//ICMP请求报文
    myIcmp.icmphead.type = 8;
    myIcmp.icmphead.code = 0;
    //设置初始检验和为0
    myIcmp.icmphead.checkSum = 0;
    //获得一个进程标识
    myIcmp.icmphead.ident = (uint16_t)getpid();
    //设置当前序号为0
    myIcmp.icmphead.seqNum = ++num;
    //保存发送时间
    gettimeofday(&tv, NULL);
    myIcmp.timeStamp = tv.tv_sec * 1000 + tv.tv_usec / 1000;
    // myIcmp.timeStamp = GetTickCount();
    //计算并且保存校验和
    myIcmp.icmphead.checkSum = getCheckSum((void*)&myIcmp);
    
    //发送报文
    printf("[%s:%d]checkSum:0x%x\n", __FUNCTION__, __LINE__, myIcmp.icmphead.checkSum);
    printf("[%s:%d]send timeStamp:%lld\n", __FUNCTION__, __LINE__, myIcmp.timeStamp);
    int len = sendto(mysocket, (char*)&myIcmp, sizeof(ICMP), 0, (sockaddr*)&dstAddr, sizeof(sockaddr_in));
    if (len < 0)
    {
        printf("sendto len:%d\n", len);
        std::cerr << "socket 发送错误:" << errno << std::endl;
        return false;
    }
    return true;
}

// int waitForSocket(int mysocket)
// {
//     //5S 等待套接字是否由数据
//     timeval timeOut;
//     fd_set  readfd;
//     readfd.fd_count = 1;
//     readfd.fd_array[0] = mysocket;
//     timeOut.tv_sec = 5;
//     timeOut.tv_usec = 0;
//     return (select(1, &readfd, NULL, NULL, &timeOut));
// }

static int waitForSocket(int fd, int write)
{
    int ev = write ? POLLOUT : POLLIN;
    struct pollfd p = { .fd = fd, .events = ev, .revents = 0 };
    int ret;
    ret = poll(&p, 1, 2000);
    return ret < 0 ? ret : p.revents & (ev | POLLERR | POLLHUP) ? 0 : 1;
}

static int64_t readICMPanswer(int mysocket, sockaddr_in &srcAddr, char &TTL)
{
    ICMPReply icmpReply;//接收应答报文
    int addrLen = sizeof(sockaddr_in);
    //接收应答
    int len = recvfrom(mysocket, (char*)&icmpReply, sizeof(ICMPReply), 0, (sockaddr*)&srcAddr, (socklen_t *)&addrLen);
    printf("[%s:%d]recv len:%d\n", __FUNCTION__, __LINE__, len);
    if (len < 0)
    {
        std::cerr << "socket 接收错误:" << errno << std::endl;
        return -1;
    }
    //读取校验并重新计算对比
    uint16_t checkSum = icmpReply.icmpanswer.icmphead.checkSum;
    //因为发出去的时候计算的校验和是0
    icmpReply.icmpanswer.icmphead.checkSum = 0;
    printf("[%s:%d]recv timeStamp:%lld\n", __FUNCTION__, __LINE__, icmpReply.icmpanswer.timeStamp);
    //重新计算
    if (checkSum == getCheckSum((void*)&icmpReply.icmpanswer)) {
        //获取TTL值
        TTL = icmpReply.iphead.timetoLive;
        return icmpReply.icmpanswer.timeStamp;
    }

    return -1;
}

static void doPing(int mysocket, sockaddr_in & srcAddr, sockaddr_in & dstAddr, int num)
{
    int64_t    timeSent;//发送时的时间
    uint32_t    timeElapsed;//延迟时间
    char        TTL;//跳数
    struct timeval tv;
    //发送ICMP回显请求
    sendICMPReq(mysocket, dstAddr, num);
    //等待数据
    int ret = waitForSocket(mysocket, 0);
    if (ret < 0)
    {
        std::cerr << "socket发生错误:" << errno << std::endl;
        return;
    }
    if (ret > 0)
    {
        std::cout << "请求超时:" << std::endl;
        return;
    }
    timeSent = readICMPanswer(mysocket, srcAddr, TTL);
    printf("[%s:%d]timeSent:%u\n", __FUNCTION__, __LINE__, (uint32_t)timeSent);
	if (!g_fd.is_open())
	{
		//std::cout << "not open log file dest url = " << log_name << std::endl;
		g_fd.open("ping.csv", std::ios::out | std::ios::trunc);
		if (!g_fd.is_open())
		{
			
			std::cout << "not open log file dest url = " << "" << std::endl;
		}
		//return false;
	}
	static int row_num = 0;
	char buf[1024] = {0};
	if (row_num == 0)
	{
		g_fd << "date, M/s" << std::endl;
		++row_num;
	}
	time64_datetime_format(::time(NULL), buf, '-', ' ', ':');
	g_fd << buf << ",";
    if (timeSent != -1) {
        gettimeofday(&tv, NULL);
		uint32_t timer_sec_usec = tv.tv_sec * 1000 + tv.tv_usec / 1000;
		printf("[cur_time = %lld]\n", timer_sec_usec);
        timeElapsed = (timer_sec_usec) - timeSent;
		//double speed_time = static_cast<double>( timeElapsed / 1000)
        //输出信息,注意TTL值是ASCII码,要进行转换
		double temp_speed =  static_cast<double>( 1000 * 8 / timeElapsed );
        std::cout << "[dst " << inet_ntoa(srcAddr.sin_addr) << " recv: byte= " << sizeof(((ICMP *)0)->data) << " time= " << timeElapsed << "ms TTL= " << fabs((int)TTL) << "]" << std::endl;
		std::cout << "[ speed = " << temp_speed  << " bps/ms]" << std::endl;
		uint32_t   bps_time = temp_speed * 1000 ;
		std::cout << "[ speed = " << bps_time << " bps/s]" << std::endl;
		//std::cout << "[ speed = " << bps_time / 1000000 << " Mbps/s]" << std::endl;
		g_fd << temp_speed << std::endl;
    }
    else {
        std::cout << "请求超时" << std::endl;
		g_fd << 0 << std::endl;
    }
	//g_fd.write('\n', 1);
	g_fd.flush();
	
	
}

static int ping(const char * dstIPaddr, short port)
{
    int      rawSocket;//socket
    sockaddr_in srcAddr;//socket源地址
    sockaddr_in dstAddr;//socket目的地址
    int         Ret;//捕获状态值
    char        TTL = '0';//跳数

    //生成一个套接字
    //TCP/IP协议族,RAW模式,ICMP协议
    //RAW创建的是一个原始套接字,最低可以访问到数据链路层的数据,也就是说在网络层的IP头的数据也可以拿到了。
    // rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    printf("[%s:%d]rawSocket:%d\n", __FUNCTION__, __LINE__, rawSocket);
    if (rawSocket < 0) {
        printf("[%s:%d]socket create err:<%d> rawSocket:%d\n", __FUNCTION__, __LINE__, errno, rawSocket);
        return -1;
    }
    //设置目标IP地址
    dstAddr.sin_addr.s_addr = inet_addr(dstIPaddr);
    //端口
    dstAddr.sin_port = htons(port);
    //协议族
    dstAddr.sin_family = AF_INET;

    //提示信息
    printf("ping address:%s bytes:%d\n", inet_ntoa(dstAddr.sin_addr), sizeof(((ICMP *)0)->data));

    //执行4次ping
    for (int i = 0; i < 4000000000; i++)
    {
        doPing(rawSocket, srcAddr, dstAddr, i);
        usleep(1000000);
    }


    close(rawSocket);

    return 0;
}



int main(int argc, char *argv[]) {
    char *addr = "127.0.0.1";
    printf("start ping ...\n");
    // ping("172.16.20.202", 0);
    if (argc > 1) {
        addr = argv[1];
    }
    ping(addr, 0);
    printf("end ping ...\n");
}

结语