基于Windows的Ping实验原理及C++实现

在这里插入图片描述

Ping的原理以及工作过程

Ping的原理

       ping 程序是用来探测主机到主机之间是否可通信,如果不能ping到某台主机,表明不能和这台主机建立连接。

       ping 使用的是ICMP协议,它发送ICMP回送请求消息给目的主机。ICMP协议规定:目的主机必须返回ICMP回送应答消息给源主机。如果源主机在一定时间内收到应答,则认为主机可达。

       ICMP协议通过IP协议发送的,IP协议是一种无连接的,不可靠的数据包协议。在Unix/Linux,序列号从0开始计数,依次递增。而Windows ping程序的ICMP序列号是没有规律。

Ping工作过程

        假定主机A的IP地址是192.168.1.1,主机B的IP地址是192.168.1.2,都在同一子网内,则当你在主机A上运行“Ping 192.168.1.2”后,都发生了些什么呢?

       首先,Ping命令会构建一个固定格式的ICMP请求数据包,然后由ICMP协议将这个数据包连同地址“192.168.1.2”一起交给IP层协议(和ICMP一样,实际上是一组后台运行的进程),IP层协议将以地址“192.168.1.2”作为目的地址,本机IP地址作为源地址,加上一些其他的控制信息,构建一个IP数据包,并在一个映射表中查找出IP地址192.168.1.2所对应的物理地址(也叫MAC地址,熟悉网卡配置的朋友不会陌生,这是数据链路层协议构建数据链路层的传输单元——帧所必需的),一并交给数据链路层。后者构建一个数据帧,目的地址是IP层传过来的物理地址,源地址则是本机的物理地址,还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。其中映射表由ARP实现。ARP(Address Resolution Protocol)是地址解析协议,是一种将IP地址转化成物理地址的协议。ARP具体说来就是将网络层(IP层,也就是相当于OSI的第三层)地址解析为数据连接层(MAC层,也就是相当于OSI的第二层)的MAC地址。

       主机B收到这个数据帧后,先检查它的目的地址,并和本机的物理地址对比,如符合,则接收;否则丢弃。接收后检查该数据帧,将IP数据包从帧中提取出来,交给本机的IP层协议。同样,IP层检查后,将有用的信息提取后交给ICMP协议,后者处理后,马上构建一个ICMP应答包,发送给主机A,其过程和主机A发送ICMP请求包到主机B一模一样。即先由IP地址,在网络层传输,然后再根据mac地址由数据链路层传送到目的主机。


ICMP协议

       前面讲到了,IP协议并不是一个可靠的协议,它不保证数据被送达,那么,自然的,保证数据送达的工作应该由其他的模块来完成。其中一个重要的模块就是ICMP(网络控制报文)协议。

       当传送IP数据包发生错误--比如主机不可达,路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机。给主机一个处理错误的机会,这 也就是为什么说建立在IP层以上的协议是可能做到安全的原因。ICMP数据包由8bit的错误类型和8bit的代码和16bit的校验和组成。而前 16bit就组成了ICMP所要传递的信息。

        尽管在大多数情况下,错误的包传送应该给出ICMP报文,但是在特殊情况下,是不产生ICMP错误报文的。如下

  • CMP差错报文不会产生ICMP差错报文(出IMCP查询报文)(防止IMCP的无限产生和传送)。

  • 目的地址是广播地址或多播地址的IP数据报。

  • 作为链路层广播的数据报。

  • 不是IP分片的第一片。

       源地址不是单个主机的数据报。这就是说,源地址不能为零地址、环回地址、广播地 址或多播地址。

       虽然里面的一些规定现在还不是很明白,但是所有的这一切规定,都是为了防止产生ICMP报文的无限传播而定义的。

       ICMP协议大致分为两类,一种是查询报文,一种是差错报文。其中查询报文有以下几种用途:

  • ping查询

  • 子网掩码查询(用于无盘工作站在初始化自身的时候初始化子网掩码)

  • 时间戳查询(可以用来同步时间)

  • 而差错报文则产生在数据传送发生错误的时候。就不赘述了。

ICMP报文结构

报头

        ICMP报文格式:IP首部(20字节)+8位类型+8位代码+16位校验和+(不同的类型和代码,格式也有所不同)
       ICMP协议在实际传输中数据包:20字节IP首部 + 8字节ICMP首部+ 1472字节<数据大小>38字节

        ICMP报头从IP报头的第160位开始(IP首部20字节)(除非使用了IP报头的可选部分)。ICMP报文包括8个字节的报头和长度可变的数据部分。对于不同的报文类型,报头的格式一般是不相同的,但是前3个字段(4个字节)对所有的ICMP报文都是相同的。
在这里插入图片描述

(1)类型(Type)字段,长度是1字节,用于定义报文类型。 (2)代码(Code)字段,长度是1字节,表示发送这个特定报文类型的原因。
(3)校验和(Checksum)字段,长度是2字节,用于数据报传输过程中的差错控制。与IP报头校验和的计算方法类似,不同的是其是对整个ICMP报文进行校验。
(4)报头的其余部分,其内容因不同的报文而不同。
(5)数据字段,其内容因不同的报文而不同。对于差错报告报文类型,数据字段包括ICMP差错信息和触发ICMP的整个原始数据报,其长度不超过576字节。

填充数据

        填充的数据紧接在ICMP报头的后面(以8位为一组):
Linux的"ping"工具填充的ICMP除了8个8位组的报头以外,默认情况下还另外填充数据使得总大小为64字节。
        Windows的"ping.exe"填充的ICMP除了8个8位组的报头以外,默认情况下还另外填充数据使得总大小为40字节。

ICMP的封装

        ICMP封装在IP报进行传输。ICMP报文本身被封装在IP数据报的数据区中,而这个IP数据报又被封装在帧数据中。在IP数据报报头中的协议(Protocol)字段设置成1,表示该数据是ICMP报文
![enter description here][2]
        其中,ICMP报文包含:ICMP首部(8字节)+产生差错的数据报IP首部+IP首部后的8个字节。具体如下图:

![enter description here][3]
        IP包首部要被传回的原因,因为IP首部中包含了协议字段,使得ICMP可以知道如何解释后面的8个字节。而IP首部后面的8字节(UDP的首部或者TCP首部,UDP和TCP首部的8个字节分别包含了16位的目的端口号和源端口号),根据源端口号就可以把差错报文与某个特定的用户进程关联。

ICMP的应用

ICMP的应用–PING

       ping可以说是ICMP的最著名的应用,当我们某一个网站上不去的时候。通常会ping一下这个网站。ping会回显出一些有用的信息。一般的信息如下:

       ping这个单词源自声纳定位,而这个程序的作用也确实如此,它利用ICMP协议包来侦测另一个主机是否可达。原理是用类型码为0的ICMP发请求,受到请求的主机则用类型码为8的ICMP回应。ping程序来计算间隔时间,并计算有多少个包被送达。用户就可以判断网络大致的情况。我们可以看到,ping给出来了传送的时间和TTL的数据。

       ping还给我们一个看主机到目的主机的路由的机会。这是因为,ICMP的ping请求数据报在每经过一个路由器的时候,路由器都会把自己的ip放到该数据报中。而目的主机则会把这个ip列表复制到回应icmp数据包中发回给主机。但是,无论如何,ip头所能纪录的路由列表是非常的有限。如果要观察路由, 我们还是需要使用更好的工具,就是要讲到的Traceroute(windows下面的名字叫做tracert)。

ICMP的应用–Traceroute

       Traceroute是用来侦测主机到目的主机之间所经路由情况的重要工具,也是最便利的工具。前面说到,尽管ping工具也可以进行侦测,但是,因为ip头的限制,ping不能完全的记录下所经过的路由器。所以Traceroute正好就填补了这个缺憾。

       Traceroute的原理是非常非常的有意思,它收到目的主机的IP后,首先给目的主机发送一个TTL=1(还记得TTL是什么吗?)的UDP(后面就知道UDP是什么了)数据包,而经过的第一个路由器收到这个数据包以后,就自动把TTL减1,而TTL变为0以后,路由器就把这个包给抛弃了,并同时产生一个主机不可达的ICMP数据报给主机。主机收到这个数据报以后再发一个TTL=2的UDP数据报给目的主机,然后刺激第二个路由器给主机发ICMP数据 报。如此往复直到到达目的主机。这样,traceroute就拿到了所有的路由器ip。从而避开了ip头只能记录有限路由IP的问题。

        有人要问,我怎么知道UDP到没到达目的主机呢?这就涉及一个技巧的问题,TCP和UDP协议有一个端口号定义,而普通的网络程序只监控少数的几个号码较 小的端口,比如说80,比如说23,等等。而traceroute发送的是端口号>30000(真变态)的UDP报,所以到达目的主机的时候,目的 主机只能发送一个端口不可达的ICMP数据报给主机。主机接到这个报告以后就知道,主机到了。

程序流程图

Created with Raphaël 2.2.0 开始 输入地址 地址正确 封装ICMP包 发送ICMP包 接收应答包 解析接收到的数据包 显示接收结果 继续发送 显示统计信息 结束 地址有误 yes no yes no

程序C++代码实现


#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;

//IP报头
typedef struct
{
	unsigned char hdr_len : 4;         //4位头部长度
	unsigned char version : 4;         //4位版本号
	unsigned char tos;               //8位服务类型
	unsigned short total_len;        //16位总长度
	unsigned short identifier;       //16位标识符
	unsigned short frag_and_flags;   //3位标志加13位片偏移
	unsigned char ttl;               //8位生存时间
	unsigned char protocol;          //8位上层协议号
	unsigned short checksum;         //16位效验和
	unsigned long sourceIP;          //32位源IP地址
	unsigned long destIP;            //32位目的IP地址
}IP_HEADER;

//ICMP报头
typedef struct
{
	BYTE type;     //8位类型字段
	BYTE code;     //8位代码字段
	USHORT cksum;  //16位效验和
	USHORT id;     //16位标识符
	USHORT seq;    //16位序列号
}ICMP_HEADER;


//报文解码结构
typedef struct
{
	USHORT usSeqNo;          //序列号
	DWORD dwRoundTripTime;   //返回时间
	in_addr dwIPaddr;        //返回报文的IP地址
}DECODE_RESULT;

//计算网际效验和函数
USHORT checksum(USHORT *pBuf, int iSize)
{
	unsigned long cksum = 0;
	while (iSize > 1)
	{
		cksum += *pBuf++;
		iSize -= sizeof(USHORT);
	}
	if (iSize)
	{
		cksum += *(USHORT*)pBuf;
	}
	cksum = (cksum >> 16) + (cksum & 0xffff);
	cksum += (cksum >> 16);
	return(USHORT)(~cksum);
}

//对数据包进行解码
BOOL DecodeIcmpResponse(char *pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
	//检查数据报大小的合法性
	IP_HEADER *pIpHdr = (IP_HEADER*)pBuf;
	int iIpHdrLen = pIpHdr->hdr_len * 4;
	if (iPacketSize < (int)(iIpHdrLen + sizeof(ICMP_HEADER)))
		return FALSE;
	//根据ICMP报文类型提取ID字段和序列号字段
	ICMP_HEADER *pIcmpHdr = (ICMP_HEADER*)(pBuf + iIpHdrLen);
	USHORT usID, usSquNo;
	if (pIcmpHdr->type == ICMP_ECHO_REPLY)    //ICMP回显应答报文
	{
		usID = pIcmpHdr->id;   //报文ID
		usSquNo = pIcmpHdr->seq;  //报文序列号
	}
	else if (pIcmpHdr->type == ICMP_TIMEOUT)   //ICMP超时差错报文
	{
		char *pInnerIpHdr = pBuf + iIpHdrLen + sizeof(ICMP_HEADER);  //载荷中的IP头
		int iInnerIPHdrLen = ((IP_HEADER*)pInnerIpHdr)->hdr_len * 4; //载荷中的IP头长
		ICMP_HEADER *pInnerIcmpHdr = (ICMP_HEADER*)(pInnerIpHdr + iInnerIPHdrLen);//载荷中的ICMP头
		usID = pInnerIcmpHdr->id;  //报文ID
		usSquNo = pInnerIcmpHdr->seq;  //序列号
	}
	else {
		return false;
	}
	//检查ID和序列号以确定收到期待数据报
	if (usID != (USHORT)GetCurrentProcessId() || usSquNo != DecodeResult.usSeqNo)
	{
		return false;
	}
	//记录IP地址并计算往返时间
	DecodeResult.dwIPaddr.s_addr = pIpHdr->sourceIP;
	DecodeResult.dwRoundTripTime = GetTickCount() - DecodeResult.dwRoundTripTime;

	//处理正确收到的ICMP数据报
	if (pIcmpHdr->type == ICMP_ECHO_REPLY || pIcmpHdr->type == ICMP_TIMEOUT)
	{
		return true;
	}
	else {
		return false;
	}
	return true;
}
int ping(const char* IpAddress) {
	//得到IP地址
	u_long ulDestIP = inet_addr(IpAddress);
	//转换不成功时按域名解析
	if (ulDestIP == INADDR_NONE)
	{
		hostent *pHostent = gethostbyname(IpAddress);
		if (pHostent)
		{
			ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
		}
		else
		{
			cout << "输入的IP地址或域名无效" << endl;
			WSACleanup();
			return 0;
		}
	}

	cout << "正在 Ping " << IpAddress << "具有 32 字节的数据:" << endl << endl;

	//填充目的端socket地址
	sockaddr_in destSockAddr;
	ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
	destSockAddr.sin_family = AF_INET;//AF_INET(IPv4)、
	destSockAddr.sin_addr.s_addr = ulDestIP;

	/* 指明socket的类型,Windows Sockets 2常见类型如下:

	SOCK_STREAM(流套接字,使用TCP协议)、

	SOCK_DGRAM(数据报套接字,使用UDP协议)、

	SOCK_RAW(原始套接字)、

	SOCK_RDM(提供可靠的消息数据报文,reliable message datagram)、

	SOCK_SEQPACKET(Provides a pseudo-stream packet based on datagrams,在UDP的基础上提供了伪流数据包)。

	protocol

	指明数据传输协议,该参数取决于af和type参数的类型。protocol参数在Winsock2.h and Wsrm.h定义。通常使用如下3中协议:

	IPPROTO_TCP(TCP协议,使用条件,af是AF_INET or AF_INET6、type是SOCK_STREAM )

	IPPROTO_UDP(UDP协议,使用条件,af是AF_INET or AF_INET6、type是SOCK_DGRAM)

	IPPROTO_RM(PGM(Pragmatic General Multicast,实际通用组播协议)协议,使用条件,af是AF_INET 、type是SOCK_RDM*/
	//ICMP协议在实际传输中数据包:20字节IP首部 + 8字节ICMP首部+ 1472字节<数据大小>38字节    
	
	//创建原始套接字
	SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, WSA_FLAG_OVERLAPPED);
	//超时时间
	int iTimeout = 3000;
	//接收超时
	setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char*)&iTimeout, sizeof(iTimeout));
	//发送超时
	setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char*)&iTimeout, sizeof(iTimeout));


	//构造ICMP回显请求消息,并以TTL递增的顺序发送报文
	//ICMP类型字段
	const BYTE ICMP_ECHO_REQUEST = 8;   //请求回显
	const BYTE ICMP_ECHO_REPLY = 0;     //回显应答
	const BYTE ICMP_TIMEOUT = 11;       //传输超时

	//其他常量定义
	const int DEF_ICMP_DATA_SIZE = 32;    //ICMP报文默认数据字段长度
	const int MAX_ICMP_PACKET_SIZE = 1024; //ICMP报文最大长度(包括报头)
	const DWORD DEF_ICMP_TIMEOUT = 3000;   //回显应答超时时间
	const int DEF_MAX_HOP = 4;            //最大跳站数(循环次数)

	//填充ICMP报文中每次发送时不变的字段
	char IcmpSendBuf[sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE]; //发送缓冲区
	memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf));     //初始化发送缓冲区
	char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE];        //接收缓冲区
	memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf));     //初始化接收缓冲区

	ICMP_HEADER *pIcmpHeader = (ICMP_HEADER*)IcmpSendBuf;
	pIcmpHeader->type = ICMP_ECHO_REQUEST;             //类型为请求回显
	pIcmpHeader->code = 0;                             //代码字段为0
	pIcmpHeader->id = (USHORT)GetCurrentProcessId();   //ID字段为当前进程号
	memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);   //数据字段
	USHORT usSeqNo = 0;                //ICMP报文序列号
	int iTTL = 64;                      //TTL初始值为1
	int r = 0, o = 0;
	BOOL bReachDestHost = FALSE;       //循环退出标志
	int iMaxHot = DEF_MAX_HOP;         //循环的最大次数
	DECODE_RESULT DecodeResult;      //传递给报文解码函数的结构化参数

	while (!bReachDestHost&&iMaxHot--)
	{
		//设置IP报头的TTL字段
		setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char*)&iTTL, sizeof(iTTL));

		//填充ICMP报文中每次发送变化的字段
		((ICMP_HEADER*)IcmpSendBuf)->cksum = 0;                 //效验和先置为0
		((ICMP_HEADER*)IcmpSendBuf)->seq = htons(usSeqNo++);    //填充序列号
		((ICMP_HEADER*)IcmpSendBuf)->cksum = checksum((USHORT*)IcmpSendBuf, sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE);  //计算效验和

		//记录序列号和当前时间
		DecodeResult.usSeqNo = ((ICMP_HEADER*)IcmpSendBuf)->seq;    //当前序号
		DecodeResult.dwRoundTripTime = GetTickCount();              //当前时间
		//发送TCP回显请求信息
		sendto(sockRaw, IcmpSendBuf, sizeof(IcmpSendBuf), 0, (sockaddr*)&destSockAddr, sizeof(destSockAddr));
		//接收ICMP差错报文并进行解析处理
		sockaddr_in from;              //对端socket地址
		int iFromLen = sizeof(from);     //地址结构大小
		int iReadDataLen;              //接收数据长度
		while (1)
		{
			//接收数据
			iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &iFromLen);
			if (iReadDataLen != SOCKET_ERROR)//有数据达到
			{
				//对数据包进行解码
				if (DecodeIcmpResponse(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, ICMP_TIMEOUT))
				{
					//到达目的地,退出循环
					if (DecodeResult.dwIPaddr.s_addr == destSockAddr.sin_addr.s_addr) {
						//输出IP地址
						if (DecodeResult.dwRoundTripTime)
							cout << "来自 " << inet_ntoa(DecodeResult.dwIPaddr) << " 的回复:字节32 时间=" << DecodeResult.dwRoundTripTime << "ms TTL:" << iTTL << endl;
						else
							cout << "来自 " << inet_ntoa(DecodeResult.dwIPaddr) << " 的回复:字节32 时间< 1ms TTL:" << iTTL << endl;
						r++;
						break;
					}

				}
			}
			else if (WSAGetLastError() == WSAETIMEDOUT) //接收超时,输出星号
			{
				cout << "请求超时。" << endl;
				o++;
				break;
			}
			else {
				break;
			}
		}
	}
	cout << endl;
	cout << IpAddress << " 的 Ping 统计信息:" << endl;
	cout << '\t' << "数据包:已发送=4,已接收=" << r << ",丢失=" << o << endl;
}


int main()
{
	//初始化Windows sockets网络环境
	/*指向WSADATA结构体的指针,lpWSAData返回了系统对Windows Sockets 的描述。*/
	WSADATA wsa;

	/*
	标识了用户调用的Winsock的版本号。高字节指明辅版本编号,低字节指明主版本编号。
	通常使用MAKEWORD来生成一个版本号。 
	当前Winsock sockets的版本号为2.2,用到的dll是 Ws2_32.dll。
	*/
	WSAStartup(MAKEWORD(2,2), &wsa);//WSAStartup 函数用于初始化供进程调用的Winsock相关的dll。
	char IpAddress[255];
	cout << "请输入一个IP地址或域名:";
	cin >> IpAddress;
	ping(IpAddress);
	return 0;
	
}

运行结果

输入一个正确的域名或IP地址,得到结果。
在这里插入图片描述

  • 22
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

都学点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值